@parsrun/realtime 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +176 -0
- package/dist/adapters/durable-objects.d.ts +89 -0
- package/dist/adapters/durable-objects.js +414 -0
- package/dist/adapters/durable-objects.js.map +1 -0
- package/dist/adapters/index.d.ts +3 -0
- package/dist/adapters/index.js +740 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/sse.d.ts +69 -0
- package/dist/adapters/sse.js +330 -0
- package/dist/adapters/sse.js.map +1 -0
- package/dist/hono.d.ts +76 -0
- package/dist/hono.js +551 -0
- package/dist/hono.js.map +1 -0
- package/dist/index.d.ts +120 -0
- package/dist/index.js +1084 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +317 -0
- package/dist/types.js +75 -0
- package/dist/types.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types.ts","../../src/adapters/sse.ts","../../src/adapters/durable-objects.ts"],"sourcesContent":["/**\n * @parsrun/realtime - Type Definitions\n * Realtime communication types and interfaces\n */\n\n// ============================================================================\n// Core Types\n// ============================================================================\n\n/**\n * Realtime adapter type\n */\nexport type RealtimeAdapterType = \"sse\" | \"durable-objects\" | \"memory\";\n\n/**\n * Realtime message\n */\nexport interface RealtimeMessage<T = unknown> {\n /** Unique message ID */\n id: string;\n /** Event type/name */\n event: string;\n /** Channel name */\n channel: string;\n /** Message payload */\n data: T;\n /** Sender ID (optional) */\n senderId?: string | undefined;\n /** Timestamp */\n timestamp: number;\n /** Metadata */\n metadata?: Record<string, unknown> | undefined;\n}\n\n/**\n * Presence user data\n */\nexport interface PresenceUser<T = unknown> {\n /** User ID */\n userId: string;\n /** Session ID */\n sessionId: string;\n /** Custom presence data */\n data: T;\n /** Join timestamp */\n joinedAt: number;\n /** Last activity timestamp */\n lastSeenAt: number;\n}\n\n/**\n * Channel info\n */\nexport interface ChannelInfo {\n /** Channel name */\n name: string;\n /** Number of subscribers */\n subscriberCount: number;\n /** Presence users */\n presence: PresenceUser[];\n /** Channel metadata */\n metadata?: Record<string, unknown> | undefined;\n}\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * System event types\n */\nexport type SystemEventType =\n | \"connection:open\"\n | \"connection:close\"\n | \"connection:error\"\n | \"presence:join\"\n | \"presence:leave\"\n | \"presence:update\"\n | \"channel:subscribe\"\n | \"channel:unsubscribe\"\n | \"ping\"\n | \"pong\";\n\n/**\n * Message handler function\n */\nexport type MessageHandler<T = unknown> = (\n message: RealtimeMessage<T>\n) => void | Promise<void>;\n\n/**\n * Connection event handler\n */\nexport type ConnectionHandler = (\n event: ConnectionEvent\n) => void | Promise<void>;\n\n/**\n * Connection event\n */\nexport interface ConnectionEvent {\n type: \"open\" | \"close\" | \"error\";\n sessionId: string;\n userId?: string | undefined;\n error?: Error | undefined;\n timestamp: number;\n}\n\n/**\n * Presence event\n */\nexport interface PresenceEvent<T = unknown> {\n type: \"join\" | \"leave\" | \"update\";\n channel: string;\n user: PresenceUser<T>;\n timestamp: number;\n}\n\n// ============================================================================\n// Channel Interface\n// ============================================================================\n\n/**\n * Channel interface for realtime communication\n */\nexport interface Channel {\n /** Channel name */\n readonly name: string;\n\n /**\n * Broadcast message to all subscribers\n */\n broadcast<T = unknown>(event: string, data: T): Promise<void>;\n\n /**\n * Send message to specific user\n */\n send<T = unknown>(userId: string, event: string, data: T): Promise<void>;\n\n /**\n * Get current presence users\n */\n getPresence<T = unknown>(): Promise<PresenceUser<T>[]>;\n\n /**\n * Subscribe to channel messages\n */\n subscribe<T = unknown>(handler: MessageHandler<T>): () => void;\n\n /**\n * Subscribe to presence events\n */\n onPresence<T = unknown>(\n handler: (event: PresenceEvent<T>) => void\n ): () => void;\n\n /**\n * Get channel info\n */\n getInfo(): Promise<ChannelInfo>;\n}\n\n// ============================================================================\n// Realtime Service Interface\n// ============================================================================\n\n/**\n * Realtime service interface\n */\nexport interface RealtimeService {\n /** Adapter type */\n readonly adapterType: RealtimeAdapterType;\n\n /**\n * Get or create a channel\n */\n channel(name: string): Channel;\n\n /**\n * Broadcast to a channel\n */\n broadcast<T = unknown>(channel: string, event: string, data: T): Promise<void>;\n\n /**\n * Get channel info\n */\n getChannelInfo(channel: string): Promise<ChannelInfo | null>;\n\n /**\n * List active channels\n */\n listChannels(): Promise<string[]>;\n\n /**\n * Subscribe to connection events\n */\n onConnection(handler: ConnectionHandler): () => void;\n\n /**\n * Close all connections and cleanup\n */\n close(): Promise<void>;\n}\n\n// ============================================================================\n// Adapter Interface\n// ============================================================================\n\n/**\n * Realtime adapter interface (low-level)\n */\nexport interface RealtimeAdapter {\n /** Adapter type */\n readonly type: RealtimeAdapterType;\n\n /**\n * Add a subscriber to channel\n */\n subscribe(\n channel: string,\n sessionId: string,\n handler: MessageHandler\n ): Promise<void>;\n\n /**\n * Remove a subscriber from channel\n */\n unsubscribe(channel: string, sessionId: string): Promise<void>;\n\n /**\n * Publish message to channel\n */\n publish<T = unknown>(channel: string, message: RealtimeMessage<T>): Promise<void>;\n\n /**\n * Send to specific session\n */\n sendToSession<T = unknown>(\n sessionId: string,\n message: RealtimeMessage<T>\n ): Promise<boolean>;\n\n /**\n * Get subscribers for channel\n */\n getSubscribers(channel: string): Promise<string[]>;\n\n /**\n * Set presence for user in channel\n */\n setPresence<T = unknown>(\n channel: string,\n sessionId: string,\n userId: string,\n data: T\n ): Promise<void>;\n\n /**\n * Remove presence\n */\n removePresence(channel: string, sessionId: string): Promise<void>;\n\n /**\n * Get presence for channel\n */\n getPresence<T = unknown>(channel: string): Promise<PresenceUser<T>[]>;\n\n /**\n * Cleanup and close adapter\n */\n close(): Promise<void>;\n}\n\n// ============================================================================\n// SSE Types\n// ============================================================================\n\n/**\n * SSE connection\n */\nexport interface SSEConnection {\n /** Session ID */\n sessionId: string;\n /** User ID */\n userId?: string | undefined;\n /** Subscribed channels */\n channels: Set<string>;\n /** Response writer */\n writer: WritableStreamDefaultWriter<Uint8Array>;\n /** Connection state */\n state: \"open\" | \"closed\";\n /** Created timestamp */\n createdAt: number;\n /** Last ping timestamp */\n lastPingAt: number;\n}\n\n/**\n * SSE adapter options\n */\nexport interface SSEAdapterOptions {\n /** Ping interval in milliseconds (default: 30000) */\n pingInterval?: number | undefined;\n /** Connection timeout in milliseconds (default: 0 = no timeout) */\n connectionTimeout?: number | undefined;\n /** Max connections per channel (default: 1000) */\n maxConnectionsPerChannel?: number | undefined;\n /** Retry delay for client reconnection (default: 3000) */\n retryDelay?: number | undefined;\n}\n\n// ============================================================================\n// Durable Objects Types\n// ============================================================================\n\n/**\n * Durable Objects adapter options\n */\nexport interface DurableObjectsAdapterOptions {\n /** Durable Object namespace binding */\n namespace: DurableObjectNamespace;\n /** Channel prefix (default: \"channel:\") */\n channelPrefix?: string | undefined;\n}\n\n/**\n * Durable Object WebSocket session\n */\nexport interface DOWebSocketSession {\n /** Session ID */\n sessionId: string;\n /** User ID */\n userId?: string | undefined;\n /** WebSocket connection */\n webSocket: WebSocket;\n /** Presence data */\n presence?: unknown;\n /** Connection state */\n state: \"connecting\" | \"open\" | \"closing\" | \"closed\";\n /** Created timestamp */\n createdAt: number;\n}\n\n/**\n * Durable Object state stored in storage\n */\nexport interface DOChannelState {\n /** Channel name */\n name: string;\n /** Channel metadata */\n metadata?: Record<string, unknown> | undefined;\n /** Created timestamp */\n createdAt: number;\n}\n\n// ============================================================================\n// Config Types\n// ============================================================================\n\n/**\n * Realtime config\n */\nexport interface RealtimeConfig {\n /** Adapter type */\n adapter: RealtimeAdapterType;\n\n /** SSE adapter options */\n sse?: SSEAdapterOptions | undefined;\n\n /** Durable Objects adapter options */\n durableObjects?: DurableObjectsAdapterOptions | undefined;\n\n /** Enable debug logging */\n debug?: boolean | undefined;\n}\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Realtime error\n */\nexport class RealtimeError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly cause?: unknown\n ) {\n super(message);\n this.name = \"RealtimeError\";\n }\n}\n\n/**\n * Common error codes\n */\nexport const RealtimeErrorCodes = {\n CONNECTION_FAILED: \"CONNECTION_FAILED\",\n CHANNEL_NOT_FOUND: \"CHANNEL_NOT_FOUND\",\n UNAUTHORIZED: \"UNAUTHORIZED\",\n MESSAGE_TOO_LARGE: \"MESSAGE_TOO_LARGE\",\n RATE_LIMITED: \"RATE_LIMITED\",\n ADAPTER_ERROR: \"ADAPTER_ERROR\",\n INVALID_MESSAGE: \"INVALID_MESSAGE\",\n} as const;\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Create message options\n */\nexport interface CreateMessageOptions<T = unknown> {\n event: string;\n channel: string;\n data: T;\n senderId?: string | undefined;\n metadata?: Record<string, unknown> | undefined;\n}\n\n/**\n * Create a realtime message\n */\nexport function createMessage<T = unknown>(\n options: CreateMessageOptions<T>\n): RealtimeMessage<T> {\n return {\n id: crypto.randomUUID(),\n event: options.event,\n channel: options.channel,\n data: options.data,\n senderId: options.senderId,\n timestamp: Date.now(),\n metadata: options.metadata,\n };\n}\n\n/**\n * Parse SSE event string\n */\nexport function parseSSEEvent(eventString: string): RealtimeMessage | null {\n try {\n const lines = eventString.split(\"\\n\");\n let event = \"message\";\n let data = \"\";\n let id = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"event:\")) {\n event = line.slice(6).trim();\n } else if (line.startsWith(\"data:\")) {\n data = line.slice(5).trim();\n } else if (line.startsWith(\"id:\")) {\n id = line.slice(3).trim();\n }\n }\n\n if (!data) return null;\n\n const parsed = JSON.parse(data);\n return {\n id: id || parsed.id || crypto.randomUUID(),\n event,\n channel: parsed.channel || \"\",\n data: parsed.data ?? parsed,\n senderId: parsed.senderId,\n timestamp: parsed.timestamp || Date.now(),\n metadata: parsed.metadata,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Format message as SSE event string\n */\nexport function formatSSEEvent(message: RealtimeMessage): string {\n const lines: string[] = [];\n\n lines.push(`id:${message.id}`);\n lines.push(`event:${message.event}`);\n lines.push(`data:${JSON.stringify(message)}`);\n lines.push(\"\"); // Empty line to end event\n\n return lines.join(\"\\n\") + \"\\n\";\n}\n","/**\n * @parsrun/realtime - SSE Adapter\n * Server-Sent Events adapter for all runtimes\n */\n\nimport type {\n MessageHandler,\n PresenceUser,\n RealtimeAdapter,\n RealtimeMessage,\n SSEAdapterOptions,\n SSEConnection,\n} from \"../types.js\";\nimport { formatSSEEvent, RealtimeError, RealtimeErrorCodes } from \"../types.js\";\n\n/**\n * Resolved SSE adapter options (all required)\n */\ninterface ResolvedSSEOptions {\n pingInterval: number;\n connectionTimeout: number;\n maxConnectionsPerChannel: number;\n retryDelay: number;\n}\n\n/**\n * Default SSE adapter options\n */\nconst DEFAULT_OPTIONS: ResolvedSSEOptions = {\n pingInterval: 30000,\n connectionTimeout: 0,\n maxConnectionsPerChannel: 1000,\n retryDelay: 3000,\n};\n\n/**\n * SSE Adapter\n * Works on all runtimes (Node, Deno, Bun, Cloudflare Workers)\n */\nexport class SSEAdapter implements RealtimeAdapter {\n readonly type = \"sse\" as const;\n\n private options: ResolvedSSEOptions;\n private connections: Map<string, SSEConnection> = new Map();\n private channelSubscribers: Map<string, Map<string, MessageHandler>> = new Map();\n private presence: Map<string, Map<string, PresenceUser>> = new Map();\n private pingIntervalId: ReturnType<typeof setInterval> | null = null;\n\n constructor(options: SSEAdapterOptions = {}) {\n this.options = {\n pingInterval: options.pingInterval ?? DEFAULT_OPTIONS.pingInterval,\n connectionTimeout: options.connectionTimeout ?? DEFAULT_OPTIONS.connectionTimeout,\n maxConnectionsPerChannel: options.maxConnectionsPerChannel ?? DEFAULT_OPTIONS.maxConnectionsPerChannel,\n retryDelay: options.retryDelay ?? DEFAULT_OPTIONS.retryDelay,\n };\n\n // Start ping interval\n if (this.options.pingInterval > 0) {\n this.startPingInterval();\n }\n }\n\n /**\n * Create SSE response for a new connection\n */\n createConnection(\n sessionId: string,\n userId?: string\n ): { response: Response; connection: SSEConnection } {\n const { readable, writable } = new TransformStream<Uint8Array>();\n const writer = writable.getWriter();\n\n const connection: SSEConnection = {\n sessionId,\n userId,\n channels: new Set(),\n writer,\n state: \"open\",\n createdAt: Date.now(),\n lastPingAt: Date.now(),\n };\n\n this.connections.set(sessionId, connection);\n\n // Send initial retry directive\n const encoder = new TextEncoder();\n writer.write(encoder.encode(`retry:${this.options.retryDelay}\\n\\n`));\n\n const response = new Response(readable, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n \"Connection\": \"keep-alive\",\n \"X-Accel-Buffering\": \"no\", // Disable nginx buffering\n },\n });\n\n return { response, connection };\n }\n\n /**\n * Close a connection\n */\n async closeConnection(sessionId: string): Promise<void> {\n const connection = this.connections.get(sessionId);\n if (!connection) return;\n\n connection.state = \"closed\";\n\n // Remove from all channels\n for (const channel of connection.channels) {\n await this.unsubscribe(channel, sessionId);\n await this.removePresence(channel, sessionId);\n }\n\n // Close writer\n try {\n await connection.writer.close();\n } catch {\n // Ignore close errors\n }\n\n this.connections.delete(sessionId);\n }\n\n /**\n * Get a connection by session ID\n */\n getConnection(sessionId: string): SSEConnection | undefined {\n return this.connections.get(sessionId);\n }\n\n /**\n * Get all active connections\n */\n getConnections(): Map<string, SSEConnection> {\n return this.connections;\n }\n\n // ============================================================================\n // RealtimeAdapter Implementation\n // ============================================================================\n\n async subscribe(\n channel: string,\n sessionId: string,\n handler: MessageHandler\n ): Promise<void> {\n const connection = this.connections.get(sessionId);\n if (!connection) {\n throw new RealtimeError(\n \"Connection not found\",\n RealtimeErrorCodes.CONNECTION_FAILED\n );\n }\n\n // Check max connections per channel\n const subscribers = this.channelSubscribers.get(channel);\n if (\n subscribers &&\n subscribers.size >= this.options.maxConnectionsPerChannel\n ) {\n throw new RealtimeError(\n \"Channel subscriber limit reached\",\n RealtimeErrorCodes.RATE_LIMITED\n );\n }\n\n // Add to channel subscribers\n if (!this.channelSubscribers.has(channel)) {\n this.channelSubscribers.set(channel, new Map());\n }\n this.channelSubscribers.get(channel)!.set(sessionId, handler);\n\n // Track channel on connection\n connection.channels.add(channel);\n\n // Send subscription confirmation\n await this.sendToConnection(connection, {\n id: crypto.randomUUID(),\n event: \"channel:subscribe\",\n channel,\n data: { channel },\n timestamp: Date.now(),\n });\n }\n\n async unsubscribe(channel: string, sessionId: string): Promise<void> {\n const subscribers = this.channelSubscribers.get(channel);\n if (subscribers) {\n subscribers.delete(sessionId);\n if (subscribers.size === 0) {\n this.channelSubscribers.delete(channel);\n }\n }\n\n const connection = this.connections.get(sessionId);\n if (connection) {\n connection.channels.delete(channel);\n }\n }\n\n async publish<T = unknown>(\n channel: string,\n message: RealtimeMessage<T>\n ): Promise<void> {\n const subscribers = this.channelSubscribers.get(channel);\n if (!subscribers) return;\n\n const promises: Promise<void>[] = [];\n\n for (const [sessionId, handler] of subscribers) {\n const connection = this.connections.get(sessionId);\n if (connection && connection.state === \"open\") {\n // Send via SSE\n promises.push(this.sendToConnection(connection, message));\n\n // Call handler\n try {\n const result = handler(message);\n if (result instanceof Promise) {\n promises.push(result.then(() => {}));\n }\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n await Promise.allSettled(promises);\n }\n\n async sendToSession<T = unknown>(\n sessionId: string,\n message: RealtimeMessage<T>\n ): Promise<boolean> {\n const connection = this.connections.get(sessionId);\n if (!connection || connection.state !== \"open\") {\n return false;\n }\n\n try {\n await this.sendToConnection(connection, message);\n return true;\n } catch {\n return false;\n }\n }\n\n async getSubscribers(channel: string): Promise<string[]> {\n const subscribers = this.channelSubscribers.get(channel);\n return subscribers ? Array.from(subscribers.keys()) : [];\n }\n\n async setPresence<T = unknown>(\n channel: string,\n sessionId: string,\n userId: string,\n data: T\n ): Promise<void> {\n if (!this.presence.has(channel)) {\n this.presence.set(channel, new Map());\n }\n\n const now = Date.now();\n const existing = this.presence.get(channel)!.get(sessionId);\n\n const user: PresenceUser<T> = {\n userId,\n sessionId,\n data,\n joinedAt: existing?.joinedAt ?? now,\n lastSeenAt: now,\n };\n\n this.presence.get(channel)!.set(sessionId, user as PresenceUser);\n\n // Broadcast presence update\n const eventType = existing ? \"presence:update\" : \"presence:join\";\n await this.publish(channel, {\n id: crypto.randomUUID(),\n event: eventType,\n channel,\n data: {\n type: existing ? \"update\" : \"join\",\n user,\n presence: await this.getPresence(channel),\n },\n timestamp: now,\n });\n }\n\n async removePresence(channel: string, sessionId: string): Promise<void> {\n const channelPresence = this.presence.get(channel);\n if (!channelPresence) return;\n\n const user = channelPresence.get(sessionId);\n if (!user) return;\n\n channelPresence.delete(sessionId);\n if (channelPresence.size === 0) {\n this.presence.delete(channel);\n }\n\n // Broadcast presence leave\n await this.publish(channel, {\n id: crypto.randomUUID(),\n event: \"presence:leave\",\n channel,\n data: {\n type: \"leave\",\n user,\n presence: await this.getPresence(channel),\n },\n timestamp: Date.now(),\n });\n }\n\n async getPresence<T = unknown>(channel: string): Promise<PresenceUser<T>[]> {\n const channelPresence = this.presence.get(channel);\n if (!channelPresence) return [];\n return Array.from(channelPresence.values()) as PresenceUser<T>[];\n }\n\n async close(): Promise<void> {\n // Stop ping interval\n if (this.pingIntervalId) {\n clearInterval(this.pingIntervalId);\n this.pingIntervalId = null;\n }\n\n // Close all connections\n const closePromises = Array.from(this.connections.keys()).map((sessionId) =>\n this.closeConnection(sessionId)\n );\n await Promise.allSettled(closePromises);\n\n // Clear all data\n this.connections.clear();\n this.channelSubscribers.clear();\n this.presence.clear();\n }\n\n // ============================================================================\n // Private Methods\n // ============================================================================\n\n private async sendToConnection<T = unknown>(\n connection: SSEConnection,\n message: RealtimeMessage<T>\n ): Promise<void> {\n if (connection.state !== \"open\") return;\n\n const encoder = new TextEncoder();\n const sseEvent = formatSSEEvent(message as RealtimeMessage);\n\n try {\n await connection.writer.write(encoder.encode(sseEvent));\n } catch (err) {\n // Connection likely closed\n connection.state = \"closed\";\n await this.closeConnection(connection.sessionId);\n }\n }\n\n private startPingInterval(): void {\n this.pingIntervalId = setInterval(() => {\n this.sendPingToAll();\n }, this.options.pingInterval);\n }\n\n private async sendPingToAll(): Promise<void> {\n const now = Date.now();\n const encoder = new TextEncoder();\n const pingEvent = `event:ping\\ndata:${now}\\n\\n`;\n\n for (const [sessionId, connection] of this.connections) {\n if (connection.state !== \"open\") continue;\n\n // Check connection timeout\n if (\n this.options.connectionTimeout > 0 &&\n now - connection.lastPingAt > this.options.connectionTimeout\n ) {\n await this.closeConnection(sessionId);\n continue;\n }\n\n try {\n await connection.writer.write(encoder.encode(pingEvent));\n connection.lastPingAt = now;\n } catch {\n // Connection likely closed\n await this.closeConnection(sessionId);\n }\n }\n }\n\n /**\n * Update last ping time (call when receiving client ping)\n */\n updateLastPing(sessionId: string): void {\n const connection = this.connections.get(sessionId);\n if (connection) {\n connection.lastPingAt = Date.now();\n }\n }\n\n /**\n * Get statistics\n */\n getStats(): {\n totalConnections: number;\n totalChannels: number;\n connectionsByChannel: Record<string, number>;\n } {\n const connectionsByChannel: Record<string, number> = {};\n\n for (const [channel, subscribers] of this.channelSubscribers) {\n connectionsByChannel[channel] = subscribers.size;\n }\n\n return {\n totalConnections: this.connections.size,\n totalChannels: this.channelSubscribers.size,\n connectionsByChannel,\n };\n }\n}\n\n/**\n * Create SSE adapter\n */\nexport function createSSEAdapter(options?: SSEAdapterOptions): SSEAdapter {\n return new SSEAdapter(options);\n}\n","/**\n * @parsrun/realtime - Durable Objects Adapter\n * Cloudflare Durable Objects adapter for WebSocket-based realtime\n */\n\nimport type {\n DOWebSocketSession,\n DurableObjectsAdapterOptions,\n MessageHandler,\n PresenceUser,\n RealtimeAdapter,\n RealtimeMessage,\n} from \"../types.js\";\nimport { createMessage } from \"../types.js\";\n\n// ============================================================================\n// Durable Object Class (Export for wrangler.toml binding)\n// ============================================================================\n\n/**\n * RealtimeChannel Durable Object\n * Manages a single realtime channel with WebSocket connections\n *\n * Usage in wrangler.toml:\n * ```toml\n * [durable_objects]\n * bindings = [{ name = \"REALTIME_CHANNELS\", class_name = \"RealtimeChannelDO\" }]\n *\n * [[migrations]]\n * tag = \"v1\"\n * new_classes = [\"RealtimeChannelDO\"]\n * ```\n */\nexport class RealtimeChannelDO implements DurableObject {\n private sessions: Map<string, DOWebSocketSession> = new Map();\n private presence: Map<string, PresenceUser> = new Map();\n private channelName: string = \"\";\n private state: DurableObjectState;\n\n constructor(\n state: DurableObjectState,\n _env: unknown\n ) {\n this.state = state;\n // Restore hibernated WebSocket sessions\n this.state.getWebSockets().forEach((ws) => {\n const meta = ws.deserializeAttachment() as {\n sessionId: string;\n userId?: string;\n } | null;\n if (meta) {\n this.sessions.set(meta.sessionId, {\n sessionId: meta.sessionId,\n userId: meta.userId,\n webSocket: ws,\n state: \"open\",\n createdAt: Date.now(),\n });\n }\n });\n }\n\n async fetch(request: Request): Promise<Response> {\n const url = new URL(request.url);\n\n // Extract channel name from path\n const pathParts = url.pathname.split(\"/\").filter(Boolean);\n const lastPart = pathParts[pathParts.length - 1];\n if (lastPart) {\n this.channelName = lastPart;\n }\n\n // WebSocket upgrade\n if (request.headers.get(\"Upgrade\") === \"websocket\") {\n return this.handleWebSocketUpgrade(request);\n }\n\n // REST API endpoints\n switch (url.pathname.split(\"/\").pop()) {\n case \"broadcast\":\n return this.handleBroadcast(request);\n case \"presence\":\n return this.handleGetPresence();\n case \"info\":\n return this.handleGetInfo();\n case \"send\":\n return this.handleSendToUser(request);\n default:\n return new Response(\"Not found\", { status: 404 });\n }\n }\n\n // ============================================================================\n // WebSocket Handling\n // ============================================================================\n\n private handleWebSocketUpgrade(request: Request): Response {\n const url = new URL(request.url);\n const sessionId = url.searchParams.get(\"sessionId\") || crypto.randomUUID();\n const userId = url.searchParams.get(\"userId\") || undefined;\n\n const pair = new WebSocketPair();\n const client = pair[0];\n const server = pair[1];\n\n // Store session metadata for hibernation\n server.serializeAttachment({ sessionId, userId });\n\n this.state.acceptWebSocket(server);\n\n const session: DOWebSocketSession = {\n sessionId,\n userId,\n webSocket: server,\n state: \"open\",\n createdAt: Date.now(),\n };\n\n this.sessions.set(sessionId, session);\n\n // Send connection confirmation\n server.send(\n JSON.stringify(\n createMessage({\n event: \"connection:open\",\n channel: this.channelName,\n data: { sessionId, userId },\n })\n )\n );\n\n return new Response(null, { status: 101, webSocket: client });\n }\n\n async webSocketMessage(\n ws: WebSocket,\n message: string | ArrayBuffer\n ): Promise<void> {\n const session = this.getSessionByWebSocket(ws);\n if (!session) return;\n\n try {\n const data =\n typeof message === \"string\"\n ? message\n : new TextDecoder().decode(message);\n\n const parsed = JSON.parse(data) as RealtimeMessage;\n await this.handleMessage(session, parsed);\n } catch {\n // Invalid message format, ignore\n }\n }\n\n async webSocketClose(\n ws: WebSocket,\n code: number,\n reason: string,\n _wasClean: boolean\n ): Promise<void> {\n const session = this.getSessionByWebSocket(ws);\n if (!session) return;\n\n // Remove presence\n this.presence.delete(session.sessionId);\n await this.broadcastPresenceUpdate();\n\n // Remove session\n this.sessions.delete(session.sessionId);\n\n // Broadcast leave event\n await this.broadcastToAll({\n event: \"connection:close\",\n channel: this.channelName,\n data: {\n sessionId: session.sessionId,\n userId: session.userId,\n code,\n reason,\n },\n });\n }\n\n async webSocketError(ws: WebSocket, _error: unknown): Promise<void> {\n const session = this.getSessionByWebSocket(ws);\n if (session) {\n session.state = \"closed\";\n this.sessions.delete(session.sessionId);\n this.presence.delete(session.sessionId);\n }\n }\n\n // ============================================================================\n // Message Handling\n // ============================================================================\n\n private async handleMessage(\n session: DOWebSocketSession,\n message: RealtimeMessage\n ): Promise<void> {\n switch (message.event) {\n case \"ping\":\n session.webSocket.send(\n JSON.stringify(\n createMessage({\n event: \"pong\",\n channel: this.channelName,\n data: { timestamp: Date.now() },\n })\n )\n );\n break;\n\n case \"presence:join\":\n await this.handlePresenceJoin(session, message.data);\n break;\n\n case \"presence:update\":\n await this.handlePresenceUpdate(session, message.data);\n break;\n\n case \"presence:leave\":\n await this.handlePresenceLeave(session);\n break;\n\n case \"broadcast\":\n // Broadcast to all except sender\n await this.broadcastToAll(\n {\n event: message.event,\n channel: this.channelName,\n data: message.data,\n senderId: session.sessionId,\n },\n session.sessionId\n );\n break;\n\n default:\n // Forward custom events\n await this.broadcastToAll(\n {\n event: message.event,\n channel: this.channelName,\n data: message.data,\n senderId: session.sessionId,\n },\n session.sessionId\n );\n }\n }\n\n // ============================================================================\n // Presence Handling\n // ============================================================================\n\n private async handlePresenceJoin(\n session: DOWebSocketSession,\n data: unknown\n ): Promise<void> {\n const now = Date.now();\n const user: PresenceUser = {\n userId: session.userId || session.sessionId,\n sessionId: session.sessionId,\n data,\n joinedAt: now,\n lastSeenAt: now,\n };\n\n this.presence.set(session.sessionId, user);\n session.presence = data;\n\n await this.broadcastPresenceUpdate();\n }\n\n private async handlePresenceUpdate(\n session: DOWebSocketSession,\n data: unknown\n ): Promise<void> {\n const existing = this.presence.get(session.sessionId);\n if (existing) {\n existing.data = data;\n existing.lastSeenAt = Date.now();\n session.presence = data;\n await this.broadcastPresenceUpdate();\n }\n }\n\n private async handlePresenceLeave(session: DOWebSocketSession): Promise<void> {\n this.presence.delete(session.sessionId);\n session.presence = undefined;\n await this.broadcastPresenceUpdate();\n }\n\n private async broadcastPresenceUpdate(): Promise<void> {\n const presenceList = Array.from(this.presence.values());\n\n await this.broadcastToAll({\n event: \"presence:sync\",\n channel: this.channelName,\n data: presenceList,\n });\n }\n\n // ============================================================================\n // REST API Handlers\n // ============================================================================\n\n private async handleBroadcast(request: Request): Promise<Response> {\n try {\n const body = (await request.json()) as { event: string; data: unknown };\n\n await this.broadcastToAll({\n event: body.event,\n channel: this.channelName,\n data: body.data,\n });\n\n return new Response(JSON.stringify({ success: true }), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (err) {\n return new Response(\n JSON.stringify({ success: false, error: String(err) }),\n { status: 400, headers: { \"Content-Type\": \"application/json\" } }\n );\n }\n }\n\n private handleGetPresence(): Response {\n const presenceList = Array.from(this.presence.values());\n return new Response(JSON.stringify(presenceList), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n private handleGetInfo(): Response {\n return new Response(\n JSON.stringify({\n channel: this.channelName,\n connections: this.sessions.size,\n presence: this.presence.size,\n }),\n { headers: { \"Content-Type\": \"application/json\" } }\n );\n }\n\n private async handleSendToUser(request: Request): Promise<Response> {\n try {\n const body = (await request.json()) as {\n userId: string;\n event: string;\n data: unknown;\n };\n\n let sent = false;\n for (const session of this.sessions.values()) {\n if (session.userId === body.userId && session.state === \"open\") {\n session.webSocket.send(\n JSON.stringify(\n createMessage({\n event: body.event,\n channel: this.channelName,\n data: body.data,\n })\n )\n );\n sent = true;\n }\n }\n\n return new Response(JSON.stringify({ success: true, sent }), {\n headers: { \"Content-Type\": \"application/json\" },\n });\n } catch (err) {\n return new Response(\n JSON.stringify({ success: false, error: String(err) }),\n { status: 400, headers: { \"Content-Type\": \"application/json\" } }\n );\n }\n }\n\n // ============================================================================\n // Helpers\n // ============================================================================\n\n private getSessionByWebSocket(ws: WebSocket): DOWebSocketSession | undefined {\n for (const session of this.sessions.values()) {\n if (session.webSocket === ws) {\n return session;\n }\n }\n return undefined;\n }\n\n private async broadcastToAll(\n messageData: {\n event: string;\n channel: string;\n data: unknown;\n senderId?: string;\n },\n excludeSessionId?: string\n ): Promise<void> {\n const message = createMessage(messageData);\n const payload = JSON.stringify(message);\n\n for (const session of this.sessions.values()) {\n if (session.sessionId === excludeSessionId) continue;\n if (session.state !== \"open\") continue;\n\n try {\n session.webSocket.send(payload);\n } catch {\n // Connection might be closed\n session.state = \"closed\";\n }\n }\n }\n}\n\n// ============================================================================\n// Durable Objects Adapter (Client-side)\n// ============================================================================\n\n/**\n * Durable Objects Adapter\n * Uses RealtimeChannelDO for channel management\n */\nexport class DurableObjectsAdapter implements RealtimeAdapter {\n readonly type = \"durable-objects\" as const;\n\n private namespace: DurableObjectNamespace;\n private channelPrefix: string;\n private localHandlers: Map<string, Map<string, MessageHandler>> = new Map();\n\n constructor(options: DurableObjectsAdapterOptions) {\n this.namespace = options.namespace;\n this.channelPrefix = options.channelPrefix ?? \"channel:\";\n }\n\n /**\n * Get Durable Object stub for a channel\n */\n getChannelStub(channel: string): DurableObjectStub {\n const id = this.namespace.idFromName(`${this.channelPrefix}${channel}`);\n return this.namespace.get(id);\n }\n\n /**\n * Get WebSocket URL for a channel\n */\n getWebSocketUrl(\n channel: string,\n sessionId: string,\n userId?: string,\n baseUrl?: string\n ): string {\n const base = baseUrl || \"wss://your-worker.your-subdomain.workers.dev\";\n const params = new URLSearchParams({ sessionId });\n if (userId) params.set(\"userId\", userId);\n return `${base}/realtime/${channel}?${params}`;\n }\n\n // ============================================================================\n // RealtimeAdapter Implementation\n // ============================================================================\n\n async subscribe(\n channel: string,\n sessionId: string,\n handler: MessageHandler\n ): Promise<void> {\n // Store handler locally for callback\n if (!this.localHandlers.has(channel)) {\n this.localHandlers.set(channel, new Map());\n }\n this.localHandlers.get(channel)!.set(sessionId, handler);\n\n // Note: Actual WebSocket subscription happens via WebSocket connection\n // This is used for server-side handler registration\n }\n\n async unsubscribe(channel: string, sessionId: string): Promise<void> {\n const handlers = this.localHandlers.get(channel);\n if (handlers) {\n handlers.delete(sessionId);\n if (handlers.size === 0) {\n this.localHandlers.delete(channel);\n }\n }\n }\n\n async publish<T = unknown>(\n channel: string,\n message: RealtimeMessage<T>\n ): Promise<void> {\n const stub = this.getChannelStub(channel);\n\n await stub.fetch(new Request(\"https://do/broadcast\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({\n event: message.event,\n data: message.data,\n }),\n }));\n\n // Call local handlers\n const handlers = this.localHandlers.get(channel);\n if (handlers) {\n for (const handler of handlers.values()) {\n try {\n await handler(message);\n } catch {\n // Ignore handler errors\n }\n }\n }\n }\n\n async sendToSession<T = unknown>(\n _sessionId: string,\n _message: RealtimeMessage<T>\n ): Promise<boolean> {\n // For DO adapter, we send via the REST API to a specific user\n // This requires knowing which channel the session is in\n // For simplicity, we broadcast with target metadata\n return false;\n }\n\n async getSubscribers(channel: string): Promise<string[]> {\n const handlers = this.localHandlers.get(channel);\n return handlers ? Array.from(handlers.keys()) : [];\n }\n\n async setPresence<T = unknown>(\n _channel: string,\n _sessionId: string,\n _userId: string,\n _data: T\n ): Promise<void> {\n // Presence is managed via WebSocket messages in DO\n // This is a no-op for the adapter\n }\n\n async removePresence(_channel: string, _sessionId: string): Promise<void> {\n // Presence is managed via WebSocket messages in DO\n }\n\n async getPresence<T = unknown>(channel: string): Promise<PresenceUser<T>[]> {\n const stub = this.getChannelStub(channel);\n const response = await stub.fetch(new Request(\"https://do/presence\"));\n return response.json();\n }\n\n async close(): Promise<void> {\n this.localHandlers.clear();\n }\n\n /**\n * Get channel info from Durable Object\n */\n async getChannelInfo(\n channel: string\n ): Promise<{ connections: number; presence: number }> {\n const stub = this.getChannelStub(channel);\n const response = await stub.fetch(new Request(\"https://do/info\"));\n return response.json();\n }\n\n /**\n * Send message to specific user in a channel\n */\n async sendToUser<T = unknown>(\n channel: string,\n userId: string,\n event: string,\n data: T\n ): Promise<boolean> {\n const stub = this.getChannelStub(channel);\n\n const response = await stub.fetch(\n new Request(\"https://do/send\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ userId, event, data }),\n })\n );\n\n const result = (await response.json()) as { sent: boolean };\n return result.sent;\n }\n}\n\n/**\n * Create Durable Objects adapter\n */\nexport function createDurableObjectsAdapter(\n options: DurableObjectsAdapterOptions\n): DurableObjectsAdapter {\n return new DurableObjectsAdapter(options);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { RealtimeAdapter, SSEAdapterOptions, SSEConnection, MessageHandler, RealtimeMessage, PresenceUser } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @parsrun/realtime - SSE Adapter
|
|
5
|
+
* Server-Sent Events adapter for all runtimes
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* SSE Adapter
|
|
10
|
+
* Works on all runtimes (Node, Deno, Bun, Cloudflare Workers)
|
|
11
|
+
*/
|
|
12
|
+
declare class SSEAdapter implements RealtimeAdapter {
|
|
13
|
+
readonly type: "sse";
|
|
14
|
+
private options;
|
|
15
|
+
private connections;
|
|
16
|
+
private channelSubscribers;
|
|
17
|
+
private presence;
|
|
18
|
+
private pingIntervalId;
|
|
19
|
+
constructor(options?: SSEAdapterOptions);
|
|
20
|
+
/**
|
|
21
|
+
* Create SSE response for a new connection
|
|
22
|
+
*/
|
|
23
|
+
createConnection(sessionId: string, userId?: string): {
|
|
24
|
+
response: Response;
|
|
25
|
+
connection: SSEConnection;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Close a connection
|
|
29
|
+
*/
|
|
30
|
+
closeConnection(sessionId: string): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Get a connection by session ID
|
|
33
|
+
*/
|
|
34
|
+
getConnection(sessionId: string): SSEConnection | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Get all active connections
|
|
37
|
+
*/
|
|
38
|
+
getConnections(): Map<string, SSEConnection>;
|
|
39
|
+
subscribe(channel: string, sessionId: string, handler: MessageHandler): Promise<void>;
|
|
40
|
+
unsubscribe(channel: string, sessionId: string): Promise<void>;
|
|
41
|
+
publish<T = unknown>(channel: string, message: RealtimeMessage<T>): Promise<void>;
|
|
42
|
+
sendToSession<T = unknown>(sessionId: string, message: RealtimeMessage<T>): Promise<boolean>;
|
|
43
|
+
getSubscribers(channel: string): Promise<string[]>;
|
|
44
|
+
setPresence<T = unknown>(channel: string, sessionId: string, userId: string, data: T): Promise<void>;
|
|
45
|
+
removePresence(channel: string, sessionId: string): Promise<void>;
|
|
46
|
+
getPresence<T = unknown>(channel: string): Promise<PresenceUser<T>[]>;
|
|
47
|
+
close(): Promise<void>;
|
|
48
|
+
private sendToConnection;
|
|
49
|
+
private startPingInterval;
|
|
50
|
+
private sendPingToAll;
|
|
51
|
+
/**
|
|
52
|
+
* Update last ping time (call when receiving client ping)
|
|
53
|
+
*/
|
|
54
|
+
updateLastPing(sessionId: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Get statistics
|
|
57
|
+
*/
|
|
58
|
+
getStats(): {
|
|
59
|
+
totalConnections: number;
|
|
60
|
+
totalChannels: number;
|
|
61
|
+
connectionsByChannel: Record<string, number>;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Create SSE adapter
|
|
66
|
+
*/
|
|
67
|
+
declare function createSSEAdapter(options?: SSEAdapterOptions): SSEAdapter;
|
|
68
|
+
|
|
69
|
+
export { SSEAdapter, createSSEAdapter };
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var RealtimeError = class extends Error {
|
|
3
|
+
constructor(message, code, cause) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.cause = cause;
|
|
7
|
+
this.name = "RealtimeError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var RealtimeErrorCodes = {
|
|
11
|
+
CONNECTION_FAILED: "CONNECTION_FAILED",
|
|
12
|
+
CHANNEL_NOT_FOUND: "CHANNEL_NOT_FOUND",
|
|
13
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
14
|
+
MESSAGE_TOO_LARGE: "MESSAGE_TOO_LARGE",
|
|
15
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
16
|
+
ADAPTER_ERROR: "ADAPTER_ERROR",
|
|
17
|
+
INVALID_MESSAGE: "INVALID_MESSAGE"
|
|
18
|
+
};
|
|
19
|
+
function formatSSEEvent(message) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
lines.push(`id:${message.id}`);
|
|
22
|
+
lines.push(`event:${message.event}`);
|
|
23
|
+
lines.push(`data:${JSON.stringify(message)}`);
|
|
24
|
+
lines.push("");
|
|
25
|
+
return lines.join("\n") + "\n";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/adapters/sse.ts
|
|
29
|
+
var DEFAULT_OPTIONS = {
|
|
30
|
+
pingInterval: 3e4,
|
|
31
|
+
connectionTimeout: 0,
|
|
32
|
+
maxConnectionsPerChannel: 1e3,
|
|
33
|
+
retryDelay: 3e3
|
|
34
|
+
};
|
|
35
|
+
var SSEAdapter = class {
|
|
36
|
+
type = "sse";
|
|
37
|
+
options;
|
|
38
|
+
connections = /* @__PURE__ */ new Map();
|
|
39
|
+
channelSubscribers = /* @__PURE__ */ new Map();
|
|
40
|
+
presence = /* @__PURE__ */ new Map();
|
|
41
|
+
pingIntervalId = null;
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
this.options = {
|
|
44
|
+
pingInterval: options.pingInterval ?? DEFAULT_OPTIONS.pingInterval,
|
|
45
|
+
connectionTimeout: options.connectionTimeout ?? DEFAULT_OPTIONS.connectionTimeout,
|
|
46
|
+
maxConnectionsPerChannel: options.maxConnectionsPerChannel ?? DEFAULT_OPTIONS.maxConnectionsPerChannel,
|
|
47
|
+
retryDelay: options.retryDelay ?? DEFAULT_OPTIONS.retryDelay
|
|
48
|
+
};
|
|
49
|
+
if (this.options.pingInterval > 0) {
|
|
50
|
+
this.startPingInterval();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create SSE response for a new connection
|
|
55
|
+
*/
|
|
56
|
+
createConnection(sessionId, userId) {
|
|
57
|
+
const { readable, writable } = new TransformStream();
|
|
58
|
+
const writer = writable.getWriter();
|
|
59
|
+
const connection = {
|
|
60
|
+
sessionId,
|
|
61
|
+
userId,
|
|
62
|
+
channels: /* @__PURE__ */ new Set(),
|
|
63
|
+
writer,
|
|
64
|
+
state: "open",
|
|
65
|
+
createdAt: Date.now(),
|
|
66
|
+
lastPingAt: Date.now()
|
|
67
|
+
};
|
|
68
|
+
this.connections.set(sessionId, connection);
|
|
69
|
+
const encoder = new TextEncoder();
|
|
70
|
+
writer.write(encoder.encode(`retry:${this.options.retryDelay}
|
|
71
|
+
|
|
72
|
+
`));
|
|
73
|
+
const response = new Response(readable, {
|
|
74
|
+
headers: {
|
|
75
|
+
"Content-Type": "text/event-stream",
|
|
76
|
+
"Cache-Control": "no-cache",
|
|
77
|
+
"Connection": "keep-alive",
|
|
78
|
+
"X-Accel-Buffering": "no"
|
|
79
|
+
// Disable nginx buffering
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
return { response, connection };
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Close a connection
|
|
86
|
+
*/
|
|
87
|
+
async closeConnection(sessionId) {
|
|
88
|
+
const connection = this.connections.get(sessionId);
|
|
89
|
+
if (!connection) return;
|
|
90
|
+
connection.state = "closed";
|
|
91
|
+
for (const channel of connection.channels) {
|
|
92
|
+
await this.unsubscribe(channel, sessionId);
|
|
93
|
+
await this.removePresence(channel, sessionId);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await connection.writer.close();
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
this.connections.delete(sessionId);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Get a connection by session ID
|
|
103
|
+
*/
|
|
104
|
+
getConnection(sessionId) {
|
|
105
|
+
return this.connections.get(sessionId);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get all active connections
|
|
109
|
+
*/
|
|
110
|
+
getConnections() {
|
|
111
|
+
return this.connections;
|
|
112
|
+
}
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// RealtimeAdapter Implementation
|
|
115
|
+
// ============================================================================
|
|
116
|
+
async subscribe(channel, sessionId, handler) {
|
|
117
|
+
const connection = this.connections.get(sessionId);
|
|
118
|
+
if (!connection) {
|
|
119
|
+
throw new RealtimeError(
|
|
120
|
+
"Connection not found",
|
|
121
|
+
RealtimeErrorCodes.CONNECTION_FAILED
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
const subscribers = this.channelSubscribers.get(channel);
|
|
125
|
+
if (subscribers && subscribers.size >= this.options.maxConnectionsPerChannel) {
|
|
126
|
+
throw new RealtimeError(
|
|
127
|
+
"Channel subscriber limit reached",
|
|
128
|
+
RealtimeErrorCodes.RATE_LIMITED
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (!this.channelSubscribers.has(channel)) {
|
|
132
|
+
this.channelSubscribers.set(channel, /* @__PURE__ */ new Map());
|
|
133
|
+
}
|
|
134
|
+
this.channelSubscribers.get(channel).set(sessionId, handler);
|
|
135
|
+
connection.channels.add(channel);
|
|
136
|
+
await this.sendToConnection(connection, {
|
|
137
|
+
id: crypto.randomUUID(),
|
|
138
|
+
event: "channel:subscribe",
|
|
139
|
+
channel,
|
|
140
|
+
data: { channel },
|
|
141
|
+
timestamp: Date.now()
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async unsubscribe(channel, sessionId) {
|
|
145
|
+
const subscribers = this.channelSubscribers.get(channel);
|
|
146
|
+
if (subscribers) {
|
|
147
|
+
subscribers.delete(sessionId);
|
|
148
|
+
if (subscribers.size === 0) {
|
|
149
|
+
this.channelSubscribers.delete(channel);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const connection = this.connections.get(sessionId);
|
|
153
|
+
if (connection) {
|
|
154
|
+
connection.channels.delete(channel);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async publish(channel, message) {
|
|
158
|
+
const subscribers = this.channelSubscribers.get(channel);
|
|
159
|
+
if (!subscribers) return;
|
|
160
|
+
const promises = [];
|
|
161
|
+
for (const [sessionId, handler] of subscribers) {
|
|
162
|
+
const connection = this.connections.get(sessionId);
|
|
163
|
+
if (connection && connection.state === "open") {
|
|
164
|
+
promises.push(this.sendToConnection(connection, message));
|
|
165
|
+
try {
|
|
166
|
+
const result = handler(message);
|
|
167
|
+
if (result instanceof Promise) {
|
|
168
|
+
promises.push(result.then(() => {
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await Promise.allSettled(promises);
|
|
176
|
+
}
|
|
177
|
+
async sendToSession(sessionId, message) {
|
|
178
|
+
const connection = this.connections.get(sessionId);
|
|
179
|
+
if (!connection || connection.state !== "open") {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
await this.sendToConnection(connection, message);
|
|
184
|
+
return true;
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async getSubscribers(channel) {
|
|
190
|
+
const subscribers = this.channelSubscribers.get(channel);
|
|
191
|
+
return subscribers ? Array.from(subscribers.keys()) : [];
|
|
192
|
+
}
|
|
193
|
+
async setPresence(channel, sessionId, userId, data) {
|
|
194
|
+
if (!this.presence.has(channel)) {
|
|
195
|
+
this.presence.set(channel, /* @__PURE__ */ new Map());
|
|
196
|
+
}
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
const existing = this.presence.get(channel).get(sessionId);
|
|
199
|
+
const user = {
|
|
200
|
+
userId,
|
|
201
|
+
sessionId,
|
|
202
|
+
data,
|
|
203
|
+
joinedAt: existing?.joinedAt ?? now,
|
|
204
|
+
lastSeenAt: now
|
|
205
|
+
};
|
|
206
|
+
this.presence.get(channel).set(sessionId, user);
|
|
207
|
+
const eventType = existing ? "presence:update" : "presence:join";
|
|
208
|
+
await this.publish(channel, {
|
|
209
|
+
id: crypto.randomUUID(),
|
|
210
|
+
event: eventType,
|
|
211
|
+
channel,
|
|
212
|
+
data: {
|
|
213
|
+
type: existing ? "update" : "join",
|
|
214
|
+
user,
|
|
215
|
+
presence: await this.getPresence(channel)
|
|
216
|
+
},
|
|
217
|
+
timestamp: now
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async removePresence(channel, sessionId) {
|
|
221
|
+
const channelPresence = this.presence.get(channel);
|
|
222
|
+
if (!channelPresence) return;
|
|
223
|
+
const user = channelPresence.get(sessionId);
|
|
224
|
+
if (!user) return;
|
|
225
|
+
channelPresence.delete(sessionId);
|
|
226
|
+
if (channelPresence.size === 0) {
|
|
227
|
+
this.presence.delete(channel);
|
|
228
|
+
}
|
|
229
|
+
await this.publish(channel, {
|
|
230
|
+
id: crypto.randomUUID(),
|
|
231
|
+
event: "presence:leave",
|
|
232
|
+
channel,
|
|
233
|
+
data: {
|
|
234
|
+
type: "leave",
|
|
235
|
+
user,
|
|
236
|
+
presence: await this.getPresence(channel)
|
|
237
|
+
},
|
|
238
|
+
timestamp: Date.now()
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async getPresence(channel) {
|
|
242
|
+
const channelPresence = this.presence.get(channel);
|
|
243
|
+
if (!channelPresence) return [];
|
|
244
|
+
return Array.from(channelPresence.values());
|
|
245
|
+
}
|
|
246
|
+
async close() {
|
|
247
|
+
if (this.pingIntervalId) {
|
|
248
|
+
clearInterval(this.pingIntervalId);
|
|
249
|
+
this.pingIntervalId = null;
|
|
250
|
+
}
|
|
251
|
+
const closePromises = Array.from(this.connections.keys()).map(
|
|
252
|
+
(sessionId) => this.closeConnection(sessionId)
|
|
253
|
+
);
|
|
254
|
+
await Promise.allSettled(closePromises);
|
|
255
|
+
this.connections.clear();
|
|
256
|
+
this.channelSubscribers.clear();
|
|
257
|
+
this.presence.clear();
|
|
258
|
+
}
|
|
259
|
+
// ============================================================================
|
|
260
|
+
// Private Methods
|
|
261
|
+
// ============================================================================
|
|
262
|
+
async sendToConnection(connection, message) {
|
|
263
|
+
if (connection.state !== "open") return;
|
|
264
|
+
const encoder = new TextEncoder();
|
|
265
|
+
const sseEvent = formatSSEEvent(message);
|
|
266
|
+
try {
|
|
267
|
+
await connection.writer.write(encoder.encode(sseEvent));
|
|
268
|
+
} catch (err) {
|
|
269
|
+
connection.state = "closed";
|
|
270
|
+
await this.closeConnection(connection.sessionId);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
startPingInterval() {
|
|
274
|
+
this.pingIntervalId = setInterval(() => {
|
|
275
|
+
this.sendPingToAll();
|
|
276
|
+
}, this.options.pingInterval);
|
|
277
|
+
}
|
|
278
|
+
async sendPingToAll() {
|
|
279
|
+
const now = Date.now();
|
|
280
|
+
const encoder = new TextEncoder();
|
|
281
|
+
const pingEvent = `event:ping
|
|
282
|
+
data:${now}
|
|
283
|
+
|
|
284
|
+
`;
|
|
285
|
+
for (const [sessionId, connection] of this.connections) {
|
|
286
|
+
if (connection.state !== "open") continue;
|
|
287
|
+
if (this.options.connectionTimeout > 0 && now - connection.lastPingAt > this.options.connectionTimeout) {
|
|
288
|
+
await this.closeConnection(sessionId);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
try {
|
|
292
|
+
await connection.writer.write(encoder.encode(pingEvent));
|
|
293
|
+
connection.lastPingAt = now;
|
|
294
|
+
} catch {
|
|
295
|
+
await this.closeConnection(sessionId);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Update last ping time (call when receiving client ping)
|
|
301
|
+
*/
|
|
302
|
+
updateLastPing(sessionId) {
|
|
303
|
+
const connection = this.connections.get(sessionId);
|
|
304
|
+
if (connection) {
|
|
305
|
+
connection.lastPingAt = Date.now();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Get statistics
|
|
310
|
+
*/
|
|
311
|
+
getStats() {
|
|
312
|
+
const connectionsByChannel = {};
|
|
313
|
+
for (const [channel, subscribers] of this.channelSubscribers) {
|
|
314
|
+
connectionsByChannel[channel] = subscribers.size;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
totalConnections: this.connections.size,
|
|
318
|
+
totalChannels: this.channelSubscribers.size,
|
|
319
|
+
connectionsByChannel
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
function createSSEAdapter(options) {
|
|
324
|
+
return new SSEAdapter(options);
|
|
325
|
+
}
|
|
326
|
+
export {
|
|
327
|
+
SSEAdapter,
|
|
328
|
+
createSSEAdapter
|
|
329
|
+
};
|
|
330
|
+
//# sourceMappingURL=sse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types.ts","../../src/adapters/sse.ts"],"sourcesContent":["/**\n * @parsrun/realtime - Type Definitions\n * Realtime communication types and interfaces\n */\n\n// ============================================================================\n// Core Types\n// ============================================================================\n\n/**\n * Realtime adapter type\n */\nexport type RealtimeAdapterType = \"sse\" | \"durable-objects\" | \"memory\";\n\n/**\n * Realtime message\n */\nexport interface RealtimeMessage<T = unknown> {\n /** Unique message ID */\n id: string;\n /** Event type/name */\n event: string;\n /** Channel name */\n channel: string;\n /** Message payload */\n data: T;\n /** Sender ID (optional) */\n senderId?: string | undefined;\n /** Timestamp */\n timestamp: number;\n /** Metadata */\n metadata?: Record<string, unknown> | undefined;\n}\n\n/**\n * Presence user data\n */\nexport interface PresenceUser<T = unknown> {\n /** User ID */\n userId: string;\n /** Session ID */\n sessionId: string;\n /** Custom presence data */\n data: T;\n /** Join timestamp */\n joinedAt: number;\n /** Last activity timestamp */\n lastSeenAt: number;\n}\n\n/**\n * Channel info\n */\nexport interface ChannelInfo {\n /** Channel name */\n name: string;\n /** Number of subscribers */\n subscriberCount: number;\n /** Presence users */\n presence: PresenceUser[];\n /** Channel metadata */\n metadata?: Record<string, unknown> | undefined;\n}\n\n// ============================================================================\n// Events\n// ============================================================================\n\n/**\n * System event types\n */\nexport type SystemEventType =\n | \"connection:open\"\n | \"connection:close\"\n | \"connection:error\"\n | \"presence:join\"\n | \"presence:leave\"\n | \"presence:update\"\n | \"channel:subscribe\"\n | \"channel:unsubscribe\"\n | \"ping\"\n | \"pong\";\n\n/**\n * Message handler function\n */\nexport type MessageHandler<T = unknown> = (\n message: RealtimeMessage<T>\n) => void | Promise<void>;\n\n/**\n * Connection event handler\n */\nexport type ConnectionHandler = (\n event: ConnectionEvent\n) => void | Promise<void>;\n\n/**\n * Connection event\n */\nexport interface ConnectionEvent {\n type: \"open\" | \"close\" | \"error\";\n sessionId: string;\n userId?: string | undefined;\n error?: Error | undefined;\n timestamp: number;\n}\n\n/**\n * Presence event\n */\nexport interface PresenceEvent<T = unknown> {\n type: \"join\" | \"leave\" | \"update\";\n channel: string;\n user: PresenceUser<T>;\n timestamp: number;\n}\n\n// ============================================================================\n// Channel Interface\n// ============================================================================\n\n/**\n * Channel interface for realtime communication\n */\nexport interface Channel {\n /** Channel name */\n readonly name: string;\n\n /**\n * Broadcast message to all subscribers\n */\n broadcast<T = unknown>(event: string, data: T): Promise<void>;\n\n /**\n * Send message to specific user\n */\n send<T = unknown>(userId: string, event: string, data: T): Promise<void>;\n\n /**\n * Get current presence users\n */\n getPresence<T = unknown>(): Promise<PresenceUser<T>[]>;\n\n /**\n * Subscribe to channel messages\n */\n subscribe<T = unknown>(handler: MessageHandler<T>): () => void;\n\n /**\n * Subscribe to presence events\n */\n onPresence<T = unknown>(\n handler: (event: PresenceEvent<T>) => void\n ): () => void;\n\n /**\n * Get channel info\n */\n getInfo(): Promise<ChannelInfo>;\n}\n\n// ============================================================================\n// Realtime Service Interface\n// ============================================================================\n\n/**\n * Realtime service interface\n */\nexport interface RealtimeService {\n /** Adapter type */\n readonly adapterType: RealtimeAdapterType;\n\n /**\n * Get or create a channel\n */\n channel(name: string): Channel;\n\n /**\n * Broadcast to a channel\n */\n broadcast<T = unknown>(channel: string, event: string, data: T): Promise<void>;\n\n /**\n * Get channel info\n */\n getChannelInfo(channel: string): Promise<ChannelInfo | null>;\n\n /**\n * List active channels\n */\n listChannels(): Promise<string[]>;\n\n /**\n * Subscribe to connection events\n */\n onConnection(handler: ConnectionHandler): () => void;\n\n /**\n * Close all connections and cleanup\n */\n close(): Promise<void>;\n}\n\n// ============================================================================\n// Adapter Interface\n// ============================================================================\n\n/**\n * Realtime adapter interface (low-level)\n */\nexport interface RealtimeAdapter {\n /** Adapter type */\n readonly type: RealtimeAdapterType;\n\n /**\n * Add a subscriber to channel\n */\n subscribe(\n channel: string,\n sessionId: string,\n handler: MessageHandler\n ): Promise<void>;\n\n /**\n * Remove a subscriber from channel\n */\n unsubscribe(channel: string, sessionId: string): Promise<void>;\n\n /**\n * Publish message to channel\n */\n publish<T = unknown>(channel: string, message: RealtimeMessage<T>): Promise<void>;\n\n /**\n * Send to specific session\n */\n sendToSession<T = unknown>(\n sessionId: string,\n message: RealtimeMessage<T>\n ): Promise<boolean>;\n\n /**\n * Get subscribers for channel\n */\n getSubscribers(channel: string): Promise<string[]>;\n\n /**\n * Set presence for user in channel\n */\n setPresence<T = unknown>(\n channel: string,\n sessionId: string,\n userId: string,\n data: T\n ): Promise<void>;\n\n /**\n * Remove presence\n */\n removePresence(channel: string, sessionId: string): Promise<void>;\n\n /**\n * Get presence for channel\n */\n getPresence<T = unknown>(channel: string): Promise<PresenceUser<T>[]>;\n\n /**\n * Cleanup and close adapter\n */\n close(): Promise<void>;\n}\n\n// ============================================================================\n// SSE Types\n// ============================================================================\n\n/**\n * SSE connection\n */\nexport interface SSEConnection {\n /** Session ID */\n sessionId: string;\n /** User ID */\n userId?: string | undefined;\n /** Subscribed channels */\n channels: Set<string>;\n /** Response writer */\n writer: WritableStreamDefaultWriter<Uint8Array>;\n /** Connection state */\n state: \"open\" | \"closed\";\n /** Created timestamp */\n createdAt: number;\n /** Last ping timestamp */\n lastPingAt: number;\n}\n\n/**\n * SSE adapter options\n */\nexport interface SSEAdapterOptions {\n /** Ping interval in milliseconds (default: 30000) */\n pingInterval?: number | undefined;\n /** Connection timeout in milliseconds (default: 0 = no timeout) */\n connectionTimeout?: number | undefined;\n /** Max connections per channel (default: 1000) */\n maxConnectionsPerChannel?: number | undefined;\n /** Retry delay for client reconnection (default: 3000) */\n retryDelay?: number | undefined;\n}\n\n// ============================================================================\n// Durable Objects Types\n// ============================================================================\n\n/**\n * Durable Objects adapter options\n */\nexport interface DurableObjectsAdapterOptions {\n /** Durable Object namespace binding */\n namespace: DurableObjectNamespace;\n /** Channel prefix (default: \"channel:\") */\n channelPrefix?: string | undefined;\n}\n\n/**\n * Durable Object WebSocket session\n */\nexport interface DOWebSocketSession {\n /** Session ID */\n sessionId: string;\n /** User ID */\n userId?: string | undefined;\n /** WebSocket connection */\n webSocket: WebSocket;\n /** Presence data */\n presence?: unknown;\n /** Connection state */\n state: \"connecting\" | \"open\" | \"closing\" | \"closed\";\n /** Created timestamp */\n createdAt: number;\n}\n\n/**\n * Durable Object state stored in storage\n */\nexport interface DOChannelState {\n /** Channel name */\n name: string;\n /** Channel metadata */\n metadata?: Record<string, unknown> | undefined;\n /** Created timestamp */\n createdAt: number;\n}\n\n// ============================================================================\n// Config Types\n// ============================================================================\n\n/**\n * Realtime config\n */\nexport interface RealtimeConfig {\n /** Adapter type */\n adapter: RealtimeAdapterType;\n\n /** SSE adapter options */\n sse?: SSEAdapterOptions | undefined;\n\n /** Durable Objects adapter options */\n durableObjects?: DurableObjectsAdapterOptions | undefined;\n\n /** Enable debug logging */\n debug?: boolean | undefined;\n}\n\n// ============================================================================\n// Errors\n// ============================================================================\n\n/**\n * Realtime error\n */\nexport class RealtimeError extends Error {\n constructor(\n message: string,\n public readonly code: string,\n public readonly cause?: unknown\n ) {\n super(message);\n this.name = \"RealtimeError\";\n }\n}\n\n/**\n * Common error codes\n */\nexport const RealtimeErrorCodes = {\n CONNECTION_FAILED: \"CONNECTION_FAILED\",\n CHANNEL_NOT_FOUND: \"CHANNEL_NOT_FOUND\",\n UNAUTHORIZED: \"UNAUTHORIZED\",\n MESSAGE_TOO_LARGE: \"MESSAGE_TOO_LARGE\",\n RATE_LIMITED: \"RATE_LIMITED\",\n ADAPTER_ERROR: \"ADAPTER_ERROR\",\n INVALID_MESSAGE: \"INVALID_MESSAGE\",\n} as const;\n\n// ============================================================================\n// Utility Types\n// ============================================================================\n\n/**\n * Create message options\n */\nexport interface CreateMessageOptions<T = unknown> {\n event: string;\n channel: string;\n data: T;\n senderId?: string | undefined;\n metadata?: Record<string, unknown> | undefined;\n}\n\n/**\n * Create a realtime message\n */\nexport function createMessage<T = unknown>(\n options: CreateMessageOptions<T>\n): RealtimeMessage<T> {\n return {\n id: crypto.randomUUID(),\n event: options.event,\n channel: options.channel,\n data: options.data,\n senderId: options.senderId,\n timestamp: Date.now(),\n metadata: options.metadata,\n };\n}\n\n/**\n * Parse SSE event string\n */\nexport function parseSSEEvent(eventString: string): RealtimeMessage | null {\n try {\n const lines = eventString.split(\"\\n\");\n let event = \"message\";\n let data = \"\";\n let id = \"\";\n\n for (const line of lines) {\n if (line.startsWith(\"event:\")) {\n event = line.slice(6).trim();\n } else if (line.startsWith(\"data:\")) {\n data = line.slice(5).trim();\n } else if (line.startsWith(\"id:\")) {\n id = line.slice(3).trim();\n }\n }\n\n if (!data) return null;\n\n const parsed = JSON.parse(data);\n return {\n id: id || parsed.id || crypto.randomUUID(),\n event,\n channel: parsed.channel || \"\",\n data: parsed.data ?? parsed,\n senderId: parsed.senderId,\n timestamp: parsed.timestamp || Date.now(),\n metadata: parsed.metadata,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Format message as SSE event string\n */\nexport function formatSSEEvent(message: RealtimeMessage): string {\n const lines: string[] = [];\n\n lines.push(`id:${message.id}`);\n lines.push(`event:${message.event}`);\n lines.push(`data:${JSON.stringify(message)}`);\n lines.push(\"\"); // Empty line to end event\n\n return lines.join(\"\\n\") + \"\\n\";\n}\n","/**\n * @parsrun/realtime - SSE Adapter\n * Server-Sent Events adapter for all runtimes\n */\n\nimport type {\n MessageHandler,\n PresenceUser,\n RealtimeAdapter,\n RealtimeMessage,\n SSEAdapterOptions,\n SSEConnection,\n} from \"../types.js\";\nimport { formatSSEEvent, RealtimeError, RealtimeErrorCodes } from \"../types.js\";\n\n/**\n * Resolved SSE adapter options (all required)\n */\ninterface ResolvedSSEOptions {\n pingInterval: number;\n connectionTimeout: number;\n maxConnectionsPerChannel: number;\n retryDelay: number;\n}\n\n/**\n * Default SSE adapter options\n */\nconst DEFAULT_OPTIONS: ResolvedSSEOptions = {\n pingInterval: 30000,\n connectionTimeout: 0,\n maxConnectionsPerChannel: 1000,\n retryDelay: 3000,\n};\n\n/**\n * SSE Adapter\n * Works on all runtimes (Node, Deno, Bun, Cloudflare Workers)\n */\nexport class SSEAdapter implements RealtimeAdapter {\n readonly type = \"sse\" as const;\n\n private options: ResolvedSSEOptions;\n private connections: Map<string, SSEConnection> = new Map();\n private channelSubscribers: Map<string, Map<string, MessageHandler>> = new Map();\n private presence: Map<string, Map<string, PresenceUser>> = new Map();\n private pingIntervalId: ReturnType<typeof setInterval> | null = null;\n\n constructor(options: SSEAdapterOptions = {}) {\n this.options = {\n pingInterval: options.pingInterval ?? DEFAULT_OPTIONS.pingInterval,\n connectionTimeout: options.connectionTimeout ?? DEFAULT_OPTIONS.connectionTimeout,\n maxConnectionsPerChannel: options.maxConnectionsPerChannel ?? DEFAULT_OPTIONS.maxConnectionsPerChannel,\n retryDelay: options.retryDelay ?? DEFAULT_OPTIONS.retryDelay,\n };\n\n // Start ping interval\n if (this.options.pingInterval > 0) {\n this.startPingInterval();\n }\n }\n\n /**\n * Create SSE response for a new connection\n */\n createConnection(\n sessionId: string,\n userId?: string\n ): { response: Response; connection: SSEConnection } {\n const { readable, writable } = new TransformStream<Uint8Array>();\n const writer = writable.getWriter();\n\n const connection: SSEConnection = {\n sessionId,\n userId,\n channels: new Set(),\n writer,\n state: \"open\",\n createdAt: Date.now(),\n lastPingAt: Date.now(),\n };\n\n this.connections.set(sessionId, connection);\n\n // Send initial retry directive\n const encoder = new TextEncoder();\n writer.write(encoder.encode(`retry:${this.options.retryDelay}\\n\\n`));\n\n const response = new Response(readable, {\n headers: {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n \"Connection\": \"keep-alive\",\n \"X-Accel-Buffering\": \"no\", // Disable nginx buffering\n },\n });\n\n return { response, connection };\n }\n\n /**\n * Close a connection\n */\n async closeConnection(sessionId: string): Promise<void> {\n const connection = this.connections.get(sessionId);\n if (!connection) return;\n\n connection.state = \"closed\";\n\n // Remove from all channels\n for (const channel of connection.channels) {\n await this.unsubscribe(channel, sessionId);\n await this.removePresence(channel, sessionId);\n }\n\n // Close writer\n try {\n await connection.writer.close();\n } catch {\n // Ignore close errors\n }\n\n this.connections.delete(sessionId);\n }\n\n /**\n * Get a connection by session ID\n */\n getConnection(sessionId: string): SSEConnection | undefined {\n return this.connections.get(sessionId);\n }\n\n /**\n * Get all active connections\n */\n getConnections(): Map<string, SSEConnection> {\n return this.connections;\n }\n\n // ============================================================================\n // RealtimeAdapter Implementation\n // ============================================================================\n\n async subscribe(\n channel: string,\n sessionId: string,\n handler: MessageHandler\n ): Promise<void> {\n const connection = this.connections.get(sessionId);\n if (!connection) {\n throw new RealtimeError(\n \"Connection not found\",\n RealtimeErrorCodes.CONNECTION_FAILED\n );\n }\n\n // Check max connections per channel\n const subscribers = this.channelSubscribers.get(channel);\n if (\n subscribers &&\n subscribers.size >= this.options.maxConnectionsPerChannel\n ) {\n throw new RealtimeError(\n \"Channel subscriber limit reached\",\n RealtimeErrorCodes.RATE_LIMITED\n );\n }\n\n // Add to channel subscribers\n if (!this.channelSubscribers.has(channel)) {\n this.channelSubscribers.set(channel, new Map());\n }\n this.channelSubscribers.get(channel)!.set(sessionId, handler);\n\n // Track channel on connection\n connection.channels.add(channel);\n\n // Send subscription confirmation\n await this.sendToConnection(connection, {\n id: crypto.randomUUID(),\n event: \"channel:subscribe\",\n channel,\n data: { channel },\n timestamp: Date.now(),\n });\n }\n\n async unsubscribe(channel: string, sessionId: string): Promise<void> {\n const subscribers = this.channelSubscribers.get(channel);\n if (subscribers) {\n subscribers.delete(sessionId);\n if (subscribers.size === 0) {\n this.channelSubscribers.delete(channel);\n }\n }\n\n const connection = this.connections.get(sessionId);\n if (connection) {\n connection.channels.delete(channel);\n }\n }\n\n async publish<T = unknown>(\n channel: string,\n message: RealtimeMessage<T>\n ): Promise<void> {\n const subscribers = this.channelSubscribers.get(channel);\n if (!subscribers) return;\n\n const promises: Promise<void>[] = [];\n\n for (const [sessionId, handler] of subscribers) {\n const connection = this.connections.get(sessionId);\n if (connection && connection.state === \"open\") {\n // Send via SSE\n promises.push(this.sendToConnection(connection, message));\n\n // Call handler\n try {\n const result = handler(message);\n if (result instanceof Promise) {\n promises.push(result.then(() => {}));\n }\n } catch {\n // Ignore handler errors\n }\n }\n }\n\n await Promise.allSettled(promises);\n }\n\n async sendToSession<T = unknown>(\n sessionId: string,\n message: RealtimeMessage<T>\n ): Promise<boolean> {\n const connection = this.connections.get(sessionId);\n if (!connection || connection.state !== \"open\") {\n return false;\n }\n\n try {\n await this.sendToConnection(connection, message);\n return true;\n } catch {\n return false;\n }\n }\n\n async getSubscribers(channel: string): Promise<string[]> {\n const subscribers = this.channelSubscribers.get(channel);\n return subscribers ? Array.from(subscribers.keys()) : [];\n }\n\n async setPresence<T = unknown>(\n channel: string,\n sessionId: string,\n userId: string,\n data: T\n ): Promise<void> {\n if (!this.presence.has(channel)) {\n this.presence.set(channel, new Map());\n }\n\n const now = Date.now();\n const existing = this.presence.get(channel)!.get(sessionId);\n\n const user: PresenceUser<T> = {\n userId,\n sessionId,\n data,\n joinedAt: existing?.joinedAt ?? now,\n lastSeenAt: now,\n };\n\n this.presence.get(channel)!.set(sessionId, user as PresenceUser);\n\n // Broadcast presence update\n const eventType = existing ? \"presence:update\" : \"presence:join\";\n await this.publish(channel, {\n id: crypto.randomUUID(),\n event: eventType,\n channel,\n data: {\n type: existing ? \"update\" : \"join\",\n user,\n presence: await this.getPresence(channel),\n },\n timestamp: now,\n });\n }\n\n async removePresence(channel: string, sessionId: string): Promise<void> {\n const channelPresence = this.presence.get(channel);\n if (!channelPresence) return;\n\n const user = channelPresence.get(sessionId);\n if (!user) return;\n\n channelPresence.delete(sessionId);\n if (channelPresence.size === 0) {\n this.presence.delete(channel);\n }\n\n // Broadcast presence leave\n await this.publish(channel, {\n id: crypto.randomUUID(),\n event: \"presence:leave\",\n channel,\n data: {\n type: \"leave\",\n user,\n presence: await this.getPresence(channel),\n },\n timestamp: Date.now(),\n });\n }\n\n async getPresence<T = unknown>(channel: string): Promise<PresenceUser<T>[]> {\n const channelPresence = this.presence.get(channel);\n if (!channelPresence) return [];\n return Array.from(channelPresence.values()) as PresenceUser<T>[];\n }\n\n async close(): Promise<void> {\n // Stop ping interval\n if (this.pingIntervalId) {\n clearInterval(this.pingIntervalId);\n this.pingIntervalId = null;\n }\n\n // Close all connections\n const closePromises = Array.from(this.connections.keys()).map((sessionId) =>\n this.closeConnection(sessionId)\n );\n await Promise.allSettled(closePromises);\n\n // Clear all data\n this.connections.clear();\n this.channelSubscribers.clear();\n this.presence.clear();\n }\n\n // ============================================================================\n // Private Methods\n // ============================================================================\n\n private async sendToConnection<T = unknown>(\n connection: SSEConnection,\n message: RealtimeMessage<T>\n ): Promise<void> {\n if (connection.state !== \"open\") return;\n\n const encoder = new TextEncoder();\n const sseEvent = formatSSEEvent(message as RealtimeMessage);\n\n try {\n await connection.writer.write(encoder.encode(sseEvent));\n } catch (err) {\n // Connection likely closed\n connection.state = \"closed\";\n await this.closeConnection(connection.sessionId);\n }\n }\n\n private startPingInterval(): void {\n this.pingIntervalId = setInterval(() => {\n this.sendPingToAll();\n }, this.options.pingInterval);\n }\n\n private async sendPingToAll(): Promise<void> {\n const now = Date.now();\n const encoder = new TextEncoder();\n const pingEvent = `event:ping\\ndata:${now}\\n\\n`;\n\n for (const [sessionId, connection] of this.connections) {\n if (connection.state !== \"open\") continue;\n\n // Check connection timeout\n if (\n this.options.connectionTimeout > 0 &&\n now - connection.lastPingAt > this.options.connectionTimeout\n ) {\n await this.closeConnection(sessionId);\n continue;\n }\n\n try {\n await connection.writer.write(encoder.encode(pingEvent));\n connection.lastPingAt = now;\n } catch {\n // Connection likely closed\n await this.closeConnection(sessionId);\n }\n }\n }\n\n /**\n * Update last ping time (call when receiving client ping)\n */\n updateLastPing(sessionId: string): void {\n const connection = this.connections.get(sessionId);\n if (connection) {\n connection.lastPingAt = Date.now();\n }\n }\n\n /**\n * Get statistics\n */\n getStats(): {\n totalConnections: number;\n totalChannels: number;\n connectionsByChannel: Record<string, number>;\n } {\n const connectionsByChannel: Record<string, number> = {};\n\n for (const [channel, subscribers] of this.channelSubscribers) {\n connectionsByChannel[channel] = subscribers.size;\n }\n\n return {\n totalConnections: this.connections.size,\n totalChannels: this.channelSubscribers.size,\n connectionsByChannel,\n };\n }\n}\n\n/**\n * Create SSE adapter\n */\nexport function createSSEAdapter(options?: SSEAdapterOptions): SSEAdapter {\n return new SSEAdapter(options);\n}\n"],"mappings":"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","names":[]}
|