Repository navigation
feat(api): adopt redis streams for realtime #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,10 +35,11 @@ export const env = { | |
| "UPSTASH_REDIS_REST_URL", | ||
| "http://localhost:8079" | ||
| ), | ||
| UPSTASH_REDIS_REST_TOKEN: getEnvVariable( | ||
| "UPSTASH_REDIS_REST_TOKEN", | ||
| "example_token" | ||
| ), | ||
| UPSTASH_REDIS_REST_TOKEN: getEnvVariable( | ||
| "UPSTASH_REDIS_REST_TOKEN", | ||
| "example_token" | ||
| ), | ||
| REDIS_URL: getEnvVariable("REDIS_URL", "redis://localhost:6379"), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make REDIS_URL required in production (no silent localhost). Avoid connecting to local Redis in prod when unset. - REDIS_URL: getEnvVariable("REDIS_URL", "redis://localhost:6379"),
+ REDIS_URL: getEnvVariable("REDIS_URL", process.env.NODE_ENV === "production" ? "" : "redis://localhost:6379"),And validate on bootstrap: import { env } from "@api/env";
if (env.NODE_ENV === "production" && !env.REDIS_URL) {
throw new Error("REDIS_URL is required in production");
}🤖 Prompt for AI Agents |
||
| BETTER_AUTH_URL: getEnvVariable("BETTER_AUTH_URL"), | ||
| BETTER_AUTH_SECRET: getEnvVariable("BETTER_AUTH_SECRET"), | ||
| API_KEY_SECRET: getEnvVariable("API_KEY_SECRET"), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| import type { RealtimeEvent } from "@cossistant/types/realtime-events"; | ||
| import type { ServerWebSocket } from "bun"; | ||
| import type { DispatchOptions } from "./router"; | ||
|
|
||
| export type RawSocket = ServerWebSocket & { connectionId?: string }; | ||
|
|
||
| export type LocalConnectionRecord = { | ||
| socket: RawSocket; | ||
| websiteId?: string; | ||
| organizationId?: string; | ||
| userId?: string; | ||
| visitorId?: string; | ||
| }; | ||
|
|
||
| export const localConnections = new Map<string, LocalConnectionRecord>(); | ||
|
|
||
| function createExcludePredicate( | ||
| options?: DispatchOptions | ||
| ): ((connectionId: string) => boolean) | undefined { | ||
| if (!options?.exclude) { | ||
| return; | ||
| } | ||
|
|
||
| const excludeIds = Array.isArray(options.exclude) | ||
| ? new Set(options.exclude) | ||
| : new Set<string>([options.exclude]); | ||
|
|
||
| return (connectionId: string) => excludeIds.has(connectionId); | ||
| } | ||
|
|
||
| function sendEventToSocket( | ||
| record: LocalConnectionRecord, | ||
| serializedEvent: string | ||
| ): void { | ||
| try { | ||
| record.socket.send(serializedEvent); | ||
| } catch (error) { | ||
| console.error("[WebSocket] Failed to send event:", error); | ||
| } | ||
| } | ||
|
|
||
| export function dispatchEventToLocalConnection( | ||
| connectionId: string, | ||
| event: RealtimeEvent | ||
| ): void { | ||
| const connection = localConnections.get(connectionId); | ||
| if (!connection) { | ||
| return; | ||
| } | ||
|
|
||
| const serializedEvent = JSON.stringify(event); | ||
| sendEventToSocket(connection, serializedEvent); | ||
| } | ||
|
|
||
| export function dispatchEventToLocalVisitor( | ||
| visitorId: string, | ||
| event: RealtimeEvent, | ||
| options?: DispatchOptions | ||
| ): void { | ||
| const shouldExclude = createExcludePredicate(options); | ||
| const serializedEvent = JSON.stringify(event); | ||
|
|
||
| for (const [connectionId, connection] of localConnections) { | ||
| if (connection.visitorId !== visitorId) { | ||
| continue; | ||
| } | ||
|
|
||
| if (shouldExclude?.(connectionId)) { | ||
| continue; | ||
| } | ||
|
|
||
| console.log("[WebSocket] Dispatching visitor event", { | ||
| visitorId, | ||
| connectionId, | ||
| eventType: event.type, | ||
| }); | ||
| sendEventToSocket(connection, serializedEvent); | ||
|
Comment on lines
+72
to
+77
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace console logging with centralized logger. Guidelines prohibit console*. Use a structured logger (e.g., pino) or existing project logger. - console.log("[WebSocket] Dispatching visitor event", {
+ // logger.info({ visitorId, connectionId, eventType: event.type }, "Dispatching visitor event");
visitorId,
connectionId,
eventType: event.type,
});- console.log("[WebSocket] Dispatching website event", {
+ // logger.info({ websiteId, connectionId, eventType: event.type }, "Dispatching website event");
websiteId,
connectionId,
eventType: event.type,
});Also applies to: 103-107 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| export function dispatchEventToLocalWebsite( | ||
| websiteId: string, | ||
| event: RealtimeEvent, | ||
| options?: DispatchOptions | ||
| ): void { | ||
| const shouldExclude = createExcludePredicate(options); | ||
| const serializedEvent = JSON.stringify(event); | ||
|
|
||
| for (const [connectionId, connection] of localConnections) { | ||
| if (connection.websiteId !== websiteId) { | ||
| continue; | ||
| } | ||
|
|
||
| // Only dashboard/user connections should receive website events | ||
| if (!connection.userId) { | ||
| continue; | ||
| } | ||
|
|
||
| if (shouldExclude?.(connectionId)) { | ||
| continue; | ||
| } | ||
|
|
||
| console.log("[WebSocket] Dispatching website event", { | ||
| websiteId, | ||
| connectionId, | ||
| eventType: event.type, | ||
| }); | ||
| sendEventToSocket(connection, serializedEvent); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t default sensitive tokens; fail fast instead.
Defaulting UPSTASH token to "example_token" can mask prod misconfig. Return empty and validate at startup.
Add a one-time validation (e.g., in server bootstrap):
🤖 Prompt for AI Agents