Repository navigation
feat: add redis stream support for realtime messaging - #26
Rieranthony wants to merge 2 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughIntroduces Redis/ioredis–backed realtime streaming with new server handlers and client hooks. Adds API routes for visitor and dashboard SSE streams, forwards MESSAGE_CREATED events into both channels, and updates React providers to consume via new hooks. Refactors server handler to support filtering, middleware, and response headers; updates build config and dependencies. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant API as API Server
participant Emitter as Event Emitter
participant Dispatcher as Stream Dispatcher
participant RT as realtimeStreams (Redis)
participant Redis as Redis (Streams+Pub/Sub)
participant Handler as SSE Handler
participant ClientV as Visitor Client
participant ClientD as Dashboard Client
rect rgb(245,248,255)
note over API: Startup
API->>API: registerRealtimeRoutes(app)
API-->>User: Endpoints ready (/v1/realtime/visitor, /v1/realtime/dashboard)
end
rect rgb(245,255,245)
note over Emitter: On MESSAGE_CREATED
Emitter->>Dispatcher: forwardMessageEventToRealtimeStreams(event)
alt type == MESSAGE_CREATED
Dispatcher->>RT: emit visitor.MESSAGE_CREATED
Dispatcher->>RT: emit dashboard.MESSAGE_CREATED
RT->>Redis: XADD (trim) + PUBLISH JSON
end
end
rect rgb(255,248,245)
note over Handler: SSE subscriptions
ClientV->>Handler: GET /v1/realtime/visitor?websiteId... (SSE)
Handler->>Redis: duplicate, subscribe, replay
ClientD->>Handler: GET /v1/realtime/dashboard?websiteId... (SSE)
Handler->>Redis: duplicate, subscribe, replay
Redis-->>Handler: message payloads
Handler->>Handler: filter by channel/path
Handler-->>ClientV: event: MESSAGE_CREATED (visitor)
Handler-->>ClientD: event: MESSAGE_CREATED (dashboard)
end
sequenceDiagram
autonumber
participant Hook as useVisitorRealtime/useDashboardRealtime
participant URL as URL Builder
participant SSE as EventSource
participant App as App Handlers
Hook->>URL: Build endpoint + params (channel, visitorId/publicKey)
URL-->>Hook: Validated URL or error
alt valid
Hook->>SSE: new EventSource(url)
SSE-->>Hook: open
SSE-->>Hook: message (MESSAGE_CREATED)
Hook->>App: onMessageCreated(event)
else invalid
Hook-->>App: status=error, abort
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (7)
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (1)
87-89: Remove unnecessary Array.isArray check.
handleMessageCreatedis always a single function (fromcreateMessageCreatedHandler), not an array. The defensive Array.isArray normalization adds complexity without benefit.Apply this diff to simplify:
- const handlers = Array.isArray(handleMessageCreated) - ? handleMessageCreated - : [handleMessageCreated]; - - for (const handler of handlers) { - handler(event.payload, { event, context: realtimeContext }); - } + handleMessageCreated(event.payload, { event, context: realtimeContext });apps/api/src/realtime/streams.ts (1)
6-13: Consider deriving schema from RealtimeEvent type.The
messageCreatedEventSchemamanually duplicates the structure ofRealtimeEvent<"MESSAGE_CREATED">. If the event type evolves, this schema must be updated separately.For tighter coupling and reduced maintenance, consider using a type-to-schema utility or extracting a shared schema definition that both the type and validation use.
apps/api/src/realtime/routes.ts (1)
14-30: Handle origin parsing errors gracefully.The try-catch in
parseOriginHeaderssilently swallows URL parsing errors. While this prevents crashes, it may hide malformed origin headers.Consider logging the error when parsing fails:
} catch { + console.warn("[Realtime] Failed to parse origin header", origin); return { origin }; }packages/realtime/src/client/use-dashboard-realtime.ts (2)
13-18: Add JSDoc documentation for the public options interface.The
UseDashboardRealtimeOptionsinterface is part of the public API and should include JSDoc comments documenting each field, especially the nullability semantics and the purpose ofonMessageCreated.As per coding guidelines, add documentation like:
+/** + * Options for the useDashboardRealtime hook. + * + * @property websiteId - The website ID to subscribe to dashboard events for. Required for connection. + * @property endpoint - The realtime endpoint URL. Defaults to "/v1/realtime/dashboard". + * @property enabled - Whether the realtime connection should be active. Defaults to true. + * @property onMessageCreated - Callback invoked when a MESSAGE_CREATED event is received. + */ export type UseDashboardRealtimeOptions = { websiteId: string | null | undefined; endpoint?: string; enabled?: boolean; onMessageCreated?: (event: DashboardRealtimeEvent) => void; };
20-63: Add JSDoc documentation for the public hook.The
useDashboardRealtimehook is a public API and should include comprehensive JSDoc documenting its purpose, parameters, return value, and example usage.As per coding guidelines, add documentation like:
+/** + * React hook for subscribing to dashboard realtime events via Server-Sent Events (SSE). + * + * Establishes a realtime connection when enabled and required parameters are provided. + * Uses a ref-based handler to avoid stale closures when the callback changes. + * + * @param options - Configuration options for the realtime connection. + * @returns An object containing the connection status. + * + * @example + * ```tsx + * const { status } = useDashboardRealtime({ + * websiteId: "ws_123", + * onMessageCreated: (event) => { + * console.log("New message:", event.data); + * }, + * }); + * ``` + */ export function useDashboardRealtime({ websiteId, endpoint = "/v1/realtime/dashboard", enabled = true, onMessageCreated, }: UseDashboardRealtimeOptions) {packages/realtime/src/client/use-visitor-realtime.ts (2)
13-20: Add JSDoc documentation for the public options interface.Similar to
UseDashboardRealtimeOptions, this public interface should include JSDoc comments documenting each field and the authentication requirements.As per coding guidelines:
+/** + * Options for the useVisitorRealtime hook. + * + * @property websiteId - The website ID to subscribe to visitor events for. Required for connection. + * @property visitorId - The visitor ID for authentication. Required for connection. + * @property publicKey - The public key for authentication. Required for connection. + * @property endpoint - The realtime endpoint URL. Defaults to "/v1/realtime/visitor". + * @property enabled - Whether the realtime connection should be active. Defaults to true. + * @property onMessageCreated - Callback invoked when a MESSAGE_CREATED event is received. + */ export type UseVisitorRealtimeOptions = { websiteId: string | null | undefined; visitorId: string | null | undefined; publicKey: string | null | undefined; endpoint?: string; enabled?: boolean; onMessageCreated?: (event: VisitorRealtimeEvent) => void; };
22-68: Add JSDoc and consider extracting shared hook logic.The implementation is correct but duplicates significant logic from
useDashboardRealtime. Consider extracting the shared pattern into a generic factory function or base hook to reduce duplication.1. Add JSDoc documentation:
+/** + * React hook for subscribing to visitor realtime events via Server-Sent Events (SSE). + * + * Establishes a realtime connection when enabled and all required authentication parameters are provided. + * Uses a ref-based handler to avoid stale closures when the callback changes. + * + * @param options - Configuration options including authentication credentials. + * @returns An object containing the connection status. + * + * @example + * ```tsx + * const { status } = useVisitorRealtime({ + * websiteId: "ws_123", + * visitorId: "vis_456", + * publicKey: "pk_789", + * onMessageCreated: (event) => { + * console.log("New message:", event.data); + * }, + * }); + * ``` + */ export function useVisitorRealtime({2. Reduce code duplication:
Both
useDashboardRealtimeanduseVisitorRealtimeshare the same implementation pattern (handlerRef, useEffect, shouldEnable, events useMemo, useRealtime delegation). Consider extracting this into a generic helper to improve maintainability:function createRealtimeHook<TEvent, TSchema>(config: { eventNamespace: string; defaultEndpoint: string; buildParams: (options: any) => Record<string, any>; shouldEnableCheck: (options: any) => boolean; }) { // shared implementation }This would eliminate duplication while keeping the public API surfaces distinct for dashboard and visitor use cases.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (15)
apps/api/src/index.ts(2 hunks)apps/api/src/realtime/emitter.ts(3 hunks)apps/api/src/realtime/routes.ts(1 hunks)apps/api/src/realtime/stream-dispatcher.ts(1 hunks)apps/api/src/realtime/streams.ts(1 hunks)apps/web/package.json(1 hunks)apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx(2 hunks)packages/react/src/realtime/support-provider.tsx(4 hunks)packages/realtime/package.json(1 hunks)packages/realtime/src/client/index.ts(1 hunks)packages/realtime/src/client/use-dashboard-realtime.ts(1 hunks)packages/realtime/src/client/use-realtime.ts(3 hunks)packages/realtime/src/client/use-visitor-realtime.ts(1 hunks)packages/realtime/src/server/handler.ts(7 hunks)packages/realtime/src/server/realtime.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (15)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/global-rules.mdc)
**/*.{ts,tsx}: Manage shared component state via React Context
Implement full TypeScript support for all public APIs
Provide proper type definitions for all props and refs
Use appropriate generic constraints where necessary
Export type definitions for the public API
Avoid TypeScript enums; prefer const objects with 'as const' or Object.freeze
Provide clear, descriptive runtime errors for missing context/props and misuse
Expose dedicated hooks for context access that throw helpful errors when context is missing
Add explicit error messages for: outside-context usage, missing required props, invalid prop combinations, and validation failures
Document each component with JSDoc: purpose, props (required/optional), context requirements, example usage, pitfalls
State management: use React Context for global state, perform correct state updates, and handle side effects appropriately
Validation: implement runtime checks and validate prop combinations with clear errors
Create context with createContext<ContextType|null>(null) and a useContext hook that throws a clear error when missing
**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use empty type parameters in type aliases and interfaces
Don't use any or unknown as type constraints
Don't have unused function parameters
Don't have unused private class members
Don't return a value from a function that has a 'void' return type
Don't use the TypeScript directive @ts-ignore
Ensure switch-case statements are exhaustive (especially with unions)
Don't use TypeScript enums
Don't add type annotations to variables, parameters, and class properties initialized with literal expressions
Don't use TypeScript namespaces
Don't use non-null assertions with the ! postfix operator
Don't use parameter properties in class constructors
Don't use user-defined types (where discouraged by policy)
Use as const instead of literal types and type annotations
Use either T[] or Array consistently
Use consistent accessibili...
Files:
apps/api/src/realtime/routes.tspackages/realtime/src/client/index.tsapps/api/src/realtime/emitter.tsapps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-dashboard-realtime.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/realtime/src/server/handler.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/realtime.tsapps/api/src/index.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,jsx,ts,tsx}: Don't use theaccessKeyattribute on any HTML element
Use correct ISO language/country codes for the lang attribute
Don't hardcode sensitive data like API keys and tokens
Use === and !==
Files:
apps/api/src/realtime/routes.tspackages/realtime/src/client/index.tsapps/api/src/realtime/emitter.tsapps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-dashboard-realtime.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/realtime/src/server/handler.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/realtime.tsapps/api/src/index.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,ts}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,ts}: Don't use consecutive spaces in regular expression literals
Don't use the arguments object
Don't use the comma operator
Don't use unnecessary boolean casts
Don't use unnecessary callbacks with flatMap
Prefer for...of statements instead of Array.forEach
Don't use this and super in static contexts
Don't use unnecessary catch clauses
Don't use unnecessary constructors
Don't use unnecessary continue statements
Don't export empty modules that don't change anything
Don't use unnecessary escape sequences in regular expression literals
Don't use unnecessary labels
Don't use unnecessary nested block statements
Don't rename imports, exports, and destructured assignments to the same name
Don't use unnecessary string or template literal concatenation
Don't use String.raw in template literals when there are no escape sequences
Don't use useless case statements in switch statements
Avoid ternary operators when simpler alternatives exist
Don't use useless this aliasing
Don't initialize variables to undefined
Don't use void operators
Use Date.now() to get milliseconds since the Unix Epoch
Prefer .flatMap() over map().flat() when possible
Use literal property access instead of computed property access
Don't use parseInt() or Number.parseInt() when numeric literals suffice (binary/octal/hex)
Use concise optional chaining instead of chained logical expressions
Prefer regular expression literals over the RegExp constructor when possible
Don't use non–base-10 or underscore-separated number literal object member names
Remove redundant terms from logical expressions
Prefer while loops over for loops when no initializer/update is needed
Don't reassign const variables
Don't use constant expressions in conditions
Don't use Math.min/Math.max to clamp values when result is constant
Don't use empty character classes in regular expression literals
Don't use empty destructuring patterns
Don't call global object properties as functions
Don't declare functions and vars that are acce...
Files:
apps/api/src/realtime/routes.tspackages/realtime/src/client/index.tsapps/api/src/realtime/emitter.tsapps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/handler.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/realtime.tsapps/api/src/index.ts
**/*.{js,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,ts,tsx}: Don't create classes that only have static members
Don't return a value from a constructor
Don't use super() incorrectly inside classes; ensure super() is called in derived constructors
Ensure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass
Place getters and setters for the same property next to each other in class and object definitions
Ensure JSDoc comment lines start with a single asterisk, except for the first one
Don't use duplicate class members
Files:
apps/api/src/realtime/routes.tspackages/realtime/src/client/index.tsapps/api/src/realtime/emitter.tsapps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-dashboard-realtime.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/realtime/src/server/handler.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/realtime.tsapps/api/src/index.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,tsx,ts,jsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
Use arrow functions instead of function expressions
Files:
apps/api/src/realtime/routes.tspackages/realtime/src/client/index.tsapps/api/src/realtime/emitter.tsapps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-dashboard-realtime.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/realtime/src/server/handler.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/realtime.tsapps/api/src/index.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,ts,jsx,tsx}: Prevent duplicate polyfills from Polyfill.io
Don't use irregular whitespace characters
Files:
apps/api/src/realtime/routes.tspackages/realtime/src/client/index.tsapps/api/src/realtime/emitter.tsapps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-dashboard-realtime.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/realtime/src/server/handler.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/realtime.tsapps/api/src/index.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,ts,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
Don't redeclare variables, functions, classes, and types in the same scope
Files:
apps/api/src/realtime/routes.tspackages/realtime/src/client/index.tsapps/api/src/realtime/emitter.tsapps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-dashboard-realtime.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/realtime/src/server/handler.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/realtime.tsapps/api/src/index.tspackages/react/src/realtime/support-provider.tsx
packages/**
📄 CodeRabbit inference engine (AGENTS.md)
Put shared contracts and utilities in packages/* (e.g., core, types, transactional)
Files:
packages/realtime/src/client/index.tspackages/realtime/src/client/use-visitor-realtime.tspackages/realtime/package.jsonpackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/handler.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/realtime.tspackages/react/src/realtime/support-provider.tsx
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/global-rules.mdc)
**/*.tsx: Use the Compound Components pattern with namespaced exports (e.g., FormWizard.Root, FormWizard.Step) so pieces compose under a single parent
Ensure accessibility: keyboard navigation support
Ensure accessibility: include appropriate ARIA attributes
Ensure accessibility: use semantic markup for screen readers
Maintain proper focus management in interactive components
Structure components with forwardRef and typed props/refs as shown (ComponentRef, HTMLAttributes, ReactNode)
Root component acts as main context provider, manages global state, and handles initialization
Child components must be used within Root, read context via hooks, support asChild, and follow consistent props
Props ordering: list required props first; optional props have clear defaults; support common HTML attributes; expose className
Support composition with an asChild prop using Radix UI Slot to allow element replacement
Styling: support className, use data-* attributes for state, and allow style customization
Event handling: support standard DOM events, add custom events when needed, and manage propagation correctly
Base component pattern: forwardRef, choose Slot or intrinsic element via asChild, pass ref/className and spread props
Files:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/react/src/realtime/support-provider.tsx
**/*.{jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{jsx,tsx}: Don't set aria-hidden="true" on focusable elements
Don't add ARIA roles, states, and properties to elements that don't support them
Don't use distracting elements like or
Only use the scope prop on elements
Don't assign non-interactive ARIA roles to interactive HTML elements
Ensure label elements have text content and are associated with an input
Don't assign interactive ARIA roles to non-interactive HTML elements
Don't assign tabIndex to non-interactive HTML elements
Don't use positive integers for tabIndex property
Don't include the words "image", "picture", or "photo" in img alt prop
Don't use an explicit role that's the same as the element's implicit role
Static elements with click handlers must have a valid role attribute
Always include a <title> element within SVG elements
Provide meaningful alt text for elements that require alt text
Ensure anchors have content accessible to screen readers
Assign tabIndex to non-interactive elements only when using aria-activedescendant
Include all required ARIA attributes for elements with ARIA roles
Ensure ARIA properties are valid for the element's supported roles
Always include a type attribute for button elements
Elements with interactive roles and handlers must be focusable
Heading elements must have content accessible to screen readers (not hidden with aria-hidden)
Always include a lang attribute on the html element
Always include a title attribute for iframe elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Include caption tracks for audio and video elements
Prefer semantic elements instead of role attributes in JSX
Ensure all anchors are valid and navigable
Ensure all aria-* properties are valid
Use valid, non-abstract ARIA roles for elements with ARIA roles
Use valid ARIA state and property values
Use valid values for the autocomplete attribute on input elements
Don't use unnecessary fragmen...Files:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/react/src/realtime/support-provider.tsx**/*.{jsx,tsx,html}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{jsx,tsx,html}: Ensure the preconnect attribute is used when using Google Fonts
Don't use target="_blank" without rel="noopener"
Use a recommended display strategy with Google FontsFiles:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/react/src/realtime/support-provider.tsxapps/web/src/app/**
📄 CodeRabbit inference engine (AGENTS.md)
Place Next.js App Router pages/layouts/routes under apps/web/src/app
Files:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxapps/web/src/{app,components}/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Use snake_case filenames for React components and layouts in the web app
Files:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/react/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Develop headless React components (including and primitives) under packages/react/src/
Files:
packages/react/src/realtime/support-provider.tsxpackages/react/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/react-package-rules.mdc)
packages/react/**/*.{ts,tsx}: Components must render with no built-in styles (headless, unstyled)
Components must accept className, style, and asChild props to enable external styling/control
Do not use emotion, styled-components, or style-prop APIs; styling is via Tailwind/CSS through className/style
Use useRenderElement to create elements and encapsulate render logic (supporting intrinsic tags and render prop overrides)
Components with internal state must be controllable via props (e.g., value/defaultValue/onChange or isOpen/onOpenChange)
Expose internal state via a hook (e.g., useInput, useWindow) or a render prop function child
May reference Base UI logic internally, but never expose Base UI or MUI APIs; do not leak getRootProps/getInputProps; prefer useRenderElement
Prefer small, composable primitives (e.g., Provider, Bubble, Window, MessageList, Message, Input, SendButton, Typing, Event) over monolithic components
Provide a render prop API: render?: (props, state) => ReactElement, and allow className to be string or (state) => string
Support developer-owned rendering: components should not impose visual structure and should allow parent-driven markup via render prop/asChildFiles:
packages/react/src/realtime/support-provider.tsx🧠 Learnings (1)
📚 Learning: 2025-09-17T09:00:28.751Z
Learnt from: CR PR: cossistantcom/cossistant#0 File: CLAUDE.md:0-0 Timestamp: 2025-09-17T09:00:28.751Z Learning: Use Bun as the package manager and executor across the monorepo (install, dev, build, lint, format, type-check)Applied to files:
apps/web/package.json🧬 Code graph analysis (10)
apps/api/src/realtime/routes.ts (7)
packages/realtime/src/server/handler.ts (1)
handle(34-279)apps/api/src/realtime/streams.ts (1)
realtimeStreams(17-28)apps/api/src/lib/auth-validation.ts (2)
performAuthentication(366-415)AuthValidationError(25-33)apps/api/src/db/index.ts (1)
db(34-39)apps/api/src/db/queries/visitor.ts (1)
findVisitorForWebsite(141-160)apps/api/src/db/queries/session.ts (1)
getTRPCSession(104-111)apps/api/src/db/queries/website.ts (1)
getWebsiteByIdWithAccess(247-302)apps/api/src/realtime/emitter.ts (4)
packages/types/src/realtime-events.ts (1)
RealtimeEvent(108-115)apps/api/src/ws/socket.ts (2)
sendEventToVisitor(85-96)sendEventToWebsite(98-109)apps/api/src/realtime/stream-dispatcher.ts (1)
forwardMessageEventToRealtimeStreams(28-39)apps/api/src/ws/router.ts (1)
routeEvent(254-280)apps/api/src/realtime/stream-dispatcher.ts (1)
apps/api/src/realtime/streams.ts (1)
realtimeStreams(17-28)packages/realtime/src/client/use-visitor-realtime.ts (1)
packages/realtime/src/client/use-realtime.ts (1)
useRealtime(26-240)apps/api/src/realtime/streams.ts (3)
packages/types/src/realtime-events.ts (1)
RealtimeEvents(9-100)apps/api/src/redis.ts (1)
getRedis(7-39)packages/realtime/src/server/realtime.ts (2)
Realtime(121-129)Realtime(138-140)packages/realtime/src/client/use-dashboard-realtime.ts (1)
packages/realtime/src/client/use-realtime.ts (1)
useRealtime(26-240)apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (3)
packages/realtime/src/client/use-dashboard-realtime.ts (1)
useDashboardRealtime(20-63)packages/types/src/realtime-events.ts (1)
RealtimeEvent(108-115)apps/web/src/app/(dashboard)/[websiteSlug]/providers/events/handlers/message-created.ts (1)
handleMessageCreated(73-132)packages/realtime/src/server/handler.ts (2)
packages/realtime/src/types.ts (2)
UserEvent(8-12)SystemEvent(1-6)packages/realtime/src/server/realtime.ts (4)
Opts(11-16)Realtime(121-129)Realtime(138-140)channel(105-110)apps/api/src/index.ts (1)
apps/api/src/realtime/routes.ts (1)
registerRealtimeRoutes(142-152)packages/react/src/realtime/support-provider.tsx (4)
packages/react/src/provider.tsx (1)
useSupport(215-241)packages/types/src/realtime-events.ts (1)
RealtimeEvent(108-115)packages/react/src/realtime/use-realtime.ts (1)
useRealtime(68-144)packages/realtime/src/client/use-visitor-realtime.ts (1)
useVisitorRealtime(22-68)🔇 Additional comments (19)
apps/web/package.json (1)
109-109: No functional change; LGTM.End-of-file newline only. Nothing else to review here.
packages/realtime/src/client/use-realtime.ts (1)
91-127: LGTM! Robust URL construction with proper guards.The refactored EventSource URL construction adds important safety measures:
- Lines 91-93: Server-side guard prevents SSR-related crashes
- Lines 95-102: Try-catch around URL construction handles malformed endpoints gracefully
- Lines 104-107: Early exit on invalid URL with error state
- Lines 109-126: Structured URL parameter building is more maintainable than string concatenation
- Proper filtering of undefined/null param values
The new
endpointandparamsoptions expand the API surface while maintaining backward compatibility through default values.packages/realtime/src/server/handler.ts (5)
271-277: Verify responseHeaders don't override critical SSE headers.The
responseHeadersfunction result is spread after the default SSE headers (Line 376), which means user-provided headers will override defaults likeContent-Type,Cache-Control, and CORS headers. This could break SSE streaming if users accidentally override critical headers.Consider reversing the spread order or documenting this behavior:
- ...(init?.headers ?? {}), + ...(init?.headers ?? {}),Should be:
"Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Cache-Control", - ...(init?.headers ?? {}), + // User headers intentionally last to allow overrides + ...(init?.headers ?? {}),Or protect critical headers:
+ const userHeaders = init?.headers ?? {}; + const protectedKeys = ["Content-Type", "Cache-Control", "Connection"]; + const filteredHeaders = Object.fromEntries( + Object.entries(userHeaders).filter(([k]) => !protectedKeys.includes(k)) + ); "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive", "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Cache-Control", - ...(init?.headers ?? {}), + ...filteredHeaders,Document whether overriding SSE-critical headers is intentional or should be prevented.
81-263: LGTM! Well-structured streaming handler with proper lifecycle management.The refactored handler introduces excellent patterns:
- Lines 81-105:
safeEnqueueandsendUserEventproperly encapsulate event emission with filter integration and error handling- Lines 107-131:
closeStreamcomprehensively cleans up all resources (subscriber, timeouts, event listeners)- Lines 163-196:
replayMissedMessageshandles both cold starts (sending last cursor) and reconnects (replaying gap) correctly- Lines 200-220: Proper error and unsubscribe event handling with system events
- Lines 222-256: Message parsing with
parsePublishedPayloaddistinguishes ping vs. user events safely- Lines 259-263: Keep-alive mechanism publishes pings to maintain connection
The middleware, filter, and responseHeaders hooks provide excellent extensibility points for authentication, authorization, and custom headers.
281-359: LGTM! Robust parsing helpers with proper type guards.The parsing utilities are well-implemented:
- Lines 281-314:
parsePublishedPayloadsafely discriminates between ping and user event payloads with proper type narrowing- Lines 316-359:
parseStreamEntrycorrectly parses Redis stream entry format[id, [k1, v1, k2, v2, ...]]and extracts the JSON payload field- Both functions return
nullon parse failures, enabling graceful degradationThe type definitions (
PublishedPayload,StreamEntry,ParsedStreamEntry) clearly model the data structures.
141-143: Duplicate/connect/subscribe pattern is correct as implemented
The code explicitly awaitssubscriber.connect()before callingsubscribe(), matching ioredis documentation.
66-66: No change needed for subscriber type.ReturnType<typeof redis.duplicate>already resolves to theRedistype, so swapping to an explicitRedisimport is unnecessary.Likely an incorrect or invalid review comment.
packages/realtime/src/client/index.ts (1)
2-3: LGTM! Public API expanded with visitor and dashboard hooks.The new exports align with the PR objective to expose specialized realtime hooks for visitors and dashboards alongside the generic
use-realtimehook. This provides a cleaner API surface for consumers.packages/realtime/src/server/realtime.ts (1)
69-97: Approve Redis streaming enhancements
The updated JSON payload serialization,xaddwithMAXLEN ~100, and publishing with__stream_idalign with ioredis API and best practices.packages/realtime/package.json (1)
54-54: Ignore incorrect @types/react version warning The registry shows ^19.1.16 exists; no changes needed.Likely an incorrect or invalid review comment.
apps/api/src/realtime/emitter.ts (1)
150-154: Confirm fail-open behavior is intentional.The stream forwarding is wrapped in try-catch with error logging, but failures don't prevent the event from being routed via
routeEvent. This means if Redis streams are temporarily unavailable, events still propagate through the WebSocket system.Verify this fail-open approach aligns with your reliability requirements. If stream forwarding is critical, consider tracking failures (metrics/alerts) or implementing retry logic.
apps/api/src/realtime/routes.ts (1)
103-140: LGTM: Dashboard handler has proper authentication and authorization.The dashboard stream handler correctly:
- Validates channel is present (line 106-108)
- Authenticates via TRPC session (line 110-114)
- Authorizes website access using
getWebsiteByIdWithAccess(line 116-123)- Filters events by dashboard namespace (line 125-127)
- Includes CORS headers when origin is present (line 128-139)
This ensures only authenticated dashboard users with explicit website access can consume the dashboard stream.
apps/api/src/realtime/stream-dispatcher.ts (1)
7-9: Confirmed upstream websiteId validation. RealtimeEmitter.emit throws an error if websiteId is missing, so the early returns in stream-dispatcher are purely defensive.packages/realtime/src/client/use-dashboard-realtime.ts (3)
1-11: LGTM! Clean type definitions.The imports and type definitions are well-structured. The type alias
DashboardRealtimeEventprovides clear semantic meaning even though it's structurally identical toVisitorRealtimeEventin the companion file.
26-52: LGTM! Correct ref-based handler pattern.The implementation correctly uses a ref to store the latest handler, preventing stale closures. The
useMemooptimization and type safety withsatisfiesare both appropriate.
54-62: LGTM! Proper delegation to useRealtime.The hook correctly delegates to
useRealtimewith appropriate parameters, properly handling null/undefined conversions and conditional enabling.packages/realtime/src/client/use-visitor-realtime.ts (3)
1-11: LGTM! Consistent type definitions.The imports and type definitions follow the same pattern as the dashboard hook, maintaining consistency across the realtime client API.
30-56: LGTM! Consistent implementation pattern.The ref-based handler pattern and events construction correctly mirror the dashboard hook, ensuring consistent behavior across both realtime hook variants.
58-67: LGTM! Proper visitor-specific parameters.The hook correctly passes visitor authentication parameters (
visitorIdandpublicKey) to the underlyinguseRealtimeimplementation, with proper null-to-undefined conversion.
| const payloadVisitorId = | ||
| data.payload.message.visitorId ?? data.visitorId ?? null; | ||
|
|
||
| return !payloadVisitorId || payloadVisitorId === visitorId; |
There was a problem hiding this comment.
Verify visitor filter privacy: null visitorId events sent to all visitors.
The filter returns true when payloadVisitorId is null (line 99: !payloadVisitorId || ...), meaning MESSAGE_CREATED events without a visitorId in the payload are forwarded to all visitors on that website.
Confirm this is intentional. If MESSAGE_CREATED events should only reach the specific visitor who owns the conversation, change the logic to reject null visitorId:
Apply this diff to enforce strict visitor matching:
- return !payloadVisitorId || payloadVisitorId === visitorId;
+ return payloadVisitorId === visitorId;If null visitorId events are valid broadcast messages (e.g., system announcements), document this behavior clearly and verify the upstream event creator never accidentally omits visitorId for private conversations.
📝 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.
| const payloadVisitorId = | |
| data.payload.message.visitorId ?? data.visitorId ?? null; | |
| return !payloadVisitorId || payloadVisitorId === visitorId; | |
| const payloadVisitorId = | |
| data.payload.message.visitorId ?? data.visitorId ?? null; | |
| return payloadVisitorId === visitorId; |
🤖 Prompt for AI Agents
In apps/api/src/realtime/routes.ts around lines 96 to 99, the current filter
treats a null payloadVisitorId as a match and will forward MESSAGE_CREATED
events with no visitorId to all visitors; if messages must only reach their
owning visitor, change the condition to require a non-null visitor id and exact
match (i.e., only return true when payloadVisitorId is present and equals
visitorId), update any related tests, and add a short comment or docs clarifying
that null payloadVisitorId is a broadcast only if intentionally allowed.
| "@types/react": "^19.1.16", | ||
| "@types/react-dom": "^19.1.9", | ||
| "@upstash/redis": "^1.35.4", | ||
| "ioredis": "^5.4.2", |
There was a problem hiding this comment.
Move ioredis to dependencies or peerDependencies.
ioredis is listed as a devDependency but is imported and used at runtime in packages/realtime/src/server/realtime.ts (line 4) and packages/realtime/src/server/handler.ts. DevDependencies are not installed in production, which will cause runtime failures.
Apply this diff to move ioredis to dependencies:
"devDependencies": {
"zod": "^4.1.11",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
- "ioredis": "^5.4.2",
"typescript": "^5.9.2"
},
- "dependencies": {},
+ "dependencies": {
+ "ioredis": "^5.4.2"
+ },
"peerDependencies": {Alternatively, if you want consuming applications to provide their own Redis client:
"devDependencies": {
"zod": "^4.1.11",
"@types/node": "^24.6.0",
"@types/react": "^19.1.16",
"@types/react-dom": "^19.1.9",
- "ioredis": "^5.4.2",
"typescript": "^5.9.2"
},
"dependencies": {},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0",
+ "ioredis": "^5.0.0",
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}📝 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.
| "ioredis": "^5.4.2", | |
| "devDependencies": { | |
| "zod": "^4.1.11", | |
| "@types/node": "^24.6.0", | |
| "@types/react": "^19.1.16", | |
| "@types/react-dom": "^19.1.9", | |
| "typescript": "^5.9.2" | |
| }, | |
| "dependencies": { | |
| "ioredis": "^5.4.2" | |
| }, | |
| "peerDependencies": { | |
| "zod": "^3.25.0 || ^4.0.0", | |
| "react": ">=16.8.0", | |
| "react-dom": ">=16.8.0" | |
| } |
🤖 Prompt for AI Agents
In packages/realtime/package.json around line 56, ioredis is currently listed as
a devDependency but is required at runtime by
packages/realtime/src/server/realtime.ts and handler.ts; update package.json to
move ioredis from devDependencies into either "dependencies" (so it is installed
for production) or into "peerDependencies" (if you want consumers to supply
their own Redis client), and remove it from devDependencies; after updating, run
npm/yarn install and rebuild to ensure the runtime dependency is available.
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/realtime/src/client/use-realtime.ts (1)
225-239: Reconnect when endpoint/params changeCurrently only channel/enabled trigger reconnect; params/endpoint updates won’t. This can leave a stale connection.
Apply this diff:
- }, [channel, enabled]); + }, [channel, enabled, endpoint, JSON.stringify(params)]);
🧹 Nitpick comments (12)
packages/realtime/src/server/realtime.ts (1)
80-88: Verify MAXLEN constraint is sufficient for your traffic patterns.The MAXLEN approximate trimming (
~) set to 100 entries may be too small for high-traffic channels. This limits the stream to roughly 100 recent events, which could result in event loss if consumers fall behind or reconnect after a delay.Consider:
- Calculating expected message rate per channel and consumer reconnect latency to determine an appropriate MAXLEN value.
- If 100 entries is insufficient, increase the limit or implement a time-based expiration strategy (e.g., MAXLEN with a higher cap + TTL on the stream key).
Do you want me to generate a configuration option to make MAXLEN tunable per channel or deployment environment?
packages/realtime/tsdown.config.ts (2)
4-4: Broad entry pattern may include unintended files.The entry pattern
src/**/*.{ts,tsx}will match all TypeScript files undersrc/, including potential test files, fixtures, or internal utilities not intended for distribution. This could bloat the build output and expose internal implementation details.Consider explicitly listing entry points or using a more restrictive pattern:
- entry: ["src/**/*.{ts,tsx}"], + entry: [ + "src/client/index.ts", + "src/server/index.ts", + "src/client/use-realtime.ts", + "src/client/use-visitor-realtime.ts", + "src/client/use-dashboard-realtime.ts", + ],Or exclude test/internal files:
entry: ["src/**/*.{ts,tsx}", "!src/**/*.test.{ts,tsx}", "!src/**/__tests__/**"],
10-35: Uncomment and configure external dependencies.The commented-out
externalconfiguration suggests uncertainty about bundling strategy. For a library package, you should externalize peer dependencies and regular dependencies to avoid bundle bloat and version conflicts.Apply this diff to externalize dependencies:
- // external: [ - // // React ecosystem (peer dependencies) - // "react", - // "react-dom", - // "react/jsx-runtime", - - // // Peer dependencies - // "@tanstack/react-query", - // "motion", - // "motion/*", - // "tailwindcss", - - // // Internal workspace packages that should be bundled - // // Remove "@cossistant/*" to bundle core and types - // "@cossistant/core", - // "@cossistant/types", - - // // Regular dependencies that should stay external - // "react-use-websocket", - // "react-markdown", - // "zustand", - // "zustand/middleware", - // "nanoid", - // "ulid", - // "tailwind-merge", - // ], + external: [ + // React ecosystem (peer dependencies) + "react", + "react-dom", + "react/jsx-runtime", + + // Zod (peer dependency) + "zod", + /^zod\//, + + // ioredis (server-side, should be external for consumers to provide) + "ioredis", + ],This ensures:
- Client-side code doesn't bundle React (consumers provide it)
- Server-side code doesn't bundle ioredis (consumers provide it)
- Reduces bundle size and prevents version conflicts
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (1)
84-95: Clarify the dual-hook pattern rationale.The component now uses two separate realtime hooks:
useRealtimefor CONVERSATION_CREATED, CONVERSATION_SEEN, CONVERSATION_TYPING (lines 72-82)useDashboardRealtimefor MESSAGE_CREATED (lines 84-95)This dual-hook pattern raises questions:
- Connection efficiency: Does this establish two separate connections (WebSocket + SSE)? If so, this could increase resource usage.
- Event consistency: Why handle MESSAGE_CREATED separately? Are there technical constraints (e.g., MESSAGE_CREATED uses SSE while others use WebSocket)?
- Handler normalization: Lines 87-89 normalize
handleMessageCreatedto an array, suggesting uncertainty about its type. This should be resolved at the type level.Consider documenting why MESSAGE_CREATED requires a separate hook, or if possible, consolidate all events under a single realtime connection to reduce complexity.
Do you want me to investigate whether
useDashboardRealtimeanduseRealtimecan be unified, or if there's a technical reason (e.g., authentication scopes, transport protocols) requiring this split?apps/api/src/realtime/emitter.ts (1)
150-154: Replace console.error with project loggerAvoid console usage in JS/TS. Route through a logger (e.g., shared logger util) with structured context.
apps/api/src/realtime/routes.ts (1)
73-75: Replace console.error with project loggerUse a centralized logger; avoid console in server code.
packages/realtime/src/client/use-realtime.ts (1)
70-73: Avoid console in library code; provide a logger or gate by envReplace console.* with a pluggable logger (opts.logger?) or at least gate in non-production builds to reduce noise.
Also applies to: 81-83, 99-101, 158-159, 187-188, 199-200, 206-208
packages/realtime/src/client/use-dashboard-realtime.ts (2)
1-4: Add an explicit return type for the exported hook (per guidelines).Annotate the hook’s return using a shared type from use-realtime to keep the public API stable.
Apply:
import { useRealtime } from "./use-realtime"; +import type { UseRealtimeReturn } from "./use-realtime"; @@ -export function useDashboardRealtime({ +export function useDashboardRealtime({ websiteId, endpoint = "/v1/realtime/dashboard", enabled = true, onMessageCreated, -}: UseDashboardRealtimeOptions) { +}: UseDashboardRealtimeOptions): UseRealtimeReturn {Outside this file (in use-realtime.ts), export a compatible return type:
// in packages/realtime/src/client/use-realtime.ts export type UseRealtimeReturn = { status: ConnectionStatus };This keeps the hook’s surface explicit without leaking internals.
Also applies to: 20-26
5-18: Document the public API with JSDoc (types and hook).Add concise JSDoc for purpose, params, and example usage.
-export type DashboardRealtimeEvent = RealtimeEvent<"MESSAGE_CREATED">; +/** Dashboard stream payload for MESSAGE_CREATED events. */ +export type DashboardRealtimeEvent = RealtimeEvent<"MESSAGE_CREATED">; @@ -export type UseDashboardRealtimeOptions = { +/** + * Options for the dashboard realtime hook. + * - websiteId: channel identifier + * - endpoint: SSE endpoint (defaults to /v1/realtime/dashboard) + * - enabled: allow connecting + * - onMessageCreated: handler for MESSAGE_CREATED + */ +export type UseDashboardRealtimeOptions = { @@ -export function useDashboardRealtime({ +/** + * Subscribe to dashboard realtime events for a website. + * Connects only if both websiteId and onMessageCreated are provided. + */ +export function useDashboardRealtime({Also applies to: 20-25
packages/realtime/src/server/handler.ts (3)
177-195: Bound and chunk the XRANGE backfill to avoid large bursts.Unbounded XRANGE can pull a large backlog, blocking the event loop and flooding the client. Iterate with COUNT and advance the start id.
Example:
- const startId = `(${last_ack}`; - const missingMessages = await redis.xrange(streamKey, startId, "+"); - - sendConnectedEvent(); - - for (const entry of missingMessages) { - const parsed = parseStreamEntry(entry); - if (!parsed) { - continue; - } - const userEvent: UserEvent = { - data: parsed.data, - __event_path: parsed.__event_path, - __stream_id: parsed.id, - }; - await sendUserEvent(userEvent); - } + let next = `(${last_ack}`; + sendConnectedEvent(); + // Fetch in chunks to avoid long event loops on large backfills. + for (;;) { + const batch = await redis.xrange(streamKey, next, "+", "COUNT", 100); + if (!batch.length) break; + for (const entry of batch) { + const parsed = parseStreamEntry(entry); + if (!parsed) continue; + const userEvent: UserEvent = { + data: parsed.data, + __event_path: parsed.__event_path, + __stream_id: parsed.id, + }; + await sendUserEvent(userEvent); + } + next = `(${batch[batch.length - 1][0]}`; + }
371-377: Revisit default CORS headers for credentialed SSE.Wildcard ACAO conflicts with credentials and may be unsafe. Prefer delegating CORS via config.responseHeaders; at minimum add Vary: Origin and avoid setting "*" by default when auth is required.
Apply:
- "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Headers": "Cache-Control", + // Prefer per-route CORS via config.responseHeaders; set minimal defaults here. + "Vary": "Origin",If cross-origin with credentials is needed, set:
- Access-Control-Allow-Origin:
- Access-Control-Allow-Credentials: true
via responseHeaders({ request, channel }).
361-363: Reuse TextEncoder to reduce allocations.Small but hot path optimization.
Apply:
+const encoder = new TextEncoder(); function json<T>(data: SystemEvent | UserEvent<T>) { - return new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`); + return encoder.encode(`data: ${JSON.stringify(data)}\n\n`); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
apps/api/src/index.ts(2 hunks)apps/api/src/realtime/emitter.ts(3 hunks)apps/api/src/realtime/routes.ts(1 hunks)apps/api/src/realtime/stream-dispatcher.ts(1 hunks)apps/api/src/realtime/streams.ts(1 hunks)apps/web/package.json(1 hunks)apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx(2 hunks)packages/react/src/realtime/support-provider.tsx(4 hunks)packages/realtime/package.json(1 hunks)packages/realtime/src/client/index.ts(1 hunks)packages/realtime/src/client/use-dashboard-realtime.ts(1 hunks)packages/realtime/src/client/use-realtime.ts(3 hunks)packages/realtime/src/client/use-visitor-realtime.ts(1 hunks)packages/realtime/src/server/handler.ts(7 hunks)packages/realtime/src/server/realtime.ts(3 hunks)packages/realtime/tsdown.config.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (15)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/global-rules.mdc)
**/*.{ts,tsx}: Manage shared component state via React Context
Implement full TypeScript support for all public APIs
Provide proper type definitions for all props and refs
Use appropriate generic constraints where necessary
Export type definitions for the public API
Avoid TypeScript enums; prefer const objects with 'as const' or Object.freeze
Provide clear, descriptive runtime errors for missing context/props and misuse
Expose dedicated hooks for context access that throw helpful errors when context is missing
Add explicit error messages for: outside-context usage, missing required props, invalid prop combinations, and validation failures
Document each component with JSDoc: purpose, props (required/optional), context requirements, example usage, pitfalls
State management: use React Context for global state, perform correct state updates, and handle side effects appropriately
Validation: implement runtime checks and validate prop combinations with clear errors
Create context with createContext<ContextType|null>(null) and a useContext hook that throws a clear error when missing
**/*.{ts,tsx}: Don't use primitive type aliases or misleading types
Don't use empty type parameters in type aliases and interfaces
Don't use any or unknown as type constraints
Don't have unused function parameters
Don't have unused private class members
Don't return a value from a function that has a 'void' return type
Don't use the TypeScript directive @ts-ignore
Ensure switch-case statements are exhaustive (especially with unions)
Don't use TypeScript enums
Don't add type annotations to variables, parameters, and class properties initialized with literal expressions
Don't use TypeScript namespaces
Don't use non-null assertions with the ! postfix operator
Don't use parameter properties in class constructors
Don't use user-defined types (where discouraged by policy)
Use as const instead of literal types and type annotations
Use either T[] or Array consistently
Use consistent accessibili...
Files:
apps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/handler.tsapps/api/src/realtime/routes.tspackages/realtime/src/client/index.tspackages/realtime/tsdown.config.tsapps/api/src/realtime/emitter.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxapps/api/src/index.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/realtime.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,jsx,ts,tsx}: Don't use theaccessKeyattribute on any HTML element
Use correct ISO language/country codes for the lang attribute
Don't hardcode sensitive data like API keys and tokens
Use === and !==
Files:
apps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/handler.tsapps/api/src/realtime/routes.tspackages/realtime/src/client/index.tspackages/realtime/tsdown.config.tsapps/api/src/realtime/emitter.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxapps/api/src/index.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/realtime.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,ts}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,ts}: Don't use consecutive spaces in regular expression literals
Don't use the arguments object
Don't use the comma operator
Don't use unnecessary boolean casts
Don't use unnecessary callbacks with flatMap
Prefer for...of statements instead of Array.forEach
Don't use this and super in static contexts
Don't use unnecessary catch clauses
Don't use unnecessary constructors
Don't use unnecessary continue statements
Don't export empty modules that don't change anything
Don't use unnecessary escape sequences in regular expression literals
Don't use unnecessary labels
Don't use unnecessary nested block statements
Don't rename imports, exports, and destructured assignments to the same name
Don't use unnecessary string or template literal concatenation
Don't use String.raw in template literals when there are no escape sequences
Don't use useless case statements in switch statements
Avoid ternary operators when simpler alternatives exist
Don't use useless this aliasing
Don't initialize variables to undefined
Don't use void operators
Use Date.now() to get milliseconds since the Unix Epoch
Prefer .flatMap() over map().flat() when possible
Use literal property access instead of computed property access
Don't use parseInt() or Number.parseInt() when numeric literals suffice (binary/octal/hex)
Use concise optional chaining instead of chained logical expressions
Prefer regular expression literals over the RegExp constructor when possible
Don't use non–base-10 or underscore-separated number literal object member names
Remove redundant terms from logical expressions
Prefer while loops over for loops when no initializer/update is needed
Don't reassign const variables
Don't use constant expressions in conditions
Don't use Math.min/Math.max to clamp values when result is constant
Don't use empty character classes in regular expression literals
Don't use empty destructuring patterns
Don't call global object properties as functions
Don't declare functions and vars that are acce...
Files:
apps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/handler.tsapps/api/src/realtime/routes.tspackages/realtime/src/client/index.tspackages/realtime/tsdown.config.tsapps/api/src/realtime/emitter.tsapps/api/src/index.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/realtime.ts
**/*.{js,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,ts,tsx}: Don't create classes that only have static members
Don't return a value from a constructor
Don't use super() incorrectly inside classes; ensure super() is called in derived constructors
Ensure super() is called exactly once on every code path in a class constructor before this is accessed if the class has a superclass
Place getters and setters for the same property next to each other in class and object definitions
Ensure JSDoc comment lines start with a single asterisk, except for the first one
Don't use duplicate class members
Files:
apps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/handler.tsapps/api/src/realtime/routes.tspackages/realtime/src/client/index.tspackages/realtime/tsdown.config.tsapps/api/src/realtime/emitter.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxapps/api/src/index.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/realtime.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,tsx,ts,jsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
Use arrow functions instead of function expressions
Files:
apps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/handler.tsapps/api/src/realtime/routes.tspackages/realtime/src/client/index.tspackages/realtime/tsdown.config.tsapps/api/src/realtime/emitter.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxapps/api/src/index.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/realtime.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{js,ts,jsx,tsx}: Prevent duplicate polyfills from Polyfill.io
Don't use irregular whitespace characters
Files:
apps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/handler.tsapps/api/src/realtime/routes.tspackages/realtime/src/client/index.tspackages/realtime/tsdown.config.tsapps/api/src/realtime/emitter.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxapps/api/src/index.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/realtime.tspackages/react/src/realtime/support-provider.tsx
**/*.{js,ts,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
Don't redeclare variables, functions, classes, and types in the same scope
Files:
apps/api/src/realtime/stream-dispatcher.tspackages/realtime/src/client/use-visitor-realtime.tsapps/api/src/realtime/streams.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/handler.tsapps/api/src/realtime/routes.tspackages/realtime/src/client/index.tspackages/realtime/tsdown.config.tsapps/api/src/realtime/emitter.tsapps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxapps/api/src/index.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/realtime.tspackages/react/src/realtime/support-provider.tsx
packages/**
📄 CodeRabbit inference engine (AGENTS.md)
Put shared contracts and utilities in packages/* (e.g., core, types, transactional)
Files:
packages/realtime/src/client/use-visitor-realtime.tspackages/realtime/src/client/use-realtime.tspackages/realtime/src/server/handler.tspackages/realtime/src/client/index.tspackages/realtime/package.jsonpackages/realtime/tsdown.config.tspackages/realtime/src/client/use-dashboard-realtime.tspackages/realtime/src/server/realtime.tspackages/react/src/realtime/support-provider.tsx
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/global-rules.mdc)
**/*.tsx: Use the Compound Components pattern with namespaced exports (e.g., FormWizard.Root, FormWizard.Step) so pieces compose under a single parent
Ensure accessibility: keyboard navigation support
Ensure accessibility: include appropriate ARIA attributes
Ensure accessibility: use semantic markup for screen readers
Maintain proper focus management in interactive components
Structure components with forwardRef and typed props/refs as shown (ComponentRef, HTMLAttributes, ReactNode)
Root component acts as main context provider, manages global state, and handles initialization
Child components must be used within Root, read context via hooks, support asChild, and follow consistent props
Props ordering: list required props first; optional props have clear defaults; support common HTML attributes; expose className
Support composition with an asChild prop using Radix UI Slot to allow element replacement
Styling: support className, use data-* attributes for state, and allow style customization
Event handling: support standard DOM events, add custom events when needed, and manage propagation correctly
Base component pattern: forwardRef, choose Slot or intrinsic element via asChild, pass ref/className and spread props
Files:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/react/src/realtime/support-provider.tsx
**/*.{jsx,tsx}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{jsx,tsx}: Don't set aria-hidden="true" on focusable elements
Don't add ARIA roles, states, and properties to elements that don't support them
Don't use distracting elements like or
Only use the scope prop on elements
Don't assign non-interactive ARIA roles to interactive HTML elements
Ensure label elements have text content and are associated with an input
Don't assign interactive ARIA roles to non-interactive HTML elements
Don't assign tabIndex to non-interactive HTML elements
Don't use positive integers for tabIndex property
Don't include the words "image", "picture", or "photo" in img alt prop
Don't use an explicit role that's the same as the element's implicit role
Static elements with click handlers must have a valid role attribute
Always include a <title> element within SVG elements
Provide meaningful alt text for elements that require alt text
Ensure anchors have content accessible to screen readers
Assign tabIndex to non-interactive elements only when using aria-activedescendant
Include all required ARIA attributes for elements with ARIA roles
Ensure ARIA properties are valid for the element's supported roles
Always include a type attribute for button elements
Elements with interactive roles and handlers must be focusable
Heading elements must have content accessible to screen readers (not hidden with aria-hidden)
Always include a lang attribute on the html element
Always include a title attribute for iframe elements
Accompany onClick with at least one of: onKeyUp, onKeyDown, or onKeyPress
Accompany onMouseOver/onMouseOut with onFocus/onBlur
Include caption tracks for audio and video elements
Prefer semantic elements instead of role attributes in JSX
Ensure all anchors are valid and navigable
Ensure all aria-* properties are valid
Use valid, non-abstract ARIA roles for elements with ARIA roles
Use valid ARIA state and property values
Use valid values for the autocomplete attribute on input elements
Don't use unnecessary fragmen...Files:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/react/src/realtime/support-provider.tsx**/*.{jsx,tsx,html}
📄 CodeRabbit inference engine (.cursor/rules/ultracite.mdc)
**/*.{jsx,tsx,html}: Ensure the preconnect attribute is used when using Google Fonts
Don't use target="_blank" without rel="noopener"
Use a recommended display strategy with Google FontsFiles:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/react/src/realtime/support-provider.tsxapps/web/src/app/**
📄 CodeRabbit inference engine (AGENTS.md)
Place Next.js App Router pages/layouts/routes under apps/web/src/app
Files:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxapps/web/src/{app,components}/**/*.tsx
📄 CodeRabbit inference engine (AGENTS.md)
Use snake_case filenames for React components and layouts in the web app
Files:
apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsxpackages/react/src/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Develop headless React components (including and primitives) under packages/react/src/
Files:
packages/react/src/realtime/support-provider.tsxpackages/react/**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/react-package-rules.mdc)
packages/react/**/*.{ts,tsx}: Components must render with no built-in styles (headless, unstyled)
Components must accept className, style, and asChild props to enable external styling/control
Do not use emotion, styled-components, or style-prop APIs; styling is via Tailwind/CSS through className/style
Use useRenderElement to create elements and encapsulate render logic (supporting intrinsic tags and render prop overrides)
Components with internal state must be controllable via props (e.g., value/defaultValue/onChange or isOpen/onOpenChange)
Expose internal state via a hook (e.g., useInput, useWindow) or a render prop function child
May reference Base UI logic internally, but never expose Base UI or MUI APIs; do not leak getRootProps/getInputProps; prefer useRenderElement
Prefer small, composable primitives (e.g., Provider, Bubble, Window, MessageList, Message, Input, SendButton, Typing, Event) over monolithic components
Provide a render prop API: render?: (props, state) => ReactElement, and allow className to be string or (state) => string
Support developer-owned rendering: components should not impose visual structure and should allow parent-driven markup via render prop/asChildFiles:
packages/react/src/realtime/support-provider.tsx🧠 Learnings (2)
📚 Learning: 2025-09-17T09:00:28.751Z
Learnt from: CR PR: cossistantcom/cossistant#0 File: CLAUDE.md:0-0 Timestamp: 2025-09-17T09:00:28.751Z Learning: Use Bun as the package manager and executor across the monorepo (install, dev, build, lint, format, type-check)Applied to files:
apps/web/package.json📚 Learning: 2025-09-17T09:00:28.751Z
Learnt from: CR PR: cossistantcom/cossistant#0 File: CLAUDE.md:0-0 Timestamp: 2025-09-17T09:00:28.751Z Learning: Applies to packages/types/src/**/*.ts : Keep shared TypeScript types and Zod schemas in packages/types/src/ and update them alongside schema changesApplied to files:
packages/realtime/tsdown.config.ts🧬 Code graph analysis (10)
apps/api/src/realtime/stream-dispatcher.ts (1)
apps/api/src/realtime/streams.ts (1)
realtimeStreams(17-28)packages/realtime/src/client/use-visitor-realtime.ts (1)
packages/realtime/src/client/use-realtime.ts (1)
useRealtime(26-240)apps/api/src/realtime/streams.ts (3)
packages/types/src/realtime-events.ts (1)
RealtimeEvents(9-100)apps/api/src/redis.ts (1)
getRedis(7-39)packages/realtime/src/server/realtime.ts (2)
Realtime(121-129)Realtime(138-140)packages/realtime/src/server/handler.ts (2)
packages/realtime/src/types.ts (2)
UserEvent(8-12)SystemEvent(1-6)packages/realtime/src/server/realtime.ts (4)
Opts(11-16)Realtime(121-129)Realtime(138-140)channel(105-110)apps/api/src/realtime/routes.ts (6)
packages/realtime/src/server/handler.ts (1)
handle(34-279)apps/api/src/realtime/streams.ts (1)
realtimeStreams(17-28)apps/api/src/lib/auth-validation.ts (2)
performAuthentication(366-415)AuthValidationError(25-33)apps/api/src/db/queries/visitor.ts (1)
findVisitorForWebsite(141-160)apps/api/src/db/queries/session.ts (1)
getTRPCSession(104-111)apps/api/src/db/queries/website.ts (1)
getWebsiteByIdWithAccess(247-302)apps/api/src/realtime/emitter.ts (4)
packages/types/src/realtime-events.ts (1)
RealtimeEvent(108-115)apps/api/src/ws/socket.ts (2)
sendEventToVisitor(85-96)sendEventToWebsite(98-109)apps/api/src/realtime/stream-dispatcher.ts (1)
forwardMessageEventToRealtimeStreams(28-39)apps/api/src/ws/router.ts (1)
routeEvent(254-280)apps/web/src/app/(dashboard)/[websiteSlug]/providers/realtime.tsx (3)
packages/realtime/src/client/use-dashboard-realtime.ts (1)
useDashboardRealtime(20-63)packages/types/src/realtime-events.ts (1)
RealtimeEvent(108-115)apps/web/src/app/(dashboard)/[websiteSlug]/providers/events/handlers/message-created.ts (1)
handleMessageCreated(73-132)apps/api/src/index.ts (1)
apps/api/src/realtime/routes.ts (1)
registerRealtimeRoutes(142-152)packages/realtime/src/client/use-dashboard-realtime.ts (1)
packages/realtime/src/client/use-realtime.ts (1)
useRealtime(26-240)packages/react/src/realtime/support-provider.tsx (4)
packages/react/src/provider.tsx (1)
useSupport(215-241)packages/types/src/realtime-events.ts (1)
RealtimeEvent(108-115)packages/react/src/realtime/use-realtime.ts (1)
useRealtime(68-144)packages/realtime/src/client/use-visitor-realtime.ts (1)
useVisitorRealtime(22-68)🔇 Additional comments (11)
packages/realtime/package.json (1)
56-56: LGTM! Dependency swap aligns with the PR objective.The switch from
@upstash/redistoioredisversion 5.4.2 is consistent with the PR's goal to adopt ioredis-based XADD/XRANGE handling. Based on learnings, ioredis 5.4.2 is the current stable release with TypeScript-first support.apps/web/package.json (1)
109-109: LGTM! Standard formatting improvement.The trailing newline follows common file-ending conventions.
packages/realtime/src/client/index.ts (1)
2-3: LGTM! Clean hook exports.The new
useVisitorRealtimeanduseDashboardRealtimehooks are properly exported alongside the existinguseRealtimeexport, enabling consumers to leverage the specialized SSE-based realtime functionality introduced in this PR.packages/react/src/realtime/support-provider.tsx (2)
33-43: LGTM! Proper memoization with useCallback.The
handleMessageCreatedcallback is correctly memoized withuseCallbackand includes appropriate dependencies ([client, website?.id]). The site ownership check (line 35-37) prevents processing events from other websites.
108-113: Document the dual-hook pattern rationale.Similar to the dashboard provider, this component now uses two separate realtime hooks:
useRealtimefor CONVERSATION_SEEN and CONVERSATION_TYPING (lines 99-104)useVisitorRealtimefor MESSAGE_CREATED (lines 108-113)This creates the same architectural questions as in the dashboard provider:
- Connection efficiency: Two separate connections (WebSocket + SSE)?
- Event segregation: Why is MESSAGE_CREATED handled differently?
- Consistency: Both providers follow this pattern, suggesting it's intentional, but the reasoning should be documented.
Based on the PR objectives mentioning "continues to run websocket handlers for other events," it appears MESSAGE_CREATED was intentionally migrated to SSE while other events remain on WebSocket. If this is correct, consider adding a comment explaining this architecture decision.
// MESSAGE_CREATED events are delivered via SSE (useVisitorRealtime) // for better scalability and connection persistence. // Other events (SEEN, TYPING) continue using WebSocket (useRealtime) // for lower latency bi-directional communication. useVisitorRealtime({ websiteId: realtimeContext.websiteId, visitorId: realtimeContext.visitorId, publicKey: publicKey ?? null, onMessageCreated: handleMessageCreated, });apps/api/src/index.ts (1)
143-144: Authentication present in realtime routes visitorStreamHandler enforcespublicKey&visitorIdquery params; dashboardStreamHandler validates session viagetTRPCSessionand checksuser.id.apps/api/src/realtime/emitter.ts (1)
130-137: Event construction looks correctIncludes required fields; timestamp via Date.now(); types align.
apps/api/src/realtime/streams.ts (1)
6-13: Redis-backed Realtime schema and wiring LGTMSchema matches MESSAGE_CREATED. Namespaces and verbosity are sane.
Also applies to: 17-29
apps/api/src/realtime/stream-dispatcher.ts (1)
28-39: Forwarder implementation is solidType guard + concurrent emits with allSettled. Good non-blocking behavior.
packages/realtime/src/client/use-visitor-realtime.ts (1)
22-68: Hook API and behavior look goodGated enablement, stable handler ref, and events mapping are sound.
packages/realtime/src/client/use-dashboard-realtime.ts (1)
42-51: Sanity-check event path and client re-export
- Verify the server emits the
["dashboard","MESSAGE_CREATED"]event path.- Ensure
use-dashboard-realtimeis re-exported inpackages/realtime/src/client/index.ts.
| const visitorStreamHandler = handle({ | ||
| realtime: realtimeStreams, | ||
| middleware: async ({ request, channel }) => { | ||
| const url = new URL(request.url); | ||
| const publicKey = url.searchParams.get("publicKey"); | ||
| const visitorId = url.searchParams.get("visitorId"); | ||
|
|
||
| if (!publicKey || !visitorId) { | ||
| return new Response("Missing credentials", { status: 401 }); | ||
| } | ||
|
|
||
| try { | ||
| const authResult = await performAuthentication( | ||
| undefined, | ||
| publicKey, | ||
| db, | ||
| parseOriginHeaders(request) | ||
| ); | ||
|
|
||
| const websiteId = authResult.apiKey.website?.id; | ||
|
|
||
| if (!websiteId) { | ||
| return new Response("Website not found", { status: 403 }); | ||
| } | ||
|
|
||
| if (channel !== websiteId) { | ||
| return new Response("Channel mismatch", { status: 403 }); | ||
| } | ||
|
|
||
| const visitor = await findVisitorForWebsite(db, { | ||
| visitorId, | ||
| websiteId, | ||
| }); | ||
|
|
||
| if (!visitor || visitor.blockedAt) { | ||
| return new Response("Visitor not allowed", { status: 403 }); | ||
| } | ||
| } catch (error) { | ||
| if (error instanceof AuthValidationError) { | ||
| return new Response(error.message, { status: error.statusCode }); | ||
| } | ||
| console.error("[Realtime] Visitor auth failed", error); | ||
| return new Response("Authentication failed", { status: 401 }); | ||
| } | ||
| }, | ||
| filter: async ({ event, request }) => { | ||
| if (!Array.isArray(event.__event_path)) { | ||
| return false; | ||
| } | ||
|
|
||
| if (event.__event_path[0] !== "visitor") { | ||
| return false; | ||
| } | ||
|
|
||
| const data = event.data as RealtimeEvent; | ||
| if (!data || data.type !== "MESSAGE_CREATED") { | ||
| return false; | ||
| } | ||
|
|
||
| const visitorId = new URL(request.url).searchParams.get("visitorId"); | ||
| if (!visitorId) { | ||
| return false; | ||
| } | ||
|
|
||
| const payloadVisitorId = | ||
| data.payload.message.visitorId ?? data.visitorId ?? null; | ||
|
|
||
| return !payloadVisitorId || payloadVisitorId === visitorId; | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Add CORS headers to visitor stream handler
Cross-origin EventSource requires ACAO. Mirror dashboard’s approach to avoid connection failures.
Apply this diff to add responseHeaders:
const visitorStreamHandler = handle({
realtime: realtimeStreams,
middleware: async ({ request, channel }) => {
…
},
filter: async ({ event, request }) => {
…
- },
+ },
+ responseHeaders: ({ request }) => {
+ const origin = request.headers.get("origin");
+ if (!origin) {
+ return undefined;
+ }
+ return {
+ "Access-Control-Allow-Origin": origin,
+ "Access-Control-Allow-Credentials": "true",
+ Vary: "Origin",
+ } satisfies HeadersInit;
+ },
});📝 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.
| const visitorStreamHandler = handle({ | |
| realtime: realtimeStreams, | |
| middleware: async ({ request, channel }) => { | |
| const url = new URL(request.url); | |
| const publicKey = url.searchParams.get("publicKey"); | |
| const visitorId = url.searchParams.get("visitorId"); | |
| if (!publicKey || !visitorId) { | |
| return new Response("Missing credentials", { status: 401 }); | |
| } | |
| try { | |
| const authResult = await performAuthentication( | |
| undefined, | |
| publicKey, | |
| db, | |
| parseOriginHeaders(request) | |
| ); | |
| const websiteId = authResult.apiKey.website?.id; | |
| if (!websiteId) { | |
| return new Response("Website not found", { status: 403 }); | |
| } | |
| if (channel !== websiteId) { | |
| return new Response("Channel mismatch", { status: 403 }); | |
| } | |
| const visitor = await findVisitorForWebsite(db, { | |
| visitorId, | |
| websiteId, | |
| }); | |
| if (!visitor || visitor.blockedAt) { | |
| return new Response("Visitor not allowed", { status: 403 }); | |
| } | |
| } catch (error) { | |
| if (error instanceof AuthValidationError) { | |
| return new Response(error.message, { status: error.statusCode }); | |
| } | |
| console.error("[Realtime] Visitor auth failed", error); | |
| return new Response("Authentication failed", { status: 401 }); | |
| } | |
| }, | |
| filter: async ({ event, request }) => { | |
| if (!Array.isArray(event.__event_path)) { | |
| return false; | |
| } | |
| if (event.__event_path[0] !== "visitor") { | |
| return false; | |
| } | |
| const data = event.data as RealtimeEvent; | |
| if (!data || data.type !== "MESSAGE_CREATED") { | |
| return false; | |
| } | |
| const visitorId = new URL(request.url).searchParams.get("visitorId"); | |
| if (!visitorId) { | |
| return false; | |
| } | |
| const payloadVisitorId = | |
| data.payload.message.visitorId ?? data.visitorId ?? null; | |
| return !payloadVisitorId || payloadVisitorId === visitorId; | |
| }, | |
| }); | |
| const visitorStreamHandler = handle({ | |
| realtime: realtimeStreams, | |
| middleware: async ({ request, channel }) => { | |
| const url = new URL(request.url); | |
| const publicKey = url.searchParams.get("publicKey"); | |
| const visitorId = url.searchParams.get("visitorId"); | |
| if (!publicKey || !visitorId) { | |
| return new Response("Missing credentials", { status: 401 }); | |
| } | |
| try { | |
| const authResult = await performAuthentication( | |
| undefined, | |
| publicKey, | |
| db, | |
| parseOriginHeaders(request) | |
| ); | |
| const websiteId = authResult.apiKey.website?.id; | |
| if (!websiteId) { | |
| return new Response("Website not found", { status: 403 }); | |
| } | |
| if (channel !== websiteId) { | |
| return new Response("Channel mismatch", { status: 403 }); | |
| } | |
| const visitor = await findVisitorForWebsite(db, { | |
| visitorId, | |
| websiteId, | |
| }); | |
| if (!visitor || visitor.blockedAt) { | |
| return new Response("Visitor not allowed", { status: 403 }); | |
| } | |
| } catch (error) { | |
| if (error instanceof AuthValidationError) { | |
| return new Response(error.message, { status: error.statusCode }); | |
| } | |
| console.error("[Realtime] Visitor auth failed", error); | |
| return new Response("Authentication failed", { status: 401 }); | |
| } | |
| }, | |
| filter: async ({ event, request }) => { | |
| if (!Array.isArray(event.__event_path)) { | |
| return false; | |
| } | |
| if (event.__event_path[0] !== "visitor") { | |
| return false; | |
| } | |
| const data = event.data as RealtimeEvent; | |
| if (!data || data.type !== "MESSAGE_CREATED") { | |
| return false; | |
| } | |
| const visitorId = new URL(request.url).searchParams.get("visitorId"); | |
| if (!visitorId) { | |
| return false; | |
| } | |
| const payloadVisitorId = | |
| data.payload.message.visitorId ?? data.visitorId ?? null; | |
| return !payloadVisitorId || payloadVisitorId === visitorId; | |
| }, | |
| responseHeaders: ({ request }) => { | |
| const origin = request.headers.get("origin"); | |
| if (!origin) { | |
| return undefined; | |
| } | |
| return { | |
| "Access-Control-Allow-Origin": origin, | |
| "Access-Control-Allow-Credentials": "true", | |
| Vary: "Origin", | |
| } satisfies HeadersInit; | |
| }, | |
| }); |
🤖 Prompt for AI Agents
In apps/api/src/realtime/routes.ts around lines 32 to 101, the visitor stream
handler lacks CORS response headers which breaks cross-origin EventSource
connections; add a responseHeaders property to the handler that mirrors the
dashboard approach: set "Access-Control-Allow-Origin" to the request origin
(fallback to "*" if absent), include "Access-Control-Allow-Credentials": "true",
and add "Vary": "Origin" so browsers accept the EventSource connection from
other origins.
| const payloadVisitorId = | ||
| data.payload.message.visitorId ?? data.visitorId ?? null; | ||
|
|
||
| return !payloadVisitorId || payloadVisitorId === visitorId; | ||
| }, |
There was a problem hiding this comment.
Tighten visitor filter to prevent unintended broadcasts
Allowing null payloadVisitorId sends events to all visitors. Require an exact match to the requesting visitor.
Apply this diff:
- const payloadVisitorId =
- data.payload.message.visitorId ?? data.visitorId ?? null;
-
- return !payloadVisitorId || payloadVisitorId === visitorId;
+ const payloadVisitorId =
+ data.payload.message.visitorId ?? data.visitorId ?? null;
+
+ if (!payloadVisitorId) {
+ return false;
+ }
+ return payloadVisitorId === visitorId;📝 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.
| const payloadVisitorId = | |
| data.payload.message.visitorId ?? data.visitorId ?? null; | |
| return !payloadVisitorId || payloadVisitorId === visitorId; | |
| }, | |
| const payloadVisitorId = | |
| data.payload.message.visitorId ?? data.visitorId ?? null; | |
| if (!payloadVisitorId) { | |
| return false; | |
| } | |
| return payloadVisitorId === visitorId; |
🤖 Prompt for AI Agents
In apps/api/src/realtime/routes.ts around lines 96 to 100, the current filter
treats a null/undefined payloadVisitorId as a wildcard and broadcasts to all
visitors; change the condition so only an exact match passes. Replace the
existing logic that computes payloadVisitorId and returns !payloadVisitorId ||
payloadVisitorId === visitorId with a comparison that returns payloadVisitorId
!== null && payloadVisitorId !== undefined && payloadVisitorId === visitorId (or
simply return payloadVisitorId === visitorId), ensuring null/undefined values do
not match and thus prevent unintended broadcasts.
| keepaliveInterval = setInterval(async () => { | ||
| await redis.publish(`channel:${channel}`, { | ||
| type: "ping", | ||
| timestamp: Date.now(), | ||
| }); | ||
| await redis.publish( | ||
| streamKey, | ||
| JSON.stringify({ type: "ping", timestamp: Date.now() }) | ||
| ); | ||
| }, 10_000); |
There was a problem hiding this comment.
Avoid O(n²) ping fan-out; send per-connection pings instead of publishing.
Current keepalive publishes to the shared channel; with N clients, you produce N messages that each client receives → O(n²) traffic. Emit pings directly to this SSE stream.
Apply:
- keepaliveInterval = setInterval(async () => {
- await redis.publish(
- streamKey,
- JSON.stringify({ type: "ping", timestamp: Date.now() })
- );
- }, 10_000);
+ keepaliveInterval = setInterval(() => {
+ safeEnqueue(
+ json({ type: "ping", timestamp: Date.now() } as SystemEvent)
+ );
+ }, 10_000);You can keep the subscriber ping handling for externally-generated pings; this change prevents per-connection ping storms.
📝 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.
| keepaliveInterval = setInterval(async () => { | |
| await redis.publish(`channel:${channel}`, { | |
| type: "ping", | |
| timestamp: Date.now(), | |
| }); | |
| await redis.publish( | |
| streamKey, | |
| JSON.stringify({ type: "ping", timestamp: Date.now() }) | |
| ); | |
| }, 10_000); | |
| keepaliveInterval = setInterval(() => { | |
| safeEnqueue( | |
| json({ type: "ping", timestamp: Date.now() } as SystemEvent) | |
| ); | |
| }, 10_000); |
🤖 Prompt for AI Agents
In packages/realtime/src/server/handler.ts around lines 258-263, the keepalive
currently calls redis.publish to send a JSON ping to the shared channel which
causes O(n²) fan-out; change the interval callback to emit the ping directly to
the SSE response for this connection (e.g., res.write a ping/comment or an SSE
event with type "ping" and timestamp) instead of publishing to Redis, leaving
the existing subscriber-side handling intact for externally-generated pings;
ensure the interval still runs every 10_000ms and is cleared on connection
close.
| console.warn("Failed to parse stream entry", { error }); | ||
| return null; |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Don’t use console; route through logger or silence.
Replace console.warn with the internal logger, or remove to satisfy guidelines.
Apply:
- console.warn("Failed to parse stream entry", { error });
+ // Consider routing via the provided realtime logger if needed.
+ // logger.warn("Failed to parse stream entry", { error });Optionally add a logger param to parseStreamEntry and call with the scoped logger where used.
📝 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.
| console.warn("Failed to parse stream entry", { error }); | |
| return null; | |
| // Consider routing via the provided realtime logger if needed. | |
| // logger.warn("Failed to parse stream entry", { error }); | |
| return null; |
🤖 Prompt for AI Agents
In packages/realtime/src/server/handler.ts around lines 356-357, replace the
console.warn call with the internal logger (or remove it) so logging follows
project guidelines: update parseStreamEntry to accept a logger parameter (or
import/use the scoped logger available in this module), call logger.warn with
the same message and error object instead of console.warn, and update all call
sites to pass the scoped logger; ensure types/signatures are updated accordingly
and tests/linters pass.
| const streamPayload = JSON.stringify({ | ||
| data, | ||
| __event_path: [outerKey, innerKey], | ||
| }; | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add error handling for JSON.stringify.
JSON.stringify can throw on circular references, BigInt values, or other non-serializable data. Consider wrapping this in a try-catch to provide a clearer error message or fallback behavior.
Apply this diff to add error handling:
- const streamPayload = JSON.stringify({
- data,
- __event_path: [outerKey, innerKey],
- });
+ let streamPayload: string;
+ try {
+ streamPayload = JSON.stringify({
+ data,
+ __event_path: [outerKey, innerKey],
+ });
+ } catch (error) {
+ this._logger.error("Failed to serialize event payload:", error);
+ throw new Error(`Event serialization failed: ${error instanceof Error ? error.message : String(error)}`);
+ }📝 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.
| const streamPayload = JSON.stringify({ | |
| data, | |
| __event_path: [outerKey, innerKey], | |
| }; | |
| }); | |
| let streamPayload: string; | |
| try { | |
| streamPayload = JSON.stringify({ | |
| data, | |
| __event_path: [outerKey, innerKey], | |
| }); | |
| } catch (error) { | |
| this._logger.error("Failed to serialize event payload:", error); | |
| throw new Error( | |
| `Event serialization failed: ${ | |
| error instanceof Error ? error.message : String(error) | |
| }` | |
| ); | |
| } |
🤖 Prompt for AI Agents
In packages/realtime/src/server/realtime.ts around lines 69 to 72,
JSON.stringify can throw for circular refs or non-serializable values; wrap the
JSON.stringify call in a try-catch, and on error log/emit a clear diagnostic
including outerKey and innerKey and the error message, then use a safe fallback
payload (for example an object indicating serialization failed with
error.message and data type info, or a sanitized version converting BigInt to
string) so the process doesn’t crash; ensure the streamPayload variable is set
to either the successful JSON string or the fallback string and preserve enough
context to debug.
| await this._redis.publish( | ||
| `channel:${channel}`, | ||
| JSON.stringify({ | ||
| data, | ||
| __event_path: [outerKey, innerKey], | ||
| __stream_id: id, | ||
| }) | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Add error handling for publish JSON.stringify.
Similar to the stream payload, the publish call uses JSON.stringify without error handling.
Apply this diff to add error handling:
- await this._redis.publish(
- `channel:${channel}`,
- JSON.stringify({
- data,
- __event_path: [outerKey, innerKey],
- __stream_id: id,
- })
- );
+ let publishPayload: string;
+ try {
+ publishPayload = JSON.stringify({
+ data,
+ __event_path: [outerKey, innerKey],
+ __stream_id: id,
+ });
+ } catch (error) {
+ this._logger.error("Failed to serialize publish payload:", error);
+ throw new Error(`Publish serialization failed: ${error instanceof Error ? error.message : String(error)}`);
+ }
+
+ await this._redis.publish(
+ `channel:${channel}`,
+ publishPayload
+ );📝 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.
| await this._redis.publish( | |
| `channel:${channel}`, | |
| JSON.stringify({ | |
| data, | |
| __event_path: [outerKey, innerKey], | |
| __stream_id: id, | |
| }) | |
| ); | |
| let publishPayload: string; | |
| try { | |
| publishPayload = JSON.stringify({ | |
| data, | |
| __event_path: [outerKey, innerKey], | |
| __stream_id: id, | |
| }); | |
| } catch (error) { | |
| this._logger.error("Failed to serialize publish payload:", error); | |
| throw new Error(`Publish serialization failed: ${error instanceof Error ? error.message : String(error)}`); | |
| } | |
| await this._redis.publish( | |
| `channel:${channel}`, | |
| publishPayload | |
| ); |
🤖 Prompt for AI Agents
In packages/realtime/src/server/realtime.ts around lines 90 to 97, the
JSON.stringify call inside the publish lacks error handling which can throw and
crash the flow; wrap the JSON.stringify(...) in a try/catch, on error log the
serialization failure with contextual info (channel, outerKey, innerKey, id and
the data that failed) and do not call publish with an invalid payload (either
skip publishing or publish a safe error/placeholder payload), ensuring any
caught exception does not bubble out.
| bundle: false, | ||
| dts: true, | ||
| entry: ["src/**/*.{ts,tsx}"], | ||
| platform: "browser", |
There was a problem hiding this comment.
Platform "browser" may not be appropriate for server-side code.
The platform: "browser" setting is applied globally, but this package contains both client code (src/client/) and server code (src/server/). Server-side modules (e.g., realtime.ts using ioredis) require Node.js runtime features and should be built with platform: "node".
Consider one of the following solutions:
Solution 1 (recommended): Split build configuration
export default defineConfig({
- entry: ["src/**/*.{ts,tsx}"],
- platform: "browser",
+ entry: {
+ "client/index": "src/client/index.ts",
+ "server/index": "src/server/index.ts",
+ },
+ platform: {
+ "client/*": "browser",
+ "server/*": "node",
+ },Solution 2: Use conditional platform based on entry path
Check tsdown documentation to see if it supports per-entry platform overrides or use separate build configs for client and server.
📝 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.
| platform: "browser", | |
| export default defineConfig({ | |
| entry: { | |
| "client/index": "src/client/index.ts", | |
| "server/index": "src/server/index.ts", | |
| }, | |
| platform: { | |
| "client/*": "browser", | |
| "server/*": "node", | |
| }, | |
| // …other existing config options (e.g. commented externals, outdir, etc.)… | |
| }); |
🤖 Prompt for AI Agents
In packages/realtime/tsdown.config.ts around line 5, the config sets platform:
"browser" globally but this package contains server code (src/server/* and files
like realtime.ts using ioredis) that must be built for Node; change the config
to apply "browser" only to client entries and "node" to server entries by either
(recommended) splitting into two tsdown config files (e.g.,
tsdown.client.config.ts and tsdown.server.config.ts) or configuring
per-entry/platform overrides if tsdown supports it; update build scripts to
invoke the appropriate config for client and server builds and ensure server
entries are built with platform: "node" so Node runtime APIs are preserved.
Summary
Testing
https://chatgpt.com/codex/tasks/task_e_68e7952b9b28832b9d6f041c8db8c71a
Summary by CodeRabbit
New Features
Improvements
Chores
Style