@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
package/dist/hono.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hono.ts","../src/types.ts","../src/adapters/sse.ts"],"sourcesContent":["/**\n * @parsrun/realtime - Hono Integration\n * Middleware and routes for Hono framework\n */\n\nimport { Hono } from \"hono\";\nimport type { Context, MiddlewareHandler } from \"hono\";\nimport { SSEAdapter } from \"./adapters/sse.js\";\nimport type {\n RealtimeAdapter,\n SSEAdapterOptions,\n} from \"./types.js\";\nimport { createMessage } from \"./types.js\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Hono realtime context variables\n */\nexport interface RealtimeVariables {\n realtime: {\n adapter: RealtimeAdapter;\n sessionId: string;\n userId: string | undefined;\n };\n}\n\n/**\n * SSE route options\n */\nexport interface SSERouteOptions {\n /** Get session ID from context (default: query param or random) */\n getSessionId?: (c: Context) => string;\n /** Get user ID from context (default: from auth) */\n getUserId?: (c: Context) => string | undefined;\n /** Channels the user can subscribe to */\n getChannels?: (c: Context) => string[];\n /** Called when connection opens */\n onConnect?: (c: Context, sessionId: string) => void | Promise<void>;\n /** Called when connection closes */\n onDisconnect?: (c: Context, sessionId: string) => void | Promise<void>;\n}\n\n/**\n * Durable Objects route options\n */\nexport interface DORouteOptions {\n /** Durable Object namespace binding name */\n namespaceBinding: string;\n /** Channel prefix */\n channelPrefix?: string;\n /** Authorize connection */\n authorize?: (c: Context, channel: string) => boolean | Promise<boolean>;\n}\n\n// ============================================================================\n// SSE Middleware and Routes\n// ============================================================================\n\n/**\n * Create SSE realtime middleware\n * Adds SSE adapter to context\n */\nexport function sseMiddleware(\n options?: SSEAdapterOptions\n): MiddlewareHandler<{ Variables: RealtimeVariables }> {\n const adapter = new SSEAdapter(options);\n\n return async (c, next) => {\n const sessionId = c.req.query(\"sessionId\") || crypto.randomUUID();\n // Get userId from context if set by auth middleware (type-safe access)\n const vars = c.var as Record<string, unknown>;\n const userId = typeof vars[\"userId\"] === \"string\" ? vars[\"userId\"] : undefined;\n\n c.set(\"realtime\", {\n adapter,\n sessionId,\n userId,\n });\n\n await next();\n };\n}\n\n/**\n * Create SSE subscription endpoint\n * Client connects to this endpoint to receive events\n */\nexport function createSSEHandler(\n adapter: SSEAdapter,\n options: SSERouteOptions = {}\n): (c: Context) => Response | Promise<Response> {\n return async (c: Context) => {\n const sessionId = options.getSessionId?.(c) ||\n c.req.query(\"sessionId\") ||\n crypto.randomUUID();\n\n const vars = c.var as Record<string, unknown>;\n const userId = options.getUserId?.(c) ||\n (typeof vars[\"userId\"] === \"string\" ? vars[\"userId\"] : undefined);\n\n const channels = options.getChannels?.(c) ||\n c.req.query(\"channels\")?.split(\",\") ||\n [];\n\n // Create SSE connection\n const { response } = adapter.createConnection(sessionId, userId);\n\n // Subscribe to requested channels\n for (const channel of channels) {\n await adapter.subscribe(channel, sessionId, () => {\n // Handler is called but SSE sends via connection.writer\n });\n }\n\n // Call onConnect callback\n if (options.onConnect) {\n await options.onConnect(c, sessionId);\n }\n\n // Handle connection close\n c.req.raw.signal.addEventListener(\"abort\", async () => {\n await adapter.closeConnection(sessionId);\n if (options.onDisconnect) {\n await options.onDisconnect(c, sessionId);\n }\n });\n\n return response;\n };\n}\n\n/**\n * Create complete SSE routes for Hono\n */\nexport function createSSERoutes(\n adapter: SSEAdapter,\n options: SSERouteOptions = {}\n): Hono {\n const app = new Hono();\n\n // SSE subscription endpoint\n app.get(\"/subscribe\", createSSEHandler(adapter, options));\n\n // Subscribe to additional channel\n app.post(\"/subscribe/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n const sessionId = c.req.query(\"sessionId\");\n\n if (!sessionId) {\n return c.json({ error: \"sessionId required\" }, 400);\n }\n\n const connection = adapter.getConnection(sessionId);\n if (!connection) {\n return c.json({ error: \"Connection not found\" }, 404);\n }\n\n await adapter.subscribe(channel, sessionId, () => {});\n\n return c.json({ success: true, channel });\n });\n\n // Unsubscribe from channel\n app.post(\"/unsubscribe/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n const sessionId = c.req.query(\"sessionId\");\n\n if (!sessionId) {\n return c.json({ error: \"sessionId required\" }, 400);\n }\n\n await adapter.unsubscribe(channel, sessionId);\n\n return c.json({ success: true, channel });\n });\n\n // Broadcast to channel\n app.post(\"/broadcast/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n const body = await c.req.json<{ event: string; data: unknown }>();\n\n const message = createMessage({\n event: body.event,\n channel,\n data: body.data,\n });\n\n await adapter.publish(channel, message);\n\n return c.json({ success: true });\n });\n\n // Get channel presence\n app.get(\"/presence/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n const presence = await adapter.getPresence(channel);\n return c.json(presence);\n });\n\n // Set presence\n app.post(\"/presence/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n const sessionId = c.req.query(\"sessionId\");\n const userId = c.req.query(\"userId\") || sessionId;\n const data = await c.req.json();\n\n if (!sessionId) {\n return c.json({ error: \"sessionId required\" }, 400);\n }\n\n await adapter.setPresence(channel, sessionId, userId!, data);\n\n return c.json({ success: true });\n });\n\n // Remove presence\n app.delete(\"/presence/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n const sessionId = c.req.query(\"sessionId\");\n\n if (!sessionId) {\n return c.json({ error: \"sessionId required\" }, 400);\n }\n\n await adapter.removePresence(channel, sessionId);\n\n return c.json({ success: true });\n });\n\n // Get stats\n app.get(\"/stats\", (c) => {\n return c.json(adapter.getStats());\n });\n\n return app;\n}\n\n// ============================================================================\n// Durable Objects Routes\n// ============================================================================\n\n/**\n * Create Durable Objects realtime routes for Hono\n * Proxies requests to the appropriate Durable Object\n */\nexport function createDORoutes(options: DORouteOptions): Hono {\n const app = new Hono();\n const prefix = options.channelPrefix ?? \"channel:\";\n\n // WebSocket upgrade handler\n app.get(\"/ws/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n\n // Authorization check\n if (options.authorize) {\n const authorized = await options.authorize(c, channel);\n if (!authorized) {\n return c.json({ error: \"Unauthorized\" }, 403);\n }\n }\n\n // Get Durable Object namespace from env\n const env = c.env as Record<string, unknown>;\n const namespace = env[options.namespaceBinding] as DurableObjectNamespace;\n\n if (!namespace) {\n return c.json(\n { error: `Namespace ${options.namespaceBinding} not found` },\n 500\n );\n }\n\n // Get Durable Object stub\n const id = namespace.idFromName(`${prefix}${channel}`);\n const stub = namespace.get(id);\n\n // Forward the request to the Durable Object\n const url = new URL(c.req.url);\n url.pathname = `/ws/${channel}`;\n\n return stub.fetch(new Request(url.toString(), c.req.raw));\n });\n\n // Broadcast to channel\n app.post(\"/broadcast/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n\n const env = c.env as Record<string, unknown>;\n const namespace = env[options.namespaceBinding] as DurableObjectNamespace;\n\n if (!namespace) {\n return c.json(\n { error: `Namespace ${options.namespaceBinding} not found` },\n 500\n );\n }\n\n const id = namespace.idFromName(`${prefix}${channel}`);\n const stub = namespace.get(id);\n\n const body = await c.req.json();\n\n const response = await stub.fetch(\n new Request(\"https://do/broadcast\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(body),\n })\n );\n\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n });\n\n // Get channel presence\n app.get(\"/presence/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n\n const env = c.env as Record<string, unknown>;\n const namespace = env[options.namespaceBinding] as DurableObjectNamespace;\n\n if (!namespace) {\n return c.json(\n { error: `Namespace ${options.namespaceBinding} not found` },\n 500\n );\n }\n\n const id = namespace.idFromName(`${prefix}${channel}`);\n const stub = namespace.get(id);\n\n const response = await stub.fetch(new Request(\"https://do/presence\"));\n\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n });\n\n // Get channel info\n app.get(\"/info/:channel\", async (c) => {\n const channel = c.req.param(\"channel\");\n\n const env = c.env as Record<string, unknown>;\n const namespace = env[options.namespaceBinding] as DurableObjectNamespace;\n\n if (!namespace) {\n return c.json(\n { error: `Namespace ${options.namespaceBinding} not found` },\n 500\n );\n }\n\n const id = namespace.idFromName(`${prefix}${channel}`);\n const stub = namespace.get(id);\n\n const response = await stub.fetch(new Request(\"https://do/info\"));\n\n return new Response(response.body, {\n status: response.status,\n headers: response.headers,\n });\n });\n\n return app;\n}\n\n// ============================================================================\n// Utilities\n// ============================================================================\n\n/**\n * Broadcast helper for use in route handlers\n */\nexport async function broadcast(\n adapter: RealtimeAdapter,\n channel: string,\n event: string,\n data: unknown\n): Promise<void> {\n const message = createMessage({ event, channel, data });\n await adapter.publish(channel, message);\n}\n\n/**\n * Send to user helper\n */\nexport async function sendToUser(\n adapter: RealtimeAdapter,\n channel: string,\n userId: string,\n event: string,\n data: unknown\n): Promise<void> {\n const presence = await adapter.getPresence(channel);\n const userSessions = presence.filter((p) => p.userId === userId);\n\n const message = createMessage({ event, channel, data });\n\n for (const session of userSessions) {\n await adapter.sendToSession(session.sessionId, message);\n }\n}\n","/**\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":[]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Channel, RealtimeAdapter, PresenceUser, MessageHandler, PresenceEvent, ChannelInfo, RealtimeConfig } from './types.js';
|
|
2
|
+
export { ConnectionEvent, ConnectionHandler, CreateMessageOptions, DOChannelState, DOWebSocketSession, DurableObjectsAdapterOptions, RealtimeAdapterType, RealtimeError, RealtimeErrorCodes, RealtimeMessage, RealtimeService, SSEAdapterOptions, SSEConnection, SystemEventType, createMessage, formatSSEEvent, parseSSEEvent } from './types.js';
|
|
3
|
+
import { createSSEAdapter } from './adapters/sse.js';
|
|
4
|
+
export { SSEAdapter } from './adapters/sse.js';
|
|
5
|
+
import { createDurableObjectsAdapter } from './adapters/durable-objects.js';
|
|
6
|
+
export { DurableObjectsAdapter, RealtimeChannelDO } from './adapters/durable-objects.js';
|
|
7
|
+
export { DORouteOptions, RealtimeVariables, SSERouteOptions, broadcast, createDORoutes, createSSEHandler, createSSERoutes, sendToUser, sseMiddleware } from './hono.js';
|
|
8
|
+
import 'hono';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @parsrun/realtime - Channel Implementation
|
|
12
|
+
* Channel abstraction for realtime communication
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Channel implementation using an adapter
|
|
17
|
+
*/
|
|
18
|
+
declare class ChannelImpl implements Channel {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
private adapter;
|
|
21
|
+
private sessionId;
|
|
22
|
+
private presenceHandlers;
|
|
23
|
+
constructor(name: string, adapter: RealtimeAdapter, sessionId: string);
|
|
24
|
+
broadcast<T = unknown>(event: string, data: T): Promise<void>;
|
|
25
|
+
send<T = unknown>(userId: string, event: string, data: T): Promise<void>;
|
|
26
|
+
getPresence<T = unknown>(): Promise<PresenceUser<T>[]>;
|
|
27
|
+
subscribe<T = unknown>(handler: MessageHandler<T>): () => void;
|
|
28
|
+
onPresence<T = unknown>(handler: (event: PresenceEvent<T>) => void): () => void;
|
|
29
|
+
/**
|
|
30
|
+
* Emit presence event to handlers
|
|
31
|
+
*/
|
|
32
|
+
emitPresence<T = unknown>(event: PresenceEvent<T>): void;
|
|
33
|
+
getInfo(): Promise<ChannelInfo>;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Create a channel instance
|
|
37
|
+
*/
|
|
38
|
+
declare function createChannel(name: string, adapter: RealtimeAdapter, sessionId: string): Channel;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @parsrun/realtime
|
|
42
|
+
* Edge-compatible realtime for Pars
|
|
43
|
+
*
|
|
44
|
+
* Supports multiple adapters:
|
|
45
|
+
* - SSE (Server-Sent Events) - Works on all runtimes
|
|
46
|
+
* - Durable Objects - Cloudflare Workers with WebSocket
|
|
47
|
+
*
|
|
48
|
+
* @example SSE Usage
|
|
49
|
+
* ```typescript
|
|
50
|
+
* import { createSSEAdapter, createSSERoutes } from '@parsrun/realtime';
|
|
51
|
+
* import { Hono } from 'hono';
|
|
52
|
+
*
|
|
53
|
+
* const app = new Hono();
|
|
54
|
+
* const sse = createSSEAdapter({ pingInterval: 30000 });
|
|
55
|
+
*
|
|
56
|
+
* // Mount realtime routes
|
|
57
|
+
* app.route('/realtime', createSSERoutes(sse));
|
|
58
|
+
*
|
|
59
|
+
* // Broadcast from anywhere
|
|
60
|
+
* await sse.publish('orders', createMessage({
|
|
61
|
+
* event: 'order:created',
|
|
62
|
+
* channel: 'orders',
|
|
63
|
+
* data: { orderId: '123' }
|
|
64
|
+
* }));
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @example Durable Objects Usage
|
|
68
|
+
* ```typescript
|
|
69
|
+
* import { RealtimeChannelDO, createDORoutes } from '@parsrun/realtime';
|
|
70
|
+
* import { Hono } from 'hono';
|
|
71
|
+
*
|
|
72
|
+
* const app = new Hono();
|
|
73
|
+
*
|
|
74
|
+
* // Mount DO routes
|
|
75
|
+
* app.route('/realtime', createDORoutes({
|
|
76
|
+
* namespaceBinding: 'REALTIME_CHANNELS'
|
|
77
|
+
* }));
|
|
78
|
+
*
|
|
79
|
+
* // Export DO class for wrangler
|
|
80
|
+
* export { RealtimeChannelDO };
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example Client-side SSE
|
|
84
|
+
* ```typescript
|
|
85
|
+
* const eventSource = new EventSource('/realtime/subscribe?channels=orders');
|
|
86
|
+
*
|
|
87
|
+
* eventSource.addEventListener('order:created', (e) => {
|
|
88
|
+
* const data = JSON.parse(e.data);
|
|
89
|
+
* console.log('New order:', data);
|
|
90
|
+
* });
|
|
91
|
+
* ```
|
|
92
|
+
*
|
|
93
|
+
* @example Client-side WebSocket (DO)
|
|
94
|
+
* ```typescript
|
|
95
|
+
* const ws = new WebSocket('wss://api.example.com/realtime/ws/orders');
|
|
96
|
+
*
|
|
97
|
+
* ws.onmessage = (e) => {
|
|
98
|
+
* const message = JSON.parse(e.data);
|
|
99
|
+
* console.log('Received:', message);
|
|
100
|
+
* };
|
|
101
|
+
*
|
|
102
|
+
* // Join presence
|
|
103
|
+
* ws.send(JSON.stringify({
|
|
104
|
+
* event: 'presence:join',
|
|
105
|
+
* data: { name: 'John', status: 'online' }
|
|
106
|
+
* }));
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Create a realtime adapter based on config
|
|
112
|
+
*/
|
|
113
|
+
declare function createRealtimeAdapter(config: RealtimeConfig): RealtimeAdapter;
|
|
114
|
+
declare const _default: {
|
|
115
|
+
createRealtimeAdapter: typeof createRealtimeAdapter;
|
|
116
|
+
createSSEAdapter: typeof createSSEAdapter;
|
|
117
|
+
createDurableObjectsAdapter: typeof createDurableObjectsAdapter;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export { Channel, ChannelImpl, ChannelInfo, MessageHandler, PresenceEvent, PresenceUser, RealtimeAdapter, RealtimeConfig, createChannel, createDurableObjectsAdapter, createRealtimeAdapter, createSSEAdapter, _default as default };
|