@spfn/core 0.2.0-beta.45 → 0.2.0-beta.48
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/dist/event/index.d.ts +6 -2
- package/dist/event/index.js +12 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +4 -3
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +59 -0
- package/dist/event/ws/client.js +273 -0
- package/dist/event/ws/client.js.map +1 -0
- package/dist/event/ws/index.d.ts +94 -0
- package/dist/event/ws/index.js +213 -0
- package/dist/event/ws/index.js.map +1 -0
- package/dist/server/index.d.ts +58 -2
- package/dist/server/index.js +285 -2
- package/dist/server/index.js.map +1 -1
- package/dist/{router-Di7ENoah.d.ts → token-manager-CyG7la3p.d.ts} +116 -1
- package/dist/{types-DKQ90YL7.d.ts → types-C1jMLGwK.d.ts} +2 -117
- package/dist/types-Cfj--lfr.d.ts +151 -0
- package/package.json +14 -2
package/dist/event/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
-
import { a as EventDef } from '../
|
|
3
|
-
export { e as EventHandler, E as EventRouterDef, I as InferEventNames, f as InferEventPayload, c as InferEventPayloads, b as InferRouterEventPayload, J as JobQueueSender, P as PubSubCache, d as defineEventRouter } from '../
|
|
2
|
+
import { a as EventDef } from '../token-manager-CyG7la3p.js';
|
|
3
|
+
export { e as EventHandler, E as EventRouterDef, I as InferEventNames, f as InferEventPayload, c as InferEventPayloads, b as InferRouterEventPayload, J as JobQueueSender, P as PubSubCache, d as defineEventRouter } from '../token-manager-CyG7la3p.js';
|
|
4
|
+
export { defineWSRouter } from './ws/index.js';
|
|
5
|
+
import '../types-Cfj--lfr.js';
|
|
6
|
+
import 'hono';
|
|
7
|
+
import 'node:http';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Event System
|
package/dist/event/index.js
CHANGED
|
@@ -130,7 +130,18 @@ function defineEventRouter(events) {
|
|
|
130
130
|
_types: {}
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
+
logger.child("@spfn/core:ws");
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
// src/event/ws/index.ts
|
|
136
|
+
function defineWSRouter(def) {
|
|
137
|
+
return {
|
|
138
|
+
events: def.events,
|
|
139
|
+
eventNames: Object.keys(def.events),
|
|
140
|
+
messages: def.messages ?? {},
|
|
141
|
+
_types: {}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { defineEvent, defineEventRouter, defineWSRouter, eventRouteMap };
|
|
135
146
|
//# sourceMappingURL=index.js.map
|
|
136
147
|
//# sourceMappingURL=index.js.map
|
package/dist/event/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/event/event.ts","../../src/event/sse/route-map.ts","../../src/event/router.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Event System\n *\n * Decoupled pub/sub event system with optional cache integration for multi-instance support\n *\n * @example\n * ```typescript\n * // Define event\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe (in-memory)\n * userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit\n * await userCreated.emit({ userId: '123' });\n *\n * // With cache for multi-instance\n * const event = defineEvent('user.created', schema);\n * await event.useCache(cache); // Must await before emitting\n * await event.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\n\nimport type { TSchema, Static } from '@sinclair/typebox';\nimport { logger } from '@spfn/core/logger';\nimport type { EventDef, EventHandler, JobQueueSender, PubSubCache } from './types';\n\nconst eventLogger = logger.child('@spfn/core:event');\n\n/**\n * Log handler error with consistent format\n */\nfunction logHandlerError(eventName: string, error: unknown): void\n{\n eventLogger.error(`Event handler error: ${eventName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Log job queue error with consistent format\n */\nfunction logJobQueueError(queueName: string, error: unknown): void\n{\n eventLogger.error(`Failed to send event to job queue: ${queueName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Create handler subscription manager\n */\nfunction createHandlerManager<TPayload>(name: string)\n{\n const handlers: Set<EventHandler<TPayload>> = new Set();\n\n return {\n add: (handler: EventHandler<TPayload>): (() => void) =>\n {\n handlers.add(handler);\n eventLogger.debug(`Subscribed to event: ${name}`, { handlerCount: handlers.size });\n\n return () =>\n {\n handlers.delete(handler);\n eventLogger.debug(`Unsubscribed from event: ${name}`, { handlerCount: handlers.size });\n };\n },\n\n clear: (): void =>\n {\n handlers.clear();\n eventLogger.debug(`Unsubscribed all from event: ${name}`);\n },\n\n trigger: async (payload: TPayload): Promise<void> =>\n {\n const results = await Promise.allSettled(\n [...handlers].map((handler) => handler(payload))\n );\n\n for (const result of results)\n {\n if (result.status === 'rejected')\n {\n logHandlerError(name, result.reason);\n }\n }\n },\n };\n}\n\n/**\n * Create job queue manager\n */\nfunction createJobQueueManager(name: string)\n{\n const jobQueues: Map<string, JobQueueSender> = new Map();\n\n return {\n register: (queueName: string, sender: JobQueueSender): void =>\n {\n jobQueues.set(queueName, sender);\n eventLogger.debug(`Registered job queue for event: ${name}`, { queueName });\n },\n\n send: async (payload: unknown): Promise<void> =>\n {\n if (jobQueues.size === 0)\n {\n return;\n }\n\n const entries = [...jobQueues.entries()];\n const results = await Promise.allSettled(\n entries.map(([queueName, sender]) => sender(queueName, payload))\n );\n\n for (const [i, result] of results.entries())\n {\n if (result.status === 'rejected')\n {\n logJobQueueError(entries[i][0], result.reason);\n }\n }\n },\n\n get size(): number\n {\n return jobQueues.size;\n },\n };\n}\n\n/**\n * Internal: Create event implementation\n */\nfunction createEventImpl<TPayload>(\n name: string,\n schema?: TSchema\n): EventDef<TPayload>\n{\n const handlerManager = createHandlerManager<TPayload>(name);\n const jobQueueManager = createJobQueueManager(name);\n let cache: PubSubCache | undefined;\n let cacheSubscribed = false;\n\n const emit = async (payload?: TPayload): Promise<void> =>\n {\n eventLogger.debug(`Emitting event: ${name}`, {\n payload,\n hasCache: !!cache,\n jobQueueCount: jobQueueManager.size,\n });\n\n if (cache)\n {\n await cache.publish(name, payload);\n }\n else\n {\n await handlerManager.trigger(payload as TPayload);\n }\n\n await jobQueueManager.send(payload);\n eventLogger.debug(`Event emitted: ${name}`);\n };\n\n const useCache = async (newCache: PubSubCache): Promise<EventDef<TPayload>> =>\n {\n if (cacheSubscribed)\n {\n eventLogger.warn(`Cache already configured for event: ${name}`);\n return self;\n }\n\n cache = newCache;\n cacheSubscribed = true;\n\n await newCache.subscribe(name, async (message: unknown) =>\n {\n eventLogger.debug(`Received event from cache: ${name}`);\n await handlerManager.trigger(message as TPayload);\n });\n\n eventLogger.debug(`Cache subscription ready for event: ${name}`);\n return self;\n };\n\n const self: EventDef<TPayload> = {\n name,\n schema,\n subscribe: handlerManager.add,\n unsubscribeAll: handlerManager.clear,\n emit: emit as EventDef<TPayload>['emit'],\n useCache,\n _registerJobQueue: jobQueueManager.register,\n _payload: undefined as unknown as TPayload,\n };\n\n return self;\n}\n\n/**\n * Define an event without payload\n */\nexport function defineEvent(name: string): EventDef<void>;\n\n/**\n * Define an event with typed payload\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema: T\n): EventDef<Static<T>>;\n\n/**\n * Define an event for decoupled pub/sub\n *\n * @example\n * ```typescript\n * // Define event with payload\n * export const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe to event (in-memory)\n * const unsubscribe = userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit event\n * await userCreated.emit({ userId: '123' });\n *\n * // Unsubscribe when done\n * unsubscribe();\n *\n * // Multi-instance with cache\n * await userCreated.useCache(cache);\n * await userCreated.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema?: T\n): EventDef<Static<T>> | EventDef\n{\n if (schema)\n {\n return createEventImpl<Static<T>>(name, schema);\n }\n\n return createEventImpl<void>(name);\n}\n","/**\n * SSE Event Route Map\n *\n * Static route map for SSE token endpoint.\n * Merge into RPC proxy routeMap so `eventsToken` resolves to `POST /events/token`.\n *\n * @example\n * ```typescript\n * // app/api/rpc/[routeName]/route.ts\n * import { createRpcProxy } from '@spfn/core/nextjs/server';\n * import { eventRouteMap } from '@spfn/core/event';\n * import { authRouteMap } from '@spfn/auth';\n * import { routeMap } from '@/generated/route-map';\n *\n * export const { GET, POST } = createRpcProxy({\n * routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },\n * });\n * ```\n */\nexport const eventRouteMap = {\n eventsToken: { method: 'POST' as const, path: '/events/token' },\n};\n","/**\n * Event Router\n *\n * Type-safe event router for SSE subscription\n *\n * @example\n * ```typescript\n * import { defineEvent, defineEventRouter } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * const orderPlaced = defineEvent('order.placed', Type.Object({\n * orderId: Type.String(),\n * amount: Type.Number(),\n * }));\n *\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * export type EventRouter = typeof eventRouter;\n * ```\n */\n\nimport type { EventDef } from './types';\n\n/**\n * Event Router Definition\n */\nexport interface EventRouterDef<TEvents extends Record<string, EventDef<any>>>\n{\n /**\n * Event definitions\n */\n readonly events: TEvents;\n\n /**\n * Event names as array\n */\n readonly eventNames: (keyof TEvents)[];\n\n /**\n * Type inference helper - payload types by event name\n */\n readonly _types: {\n [K in keyof TEvents]: TEvents[K]['_payload'];\n };\n}\n\n/**\n * Infer event names from EventRouter\n */\nexport type InferEventNames<T> = T extends EventRouterDef<infer E>\n ? keyof E & string\n : never;\n\n/**\n * Infer payload type for specific event\n */\nexport type InferEventPayload<\n T extends EventRouterDef<any>,\n K extends InferEventNames<T>\n> = T['_types'][K];\n\n/**\n * Infer all event payloads map\n */\nexport type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];\n\n/**\n * Define an event router for SSE subscription\n *\n * @example\n * ```typescript\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * // Type inference\n * type Names = InferEventNames<typeof eventRouter>;\n * // 'userCreated' | 'orderPlaced'\n *\n * type Payload = InferEventPayload<typeof eventRouter, 'userCreated'>;\n * // { userId: string }\n * ```\n */\nexport function defineEventRouter<\n TEvents extends Record<string, EventDef<any>>\n>(events: TEvents): EventRouterDef<TEvents>\n{\n return {\n events,\n eventNames: Object.keys(events) as (keyof TEvents)[],\n _types: {} as EventRouterDef<TEvents>['_types'],\n };\n}"]}
|
|
1
|
+
{"version":3,"sources":["../../src/event/event.ts","../../src/event/sse/route-map.ts","../../src/event/router.ts","../../src/event/ws/handler.ts","../../src/event/ws/index.ts"],"names":["logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Event System\n *\n * Decoupled pub/sub event system with optional cache integration for multi-instance support\n *\n * @example\n * ```typescript\n * // Define event\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe (in-memory)\n * userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit\n * await userCreated.emit({ userId: '123' });\n *\n * // With cache for multi-instance\n * const event = defineEvent('user.created', schema);\n * await event.useCache(cache); // Must await before emitting\n * await event.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\n\nimport type { TSchema, Static } from '@sinclair/typebox';\nimport { logger } from '@spfn/core/logger';\nimport type { EventDef, EventHandler, JobQueueSender, PubSubCache } from './types';\n\nconst eventLogger = logger.child('@spfn/core:event');\n\n/**\n * Log handler error with consistent format\n */\nfunction logHandlerError(eventName: string, error: unknown): void\n{\n eventLogger.error(`Event handler error: ${eventName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Log job queue error with consistent format\n */\nfunction logJobQueueError(queueName: string, error: unknown): void\n{\n eventLogger.error(`Failed to send event to job queue: ${queueName}`, {\n error: error instanceof Error ? error.message : String(error),\n });\n}\n\n/**\n * Create handler subscription manager\n */\nfunction createHandlerManager<TPayload>(name: string)\n{\n const handlers: Set<EventHandler<TPayload>> = new Set();\n\n return {\n add: (handler: EventHandler<TPayload>): (() => void) =>\n {\n handlers.add(handler);\n eventLogger.debug(`Subscribed to event: ${name}`, { handlerCount: handlers.size });\n\n return () =>\n {\n handlers.delete(handler);\n eventLogger.debug(`Unsubscribed from event: ${name}`, { handlerCount: handlers.size });\n };\n },\n\n clear: (): void =>\n {\n handlers.clear();\n eventLogger.debug(`Unsubscribed all from event: ${name}`);\n },\n\n trigger: async (payload: TPayload): Promise<void> =>\n {\n const results = await Promise.allSettled(\n [...handlers].map((handler) => handler(payload))\n );\n\n for (const result of results)\n {\n if (result.status === 'rejected')\n {\n logHandlerError(name, result.reason);\n }\n }\n },\n };\n}\n\n/**\n * Create job queue manager\n */\nfunction createJobQueueManager(name: string)\n{\n const jobQueues: Map<string, JobQueueSender> = new Map();\n\n return {\n register: (queueName: string, sender: JobQueueSender): void =>\n {\n jobQueues.set(queueName, sender);\n eventLogger.debug(`Registered job queue for event: ${name}`, { queueName });\n },\n\n send: async (payload: unknown): Promise<void> =>\n {\n if (jobQueues.size === 0)\n {\n return;\n }\n\n const entries = [...jobQueues.entries()];\n const results = await Promise.allSettled(\n entries.map(([queueName, sender]) => sender(queueName, payload))\n );\n\n for (const [i, result] of results.entries())\n {\n if (result.status === 'rejected')\n {\n logJobQueueError(entries[i][0], result.reason);\n }\n }\n },\n\n get size(): number\n {\n return jobQueues.size;\n },\n };\n}\n\n/**\n * Internal: Create event implementation\n */\nfunction createEventImpl<TPayload>(\n name: string,\n schema?: TSchema\n): EventDef<TPayload>\n{\n const handlerManager = createHandlerManager<TPayload>(name);\n const jobQueueManager = createJobQueueManager(name);\n let cache: PubSubCache | undefined;\n let cacheSubscribed = false;\n\n const emit = async (payload?: TPayload): Promise<void> =>\n {\n eventLogger.debug(`Emitting event: ${name}`, {\n payload,\n hasCache: !!cache,\n jobQueueCount: jobQueueManager.size,\n });\n\n if (cache)\n {\n await cache.publish(name, payload);\n }\n else\n {\n await handlerManager.trigger(payload as TPayload);\n }\n\n await jobQueueManager.send(payload);\n eventLogger.debug(`Event emitted: ${name}`);\n };\n\n const useCache = async (newCache: PubSubCache): Promise<EventDef<TPayload>> =>\n {\n if (cacheSubscribed)\n {\n eventLogger.warn(`Cache already configured for event: ${name}`);\n return self;\n }\n\n cache = newCache;\n cacheSubscribed = true;\n\n await newCache.subscribe(name, async (message: unknown) =>\n {\n eventLogger.debug(`Received event from cache: ${name}`);\n await handlerManager.trigger(message as TPayload);\n });\n\n eventLogger.debug(`Cache subscription ready for event: ${name}`);\n return self;\n };\n\n const self: EventDef<TPayload> = {\n name,\n schema,\n subscribe: handlerManager.add,\n unsubscribeAll: handlerManager.clear,\n emit: emit as EventDef<TPayload>['emit'],\n useCache,\n _registerJobQueue: jobQueueManager.register,\n _payload: undefined as unknown as TPayload,\n };\n\n return self;\n}\n\n/**\n * Define an event without payload\n */\nexport function defineEvent(name: string): EventDef<void>;\n\n/**\n * Define an event with typed payload\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema: T\n): EventDef<Static<T>>;\n\n/**\n * Define an event for decoupled pub/sub\n *\n * @example\n * ```typescript\n * // Define event with payload\n * export const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * // Subscribe to event (in-memory)\n * const unsubscribe = userCreated.subscribe((payload) => {\n * console.log('User created:', payload.userId);\n * });\n *\n * // Emit event\n * await userCreated.emit({ userId: '123' });\n *\n * // Unsubscribe when done\n * unsubscribe();\n *\n * // Multi-instance with cache\n * await userCreated.useCache(cache);\n * await userCreated.emit({ userId: '123' }); // Broadcast to all instances\n * ```\n */\nexport function defineEvent<T extends TSchema>(\n name: string,\n schema?: T\n): EventDef<Static<T>> | EventDef\n{\n if (schema)\n {\n return createEventImpl<Static<T>>(name, schema);\n }\n\n return createEventImpl<void>(name);\n}\n","/**\n * SSE Event Route Map\n *\n * Static route map for SSE token endpoint.\n * Merge into RPC proxy routeMap so `eventsToken` resolves to `POST /events/token`.\n *\n * @example\n * ```typescript\n * // app/api/rpc/[routeName]/route.ts\n * import { createRpcProxy } from '@spfn/core/nextjs/server';\n * import { eventRouteMap } from '@spfn/core/event';\n * import { authRouteMap } from '@spfn/auth';\n * import { routeMap } from '@/generated/route-map';\n *\n * export const { GET, POST } = createRpcProxy({\n * routeMap: { ...routeMap, ...authRouteMap, ...eventRouteMap },\n * });\n * ```\n */\nexport const eventRouteMap = {\n eventsToken: { method: 'POST' as const, path: '/events/token' },\n};\n","/**\n * Event Router\n *\n * Type-safe event router for SSE subscription\n *\n * @example\n * ```typescript\n * import { defineEvent, defineEventRouter } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userCreated = defineEvent('user.created', Type.Object({\n * userId: Type.String(),\n * }));\n *\n * const orderPlaced = defineEvent('order.placed', Type.Object({\n * orderId: Type.String(),\n * amount: Type.Number(),\n * }));\n *\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * export type EventRouter = typeof eventRouter;\n * ```\n */\n\nimport type { EventDef } from './types';\n\n/**\n * Event Router Definition\n */\nexport interface EventRouterDef<TEvents extends Record<string, EventDef<any>>>\n{\n /**\n * Event definitions\n */\n readonly events: TEvents;\n\n /**\n * Event names as array\n */\n readonly eventNames: (keyof TEvents)[];\n\n /**\n * Type inference helper - payload types by event name\n */\n readonly _types: {\n [K in keyof TEvents]: TEvents[K]['_payload'];\n };\n}\n\n/**\n * Infer event names from EventRouter\n */\nexport type InferEventNames<T> = T extends EventRouterDef<infer E>\n ? keyof E & string\n : never;\n\n/**\n * Infer payload type for specific event\n */\nexport type InferEventPayload<\n T extends EventRouterDef<any>,\n K extends InferEventNames<T>\n> = T['_types'][K];\n\n/**\n * Infer all event payloads map\n */\nexport type InferEventPayloads<T extends EventRouterDef<any>> = T['_types'];\n\n/**\n * Define an event router for SSE subscription\n *\n * @example\n * ```typescript\n * export const eventRouter = defineEventRouter({\n * userCreated,\n * orderPlaced,\n * });\n *\n * // Type inference\n * type Names = InferEventNames<typeof eventRouter>;\n * // 'userCreated' | 'orderPlaced'\n *\n * type Payload = InferEventPayload<typeof eventRouter, 'userCreated'>;\n * // { userId: string }\n * ```\n */\nexport function defineEventRouter<\n TEvents extends Record<string, EventDef<any>>\n>(events: TEvents): EventRouterDef<TEvents>\n{\n return {\n events,\n eventNames: Object.keys(events) as (keyof TEvents)[],\n _types: {} as EventRouterDef<TEvents>['_types'],\n };\n}","/**\n * WebSocket Handler\n *\n * Attaches a WebSocket server to an existing Node.js http.Server.\n * Handles authentication, event subscription, and client message routing.\n */\n\nimport type { Server } from 'node:http';\nimport type { EventDef } from '../types';\nimport type {\n WSRouterDef,\n WSHandlerConfig,\n WSHandlerAuthConfig,\n WSMessageHandlers,\n WSRawConnection,\n} from './types';\nimport type { SSETokenManager } from '../sse/token-manager';\nimport { logger } from '@spfn/core/logger';\n\nconst wsLogger = logger.child('@spfn/core:ws');\n\n// ============================================================================\n// Public API\n// ============================================================================\n\n/**\n * Attach a WebSocket server to a Node.js http.Server\n *\n * @returns cleanup function that closes the WebSocket server\n */\nexport async function attachWSHandler<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers\n>(\n server: Server,\n router: WSRouterDef<TEvents, TMessages>,\n config: WSHandlerConfig & { path?: string } = {},\n tokenManager?: SSETokenManager\n): Promise<() => Promise<void>>\n{\n const WebSocketServer = await loadWSServer();\n\n const {\n pingInterval = 30000,\n path = '/ws',\n auth: authConfig,\n } = config;\n\n if (authConfig?.enabled && !tokenManager)\n {\n throw new Error(\n 'WebSocket auth.enabled=true requires a tokenManager. ' +\n 'Pass tokenManager or use .websockets(router, { auth: { enabled: true } }) via startServer.'\n );\n }\n\n const wss = new WebSocketServer({ server, path });\n\n // Track live connections for graceful shutdown\n const clients = new Set<any>();\n\n wss.on('connection', (ws: any, req: any) =>\n {\n clients.add(ws);\n ws.on('close', () => clients.delete(ws));\n handleConnection(ws, req, router, authConfig, tokenManager, pingInterval)\n .catch((err: Error) =>\n {\n wsLogger.error('WebSocket connection handler error', err);\n if (ws.readyState === 1) ws.close(1011, 'Internal server error');\n });\n });\n\n wss.on('error', (err: Error) =>\n {\n wsLogger.error('WebSocket server error', err);\n });\n\n wsLogger.info(`✓ WebSocket endpoint registered at ${path}`, {\n events: router.eventNames,\n auth: !!authConfig?.enabled,\n });\n\n return () => new Promise<void>((resolve, reject) =>\n {\n // Close all existing connections with 1001 Going Away\n for (const client of clients)\n {\n client.close(1001, 'Server shutting down');\n }\n clients.clear();\n\n wss.close((err?: Error) =>\n {\n if (err) reject(err);\n else resolve();\n });\n });\n}\n\n// ============================================================================\n// Connection Handler\n// ============================================================================\n\nasync function handleConnection(\n ws: any,\n req: any,\n router: WSRouterDef<any, any>,\n authConfig: WSHandlerAuthConfig | undefined,\n tokenManager: SSETokenManager | undefined,\n pingInterval: number\n): Promise<void>\n{\n // Register close handler before any await — ensures we never miss the event even during auth\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n let connectionUnsubscribes: (() => void)[] = [];\n let subscribedEvents: string[] = [];\n ws.on('close', () =>\n {\n clearInterval(pingTimer);\n connectionUnsubscribes.forEach(fn => fn());\n if (subscribedEvents.length > 0)\n wsLogger.info('WebSocket connection closed', { events: subscribedEvents });\n });\n\n const url = parseURL(req);\n if (!url)\n {\n ws.close(1002, 'Invalid request URL');\n return;\n }\n\n // ── 1. Authenticate ──\n const subject = await resolveSubject(url, authConfig?.enabled ? tokenManager : undefined);\n if (subject === false)\n {\n ws.close(4001, 'Missing token');\n return;\n }\n if (subject === null)\n {\n ws.close(4001, 'Invalid or expired token');\n return;\n }\n\n // ── 2. Resolve subscribed events ──\n const requestedEvents = parseRequestedEvents(url, router.eventNames as string[]);\n if (requestedEvents.length === 0)\n {\n ws.close(4000, 'No valid event names specified');\n return;\n }\n\n // ── 3. Authorize ──\n const allowedEvents = await resolveAllowedEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n ws.close(4003, 'Not authorized for any requested events');\n return;\n }\n\n subscribedEvents = allowedEvents;\n wsLogger.info('WebSocket connection established', {\n events: allowedEvents,\n subject: subject ?? undefined,\n });\n\n // ── 4. Build connection wrapper ──\n const connection = createConnection(ws);\n\n // ── 5. Subscribe to server-push events ──\n connectionUnsubscribes = subscribeEvents(ws, router, allowedEvents, subject, authConfig);\n\n // If socket closed during auth awaits, clean up and bail\n if (ws.readyState !== 1)\n {\n connectionUnsubscribes.forEach(fn => fn());\n connectionUnsubscribes = [];\n return;\n }\n\n // ── 6. Handle incoming messages ──\n ws.on('message', (data: Buffer | string) =>\n {\n onClientMessage(data, router, connection, subject)\n .catch((err: Error) => wsLogger.error('Unhandled message error', err));\n });\n\n // ── 7. Keep-alive ping ──\n if (pingInterval > 0)\n {\n pingTimer = setInterval(() =>\n {\n if (ws.readyState === 1) ws.ping();\n }, pingInterval);\n }\n\n // ── 9. Send connected ack ──\n connection.send('__connected', {\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n });\n}\n\n// ============================================================================\n// Helpers\n// ============================================================================\n\nfunction parseURL(req: any): URL | null\n{\n try\n {\n return new URL(req.url ?? '/', 'ws://localhost');\n }\n catch\n {\n return null;\n }\n}\n\n/**\n * Resolve subject from token\n * - undefined: no auth required\n * - false: token param missing (when required)\n * - null: token invalid/expired\n * - string: authenticated subject\n */\nasync function resolveSubject(\n url: URL,\n tokenManager?: SSETokenManager\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = url.searchParams.get('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\nfunction parseRequestedEvents(url: URL, validEventNames: string[]): string[]\n{\n const eventsParam = url.searchParams.get('events');\n if (!eventsParam)\n {\n return [];\n }\n\n return eventsParam\n .split(',')\n .map(e => e.trim())\n .filter(e => validEventNames.includes(e));\n}\n\nasync function resolveAllowedEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: WSHandlerAuthConfig\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n return allowed.length === 0 ? null : allowed;\n}\n\nfunction createConnection(ws: any): WSRawConnection\n{\n return {\n send: (type, payload) =>\n {\n if (ws.readyState !== 1) return;\n ws.send(JSON.stringify({ type, data: payload }));\n },\n close: (code, reason) => ws.close(code, reason),\n };\n}\n\nfunction subscribeEvents(\n ws: any,\n router: WSRouterDef<any, any>,\n allowedEvents: string[],\n subject: string | undefined,\n authConfig?: WSHandlerAuthConfig\n): (() => void)[]\n{\n const unsubscribes: (() => void)[] = [];\n\n for (const eventName of allowedEvents)\n {\n const eventDef = router.events[eventName];\n if (!eventDef) continue;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (ws.readyState !== 1) return;\n\n if (subject && authConfig?.filter?.[eventName])\n {\n if (!authConfig.filter[eventName](subject, payload)) return;\n }\n\n try\n {\n ws.send(JSON.stringify({ type: eventName, data: payload }));\n }\n catch\n {\n // Socket closed between readyState check and send — ignore\n }\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return unsubscribes;\n}\n\nasync function onClientMessage(\n data: Buffer | string,\n router: WSRouterDef<any, any>,\n connection: WSRawConnection,\n subject: string | undefined\n): Promise<void>\n{\n let message: { type?: string; data?: unknown };\n\n try\n {\n message = JSON.parse(data.toString());\n }\n catch\n {\n return;\n }\n\n const { type, data: payload } = message;\n if (!type) return;\n\n const handler = router.messages[type];\n if (!handler) return;\n\n try\n {\n await handler({ payload, subject, ws: connection });\n }\n catch (err)\n {\n wsLogger.error(`WebSocket message handler error: ${type}`, err as Error);\n }\n}\n\n// ============================================================================\n// Dynamic import for optional 'ws' dependency\n// ============================================================================\n\nasync function loadWSServer(): Promise<any>\n{\n try\n {\n // ws is a CJS package: module.exports = WebSocket, WebSocket.WebSocketServer is set on it.\n // ESM dynamic import wraps CJS default export under .default\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const mod = await import('ws') as any;\n const WS = mod.default ?? mod;\n const WSS = WS.WebSocketServer ?? WS.Server;\n\n if (typeof WSS !== 'function')\n {\n throw new Error(\n 'WebSocketServer not found in ws module. ' +\n 'Ensure ws@^8 is installed: pnpm add ws'\n );\n }\n\n return WSS;\n }\n catch (err)\n {\n if (err instanceof Error && err.message.includes('WebSocketServer not found'))\n {\n throw err;\n }\n throw new Error(\n '@spfn/core WebSocket support requires the \"ws\" package.\\n' +\n 'Install it with: pnpm add ws'\n );\n }\n}\n","/**\n * WebSocket Module\n *\n * Type-safe WebSocket server with event-based pub/sub and bidirectional messaging.\n *\n * @example Server setup\n * ```typescript\n * // src/server/ws.ts\n * import { defineWSRouter } from '@spfn/core/event/ws';\n * import { defineEvent } from '@spfn/core/event';\n * import { Type } from '@sinclair/typebox';\n *\n * const userUpdated = defineEvent('user.updated', Type.Object({ userId: Type.String() }));\n * const notification = defineEvent('notification', Type.Object({ message: Type.String() }));\n *\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * },\n * });\n *\n * export type WSRouter = typeof wsRouter;\n *\n * // server.config.ts\n * defineServerConfig()\n * .websockets(wsRouter)\n * .build();\n * ```\n *\n * @example Client usage\n * ```typescript\n * import { createWSClient } from '@spfn/core/event/ws/client';\n * import type { WSRouter } from '@/server/ws';\n *\n * const client = createWSClient<WSRouter>();\n *\n * client.subscribe({\n * events: ['userUpdated', 'notification'],\n * handlers: {\n * userUpdated: ({ userId }) => console.log(userId),\n * notification: ({ message }) => console.log(message),\n * },\n * });\n *\n * client.send('ping', {});\n * ```\n */\n\nimport type { EventDef } from '../types';\nimport type { WSRouterDef, WSMessageHandlers } from './types';\n\nexport { attachWSHandler } from './handler';\nexport type {\n WSRouterDef,\n WSHandlerConfig,\n WSAuthConfig,\n WSHandlerAuthConfig,\n WSMessageContext,\n WSMessageHandlerFn,\n WSMessageHandlers,\n WSRawConnection,\n WSClientConfig,\n WSConnectionState,\n WSEventHandlers,\n WSSubscribeOptions,\n WSUnsubscribe,\n} from './types';\n\n/**\n * Define a WebSocket router\n *\n * Combines server→client event push with client→server message handlers.\n *\n * @example\n * ```typescript\n * export const wsRouter = defineWSRouter({\n * events: { userUpdated, notification },\n * messages: {\n * ping: ({ ws }) => ws.send('pong', {}),\n * 'chat.send': ({ payload, subject }) => handleChat(payload, subject),\n * },\n * });\n * ```\n */\nexport function defineWSRouter<\n TEvents extends Record<string, EventDef<any>>,\n TMessages extends WSMessageHandlers = WSMessageHandlers\n>(def: {\n events: TEvents;\n messages?: TMessages;\n}): WSRouterDef<TEvents, TMessages>\n{\n return {\n events: def.events,\n eventNames: Object.keys(def.events) as (keyof TEvents)[],\n messages: (def.messages ?? {}) as TMessages,\n _types: {} as WSRouterDef<TEvents, TMessages>['_types'],\n };\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { E as EventRouterDef, I as InferEventNames } from '../../
|
|
2
|
-
import {
|
|
1
|
+
import { E as EventRouterDef, I as InferEventNames } from '../../token-manager-CyG7la3p.js';
|
|
2
|
+
import { g as SSESubscribeOptions, i as SSEUnsubscribe, h as SSEConnectionState, d as SSEClientConfig } from '../../types-C1jMLGwK.js';
|
|
3
3
|
import '@sinclair/typebox';
|
|
4
4
|
import 'hono';
|
|
5
5
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import { E as EventRouterDef } from '../../
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { E as EventRouterDef, S as SSETokenManager } from '../../token-manager-CyG7la3p.js';
|
|
3
|
+
export { C as CacheTokenStore, g as SSEToken, i as SSETokenManagerConfig, h as SSETokenStore } from '../../token-manager-CyG7la3p.js';
|
|
4
|
+
import { S as SSEHandlerConfig } from '../../types-C1jMLGwK.js';
|
|
5
|
+
export { a as SSEAuthConfig, d as SSEClientConfig, h as SSEConnectionState, e as SSEEventHandler, f as SSEEventHandlers, c as SSEHandlerAuthConfig, b as SSEMessage, g as SSESubscribeOptions, i as SSEUnsubscribe } from '../../types-C1jMLGwK.js';
|
|
5
6
|
import '@sinclair/typebox';
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SSE Handler for Hono\n *\n * Creates SSE stream endpoint for event subscription\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { createSSEHandler } from '@spfn/core/event/sse';\n * import { eventRouter } from './events';\n *\n * const app = new Hono();\n *\n * // GET /events/stream?events=userCreated,orderPlaced\n * app.get('/events/stream', createSSEHandler(eventRouter));\n * ```\n */\n\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n// Extend Hono context with SSE subject\ndeclare module 'hono'\n{\n interface ContextVariableMap\n {\n sseSubject?: string;\n }\n}\n\n/**\n * Create SSE handler for Hono\n *\n * Query parameters:\n * - events: Comma-separated list of event names to subscribe\n * - token: One-time auth token (when auth is enabled)\n *\n * @example\n * ```typescript\n * app.get('/events/stream', createSSEHandler(eventRouter, {\n * pingInterval: 30000,\n * }));\n * ```\n */\nexport function createSSEHandler<TRouter extends EventRouterDef<any>>(\n router: TRouter,\n config: SSEHandlerConfig = {},\n tokenManager?: SSETokenManager\n)\n{\n const {\n pingInterval = 30000,\n auth: authConfig,\n } = config;\n\n return async (c: Context) =>\n {\n // ── 1. Token Authentication ──\n const subject = await authenticateToken(c, tokenManager);\n if (subject === false)\n {\n return c.json({ error: 'Missing token parameter' }, 401);\n }\n if (subject === null)\n {\n return c.json({ error: 'Invalid or expired token' }, 401);\n }\n if (subject)\n {\n c.set('sseSubject', subject);\n }\n\n // ── 2. Parse events from query parameter ──\n const requestedEvents = parseRequestedEvents(c);\n if (!requestedEvents)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n // ── 3. Validate event names ──\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requestedEvents.filter(e => !validEventNames.includes(e));\n\n if (invalidEvents.length > 0)\n {\n return c.json({\n error: 'Invalid event names',\n invalidEvents,\n validEvents: validEventNames,\n }, 400);\n }\n\n // ── 4. Subscription Authorization ──\n const allowedEvents = await authorizeEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n sseLogger.debug('SSE connection requested', {\n events: allowedEvents,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n // ── 5. SSE Stream ──\n c.header('X-Accel-Buffering', 'no');\n\n return streamSSE(c, async (stream) =>\n {\n const unsubscribes: (() => void)[] = [];\n let messageId = 0;\n let connectionDead = false;\n let pingTimer: ReturnType<typeof setInterval>;\n\n const cleanup = () =>\n {\n if (connectionDead) return;\n connectionDead = true;\n clearInterval(pingTimer);\n unsubscribes.forEach(fn => fn());\n sseLogger.info('SSE dead connection cleaned up', {\n events: allowedEvents,\n });\n };\n\n for (const eventName of allowedEvents as InferEventNames<TRouter>[])\n {\n const eventDef = router.events[eventName];\n\n if (!eventDef)\n {\n continue;\n }\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (connectionDead) return;\n\n // ── Payload Filtering ──\n if (subject && authConfig?.filter?.[eventName as string])\n {\n if (!authConfig.filter[eventName as string](subject, payload))\n {\n return;\n }\n }\n\n messageId++;\n\n const message = {\n event: eventName,\n data: payload,\n };\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n stream.writeSSE({\n id: String(messageId),\n event: eventName as string,\n data: JSON.stringify(message),\n }).catch((err) =>\n {\n sseLogger.warn('SSE write failed', {\n event: eventName,\n messageId,\n error: err.message,\n });\n cleanup();\n });\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n sseLogger.info('SSE connection established', {\n events: allowedEvents,\n subscriptionCount: unsubscribes.length,\n });\n\n // Send initial connection message\n await stream.writeSSE({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n }),\n });\n\n // Keep-alive ping\n pingTimer = setInterval(() =>\n {\n if (connectionDead) return;\n\n stream.writeSSE({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n }).catch((err) =>\n {\n sseLogger.warn('SSE ping failed', {\n error: err.message,\n });\n cleanup();\n });\n }, pingInterval);\n\n // Wait for client disconnect using abort signal\n const abortSignal = c.req.raw.signal;\n\n while (!abortSignal.aborted && !connectionDead)\n {\n await stream.sleep(pingInterval);\n }\n\n // Cleanup (normal disconnect path)\n cleanup();\n }, async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Authenticate via one-time token\n * @returns subject string if authenticated, undefined if no auth required,\n * false if token missing, null if token invalid/expired\n */\nasync function authenticateToken(\n c: Context,\n tokenManager?: SSETokenManager\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = c.req.query('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\n/**\n * Parse requested events from query parameter\n */\nfunction parseRequestedEvents(c: Context): string[] | null\n{\n const eventsParam = c.req.query('events');\n if (!eventsParam)\n {\n return null;\n }\n\n return eventsParam.split(',').map(e => e.trim());\n}\n\n/**\n * Authorize event subscription via auth hook\n * @returns allowed events array, or null if rejected\n */\nasync function authorizeEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: SSEHandlerAuthConfig\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n if (allowed.length === 0)\n {\n return null;\n }\n\n return allowed;\n}\n","/**\n * SSE Token Manager\n *\n * Auth-agnostic token issuance and verification for SSE connections.\n * Issues one-time-use tokens with TTL for Token Exchange pattern.\n *\n * @example\n * ```typescript\n * const manager = new SSETokenManager({ ttl: 30000 });\n *\n * // Issue token for authenticated user\n * const token = await manager.issue('user-123');\n *\n * // Verify and consume token (one-time use)\n * const subject = await manager.verify(token); // 'user-123'\n * const again = await manager.verify(token); // null (already consumed)\n *\n * // Cleanup on shutdown\n * manager.destroy();\n * ```\n */\n\nimport { randomBytes } from 'crypto';\n\n/**\n * Minimal cache client interface (compatible with ioredis Redis | Cluster)\n */\ntype CacheClient = {\n set(key: string, value: string, ...args: any[]): Promise<any>;\n getdel?(key: string): Promise<string | null>;\n get(key: string): Promise<string | null>;\n del(key: string | string[]): Promise<number>;\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Stored SSE token data\n */\nexport interface SSEToken\n{\n token: string;\n subject: string;\n expiresAt: number;\n}\n\n/**\n * Token storage interface\n *\n * Implement this for custom storage backends (e.g., Redis for multi-instance).\n */\nexport interface SSETokenStore\n{\n /** Store a token */\n set(token: string, data: SSEToken): Promise<void>;\n\n /** Get and delete a token (one-time use) */\n consume(token: string): Promise<SSEToken | null>;\n\n /** Remove expired tokens */\n cleanup(): Promise<void>;\n}\n\n/**\n * SSETokenManager configuration\n */\nexport interface SSETokenManagerConfig\n{\n /**\n * Token time-to-live in milliseconds\n * @default 30000\n */\n ttl?: number;\n\n /**\n * Custom token store (default: in-memory Map)\n */\n store?: SSETokenStore;\n\n /**\n * Cleanup interval in milliseconds\n * @default 60000\n */\n cleanupInterval?: number;\n}\n\n// ============================================================================\n// InMemoryTokenStore\n// ============================================================================\n\nclass InMemoryTokenStore implements SSETokenStore\n{\n private tokens = new Map<string, SSEToken>();\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n this.tokens.set(token, data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const data = this.tokens.get(token);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(token);\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [token, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(token);\n }\n }\n }\n}\n\n// ============================================================================\n// CacheTokenStore (Redis/Valkey)\n// ============================================================================\n\n/**\n * Redis/Valkey-backed token store for multi-instance deployments.\n *\n * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.\n * No cleanup needed — Redis handles expiration automatically.\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * const store = new CacheTokenStore(cache);\n * const manager = new SSETokenManager({ store });\n * }\n * ```\n */\nexport class CacheTokenStore implements SSETokenStore\n{\n private prefix = 'sse:token:';\n\n constructor(private cache: CacheClient) {}\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n const ttlSeconds = Math.max(1, Math.ceil((data.expiresAt - Date.now()) / 1000));\n await this.cache.set(\n this.prefix + token,\n JSON.stringify(data),\n 'EX',\n ttlSeconds,\n );\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = this.prefix + token;\n\n // GETDEL (Redis 6.2+) for atomic consume, fallback to GET+DEL\n let raw: string | null = null;\n\n if (this.cache.getdel)\n {\n raw = await this.cache.getdel(key);\n }\n else\n {\n raw = await this.cache.get(key);\n if (raw)\n {\n await this.cache.del(key);\n }\n }\n\n if (!raw)\n {\n return null;\n }\n\n return JSON.parse(raw) as SSEToken;\n }\n\n async cleanup(): Promise<void>\n {\n // No-op: Redis TTL handles expiration automatically\n }\n}\n\n// ============================================================================\n// SSETokenManager\n// ============================================================================\n\nexport class SSETokenManager\n{\n private store: SSETokenStore;\n private ttl: number;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config?: SSETokenManagerConfig)\n {\n this.ttl = config?.ttl ?? 30000;\n this.store = config?.store ?? new InMemoryTokenStore();\n\n const cleanupInterval = config?.cleanupInterval ?? 60000;\n this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);\n this.cleanupTimer.unref();\n }\n\n /**\n * Issue a new one-time-use token for the given subject\n */\n async issue(subject: string): Promise<string>\n {\n const token = randomBytes(32).toString('hex');\n\n await this.store.set(token, {\n token,\n subject,\n expiresAt: Date.now() + this.ttl,\n });\n\n return token;\n }\n\n /**\n * Verify and consume a token\n * @returns subject string if valid, null if invalid/expired/already consumed\n */\n async verify(token: string): Promise<string | null>\n {\n const data = await this.store.consume(token);\n\n if (!data || data.expiresAt <= Date.now())\n {\n return null;\n }\n\n return data.subject;\n }\n\n /**\n * Cleanup timer and resources\n */\n destroy(): void\n {\n if (this.cleanupTimer)\n {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SSE Handler for Hono\n *\n * Creates SSE stream endpoint for event subscription\n *\n * @example\n * ```typescript\n * import { Hono } from 'hono';\n * import { createSSEHandler } from '@spfn/core/event/sse';\n * import { eventRouter } from './events';\n *\n * const app = new Hono();\n *\n * // GET /events/stream?events=userCreated,orderPlaced\n * app.get('/events/stream', createSSEHandler(eventRouter));\n * ```\n */\n\nimport type { Context } from 'hono';\nimport { streamSSE } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n// Extend Hono context with SSE subject\ndeclare module 'hono'\n{\n interface ContextVariableMap\n {\n sseSubject?: string;\n }\n}\n\n/**\n * Create SSE handler for Hono\n *\n * Query parameters:\n * - events: Comma-separated list of event names to subscribe\n * - token: One-time auth token (when auth is enabled)\n *\n * @example\n * ```typescript\n * app.get('/events/stream', createSSEHandler(eventRouter, {\n * pingInterval: 30000,\n * }));\n * ```\n */\nexport function createSSEHandler<TRouter extends EventRouterDef<any>>(\n router: TRouter,\n config: SSEHandlerConfig = {},\n tokenManager?: SSETokenManager\n)\n{\n const {\n pingInterval = 30000,\n auth: authConfig,\n } = config;\n\n return async (c: Context) =>\n {\n // ── 1. Token Authentication ──\n const subject = await authenticateToken(c, tokenManager);\n if (subject === false)\n {\n return c.json({ error: 'Missing token parameter' }, 401);\n }\n if (subject === null)\n {\n return c.json({ error: 'Invalid or expired token' }, 401);\n }\n if (subject)\n {\n c.set('sseSubject', subject);\n }\n\n // ── 2. Parse events from query parameter ──\n const requestedEvents = parseRequestedEvents(c);\n if (!requestedEvents)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n // ── 3. Validate event names ──\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requestedEvents.filter(e => !validEventNames.includes(e));\n\n if (invalidEvents.length > 0)\n {\n return c.json({\n error: 'Invalid event names',\n invalidEvents,\n validEvents: validEventNames,\n }, 400);\n }\n\n // ── 4. Subscription Authorization ──\n const allowedEvents = await authorizeEvents(subject, requestedEvents, authConfig);\n if (allowedEvents === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n sseLogger.debug('SSE connection requested', {\n events: allowedEvents,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n // ── 5. SSE Stream ──\n c.header('X-Accel-Buffering', 'no');\n\n return streamSSE(c, async (stream) =>\n {\n const unsubscribes: (() => void)[] = [];\n let messageId = 0;\n let connectionDead = false;\n let pingTimer: ReturnType<typeof setInterval>;\n\n const cleanup = () =>\n {\n if (connectionDead) return;\n connectionDead = true;\n clearInterval(pingTimer);\n unsubscribes.forEach(fn => fn());\n sseLogger.info('SSE dead connection cleaned up', {\n events: allowedEvents,\n });\n };\n\n for (const eventName of allowedEvents as InferEventNames<TRouter>[])\n {\n const eventDef = router.events[eventName];\n\n if (!eventDef)\n {\n continue;\n }\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (connectionDead) return;\n\n // ── Payload Filtering ──\n if (subject && authConfig?.filter?.[eventName as string])\n {\n if (!authConfig.filter[eventName as string](subject, payload))\n {\n return;\n }\n }\n\n messageId++;\n\n const message = {\n event: eventName,\n data: payload,\n };\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n stream.writeSSE({\n id: String(messageId),\n event: eventName as string,\n data: JSON.stringify(message),\n }).catch((err) =>\n {\n sseLogger.warn('SSE write failed', {\n event: eventName,\n messageId,\n error: err.message,\n });\n cleanup();\n });\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n sseLogger.info('SSE connection established', {\n events: allowedEvents,\n subscriptionCount: unsubscribes.length,\n });\n\n // Send initial connection message\n await stream.writeSSE({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: allowedEvents,\n timestamp: Date.now(),\n }),\n });\n\n // Keep-alive ping\n pingTimer = setInterval(() =>\n {\n if (connectionDead) return;\n\n stream.writeSSE({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n }).catch((err) =>\n {\n sseLogger.warn('SSE ping failed', {\n error: err.message,\n });\n cleanup();\n });\n }, pingInterval);\n\n // Wait for client disconnect using abort signal\n const abortSignal = c.req.raw.signal;\n\n while (!abortSignal.aborted && !connectionDead)\n {\n await stream.sleep(pingInterval);\n }\n\n // Cleanup (normal disconnect path)\n cleanup();\n }, async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n}\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\n/**\n * Authenticate via one-time token\n * @returns subject string if authenticated, undefined if no auth required,\n * false if token missing, null if token invalid/expired\n */\nasync function authenticateToken(\n c: Context,\n tokenManager?: SSETokenManager\n): Promise<string | undefined | false | null>\n{\n if (!tokenManager)\n {\n return undefined;\n }\n\n const token = c.req.query('token');\n if (!token)\n {\n return false;\n }\n\n return await tokenManager.verify(token);\n}\n\n/**\n * Parse requested events from query parameter\n */\nfunction parseRequestedEvents(c: Context): string[] | null\n{\n const eventsParam = c.req.query('events');\n if (!eventsParam)\n {\n return null;\n }\n\n return eventsParam.split(',').map(e => e.trim());\n}\n\n/**\n * Authorize event subscription via auth hook\n * @returns allowed events array, or null if rejected\n */\nasync function authorizeEvents(\n subject: string | undefined,\n requestedEvents: string[],\n authConfig?: SSEHandlerAuthConfig\n): Promise<string[] | null>\n{\n if (!subject || !authConfig?.authorize)\n {\n return requestedEvents;\n }\n\n const allowed = await authConfig.authorize(subject, requestedEvents);\n\n if (allowed.length === 0)\n {\n return null;\n }\n\n return allowed;\n}\n","/**\n * SSE Token Manager\n *\n * Auth-agnostic token issuance and verification for SSE connections.\n * Issues one-time-use tokens with TTL for Token Exchange pattern.\n *\n * @example\n * ```typescript\n * const manager = new SSETokenManager({ ttl: 30000 });\n *\n * // Issue token for authenticated user\n * const token = await manager.issue('user-123');\n *\n * // Verify and consume token (one-time use)\n * const subject = await manager.verify(token); // 'user-123'\n * const again = await manager.verify(token); // null (already consumed)\n *\n * // Cleanup on shutdown\n * manager.destroy();\n * ```\n */\n\nimport { randomBytes } from 'crypto';\n\n/**\n * Minimal cache client interface (compatible with ioredis Redis | Cluster)\n */\ntype CacheClient = {\n set(key: string, value: string, ...args: any[]): Promise<any>;\n getdel?(key: string): Promise<string | null>;\n get(key: string): Promise<string | null>;\n del(...keys: string[]): Promise<number>;\n};\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Stored SSE token data\n */\nexport interface SSEToken\n{\n token: string;\n subject: string;\n expiresAt: number;\n}\n\n/**\n * Token storage interface\n *\n * Implement this for custom storage backends (e.g., Redis for multi-instance).\n */\nexport interface SSETokenStore\n{\n /** Store a token */\n set(token: string, data: SSEToken): Promise<void>;\n\n /** Get and delete a token (one-time use) */\n consume(token: string): Promise<SSEToken | null>;\n\n /** Remove expired tokens */\n cleanup(): Promise<void>;\n}\n\n/**\n * SSETokenManager configuration\n */\nexport interface SSETokenManagerConfig\n{\n /**\n * Token time-to-live in milliseconds\n * @default 30000\n */\n ttl?: number;\n\n /**\n * Custom token store (default: in-memory Map)\n */\n store?: SSETokenStore;\n\n /**\n * Cleanup interval in milliseconds\n * @default 60000\n */\n cleanupInterval?: number;\n}\n\n// ============================================================================\n// InMemoryTokenStore\n// ============================================================================\n\nclass InMemoryTokenStore implements SSETokenStore\n{\n private tokens = new Map<string, SSEToken>();\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n this.tokens.set(token, data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const data = this.tokens.get(token);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(token);\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [token, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(token);\n }\n }\n }\n}\n\n// ============================================================================\n// CacheTokenStore (Redis/Valkey)\n// ============================================================================\n\n/**\n * Redis/Valkey-backed token store for multi-instance deployments.\n *\n * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.\n * No cleanup needed — Redis handles expiration automatically.\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * const store = new CacheTokenStore(cache);\n * const manager = new SSETokenManager({ store });\n * }\n * ```\n */\nexport class CacheTokenStore implements SSETokenStore\n{\n private prefix = 'sse:token:';\n\n constructor(private cache: CacheClient) {}\n\n async set(token: string, data: SSEToken): Promise<void>\n {\n const ttlSeconds = Math.max(1, Math.ceil((data.expiresAt - Date.now()) / 1000));\n await this.cache.set(\n this.prefix + token,\n JSON.stringify(data),\n 'EX',\n ttlSeconds,\n );\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = this.prefix + token;\n\n // GETDEL (Redis 6.2+) for atomic consume, fallback to GET+DEL\n let raw: string | null = null;\n\n if (this.cache.getdel)\n {\n raw = await this.cache.getdel(key);\n }\n else\n {\n raw = await this.cache.get(key);\n if (raw)\n {\n await this.cache.del(key);\n }\n }\n\n if (!raw)\n {\n return null;\n }\n\n return JSON.parse(raw) as SSEToken;\n }\n\n async cleanup(): Promise<void>\n {\n // No-op: Redis TTL handles expiration automatically\n }\n}\n\n// ============================================================================\n// SSETokenManager\n// ============================================================================\n\nexport class SSETokenManager\n{\n private store: SSETokenStore;\n private ttl: number;\n private cleanupTimer: ReturnType<typeof setInterval> | null = null;\n\n constructor(config?: SSETokenManagerConfig)\n {\n this.ttl = config?.ttl ?? 30000;\n this.store = config?.store ?? new InMemoryTokenStore();\n\n const cleanupInterval = config?.cleanupInterval ?? 60000;\n this.cleanupTimer = setInterval(() => void this.store.cleanup(), cleanupInterval);\n this.cleanupTimer.unref();\n }\n\n /**\n * Issue a new one-time-use token for the given subject\n */\n async issue(subject: string): Promise<string>\n {\n const token = randomBytes(32).toString('hex');\n\n await this.store.set(token, {\n token,\n subject,\n expiresAt: Date.now() + this.ttl,\n });\n\n return token;\n }\n\n /**\n * Verify and consume a token\n * @returns subject string if valid, null if invalid/expired/already consumed\n */\n async verify(token: string): Promise<string | null>\n {\n const data = await this.store.consume(token);\n\n if (!data || data.expiresAt <= Date.now())\n {\n return null;\n }\n\n return data.subject;\n }\n\n /**\n * Cleanup timer and resources\n */\n destroy(): void\n {\n if (this.cleanupTimer)\n {\n clearInterval(this.cleanupTimer);\n this.cleanupTimer = null;\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { W as WSRouterDef, k as WSSubscribeOptions, l as WSUnsubscribe, i as WSConnectionState, h as WSClientConfig } from '../../types-Cfj--lfr.js';
|
|
2
|
+
import 'hono';
|
|
3
|
+
import '../../token-manager-CyG7la3p.js';
|
|
4
|
+
import '@sinclair/typebox';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* WebSocket Client
|
|
8
|
+
*
|
|
9
|
+
* Type-safe browser WebSocket wrapper for event subscription and bidirectional messaging.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import { createWSClient } from '@spfn/core/event/ws/client';
|
|
14
|
+
* import type { WSRouter } from '@/server/ws';
|
|
15
|
+
*
|
|
16
|
+
* const client = createWSClient<WSRouter>();
|
|
17
|
+
*
|
|
18
|
+
* const unsubscribe = client.subscribe({
|
|
19
|
+
* events: ['userUpdated', 'notification'],
|
|
20
|
+
* handlers: {
|
|
21
|
+
* userUpdated: ({ userId }) => console.log(userId),
|
|
22
|
+
* notification: ({ message }) => console.log(message),
|
|
23
|
+
* },
|
|
24
|
+
* onOpen: () => console.log('connected'),
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Send message to server
|
|
28
|
+
* client.send('ping', {});
|
|
29
|
+
*
|
|
30
|
+
* // Cleanup
|
|
31
|
+
* unsubscribe();
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
interface WSClient<TRouter extends WSRouterDef<any, any>> {
|
|
36
|
+
/**
|
|
37
|
+
* Subscribe to server-push events
|
|
38
|
+
* Returns an unsubscribe function
|
|
39
|
+
*/
|
|
40
|
+
subscribe(options: WSSubscribeOptions<TRouter>): WSUnsubscribe;
|
|
41
|
+
/**
|
|
42
|
+
* Send a message to the server
|
|
43
|
+
*/
|
|
44
|
+
send<TType extends string>(type: TType, payload: unknown): void;
|
|
45
|
+
/**
|
|
46
|
+
* Get current connection state
|
|
47
|
+
*/
|
|
48
|
+
getState(): WSConnectionState;
|
|
49
|
+
/**
|
|
50
|
+
* Close the connection permanently
|
|
51
|
+
*/
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create a type-safe WebSocket client
|
|
56
|
+
*/
|
|
57
|
+
declare function createWSClient<TRouter extends WSRouterDef<any, any>>(config?: WSClientConfig): WSClient<TRouter>;
|
|
58
|
+
|
|
59
|
+
export { type WSClient, createWSClient };
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// src/event/ws/client.ts
|
|
2
|
+
function deriveWSHost() {
|
|
3
|
+
const apiUrl = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_SPFN_API_URL || "http://localhost:8790" : "http://localhost:8790";
|
|
4
|
+
return apiUrl.replace(/^https:\/\//, "wss://").replace(/^http:\/\//, "ws://");
|
|
5
|
+
}
|
|
6
|
+
var WS_DEFAULTS = {
|
|
7
|
+
get host() {
|
|
8
|
+
return deriveWSHost();
|
|
9
|
+
},
|
|
10
|
+
pathname: "/ws"
|
|
11
|
+
};
|
|
12
|
+
function createWSClient(config = {}) {
|
|
13
|
+
const {
|
|
14
|
+
host = WS_DEFAULTS.host,
|
|
15
|
+
pathname = WS_DEFAULTS.pathname,
|
|
16
|
+
reconnect = true,
|
|
17
|
+
reconnectDelay = 3e3,
|
|
18
|
+
maxReconnectAttempts = 0,
|
|
19
|
+
acquireToken
|
|
20
|
+
} = config;
|
|
21
|
+
const baseUrl = `${host}${pathname}`;
|
|
22
|
+
let socket = null;
|
|
23
|
+
let state = "closed";
|
|
24
|
+
let reconnectAttempts = 0;
|
|
25
|
+
let reconnectTimer = null;
|
|
26
|
+
let destroyed = false;
|
|
27
|
+
let intentionalReconnect = false;
|
|
28
|
+
let sentEvents = /* @__PURE__ */ new Set();
|
|
29
|
+
let connectedEvents = /* @__PURE__ */ new Set();
|
|
30
|
+
const subscriptions = /* @__PURE__ */ new Set();
|
|
31
|
+
function setState(next) {
|
|
32
|
+
state = next;
|
|
33
|
+
}
|
|
34
|
+
function buildURL(events, token) {
|
|
35
|
+
const params = new URLSearchParams();
|
|
36
|
+
params.set("events", events.join(","));
|
|
37
|
+
if (token) params.set("token", token);
|
|
38
|
+
return `${baseUrl}?${params.toString()}`;
|
|
39
|
+
}
|
|
40
|
+
function mergeEventNames() {
|
|
41
|
+
const names = /* @__PURE__ */ new Set();
|
|
42
|
+
for (const sub of subscriptions) {
|
|
43
|
+
if (sub.active) {
|
|
44
|
+
sub.options.events.forEach((e) => names.add(e));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return [...names];
|
|
48
|
+
}
|
|
49
|
+
function dispatch(type, payload) {
|
|
50
|
+
for (const sub of subscriptions) {
|
|
51
|
+
if (!sub.active) continue;
|
|
52
|
+
const handler = sub.options.handlers[type];
|
|
53
|
+
if (handler) handler(payload);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function onOpen() {
|
|
57
|
+
setState("open");
|
|
58
|
+
reconnectAttempts = 0;
|
|
59
|
+
connectedEvents = new Set(sentEvents);
|
|
60
|
+
const current = mergeEventNames();
|
|
61
|
+
const hasNewEvents = current.some((e) => !connectedEvents.has(e));
|
|
62
|
+
if (hasNewEvents) {
|
|
63
|
+
intentionalReconnect = true;
|
|
64
|
+
socket?.close();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
for (const sub of subscriptions) {
|
|
68
|
+
if (sub.active) sub.options.onOpen?.();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function onError(evt) {
|
|
72
|
+
setState("error");
|
|
73
|
+
for (const sub of subscriptions) {
|
|
74
|
+
if (sub.active) sub.options.onError?.(evt);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function onClose() {
|
|
78
|
+
socket = null;
|
|
79
|
+
connectedEvents = /* @__PURE__ */ new Set();
|
|
80
|
+
sentEvents = /* @__PURE__ */ new Set();
|
|
81
|
+
if (destroyed) {
|
|
82
|
+
setState("closed");
|
|
83
|
+
for (const sub of subscriptions) {
|
|
84
|
+
if (sub.active) {
|
|
85
|
+
sub.options.onClose?.();
|
|
86
|
+
sub.active = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
subscriptions.clear();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (intentionalReconnect) {
|
|
93
|
+
intentionalReconnect = false;
|
|
94
|
+
connect();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const hasActive = [...subscriptions].some((s) => s.active);
|
|
98
|
+
if (!reconnect || !hasActive) {
|
|
99
|
+
setState("closed");
|
|
100
|
+
if (!reconnect && hasActive) {
|
|
101
|
+
for (const sub of subscriptions) {
|
|
102
|
+
if (sub.active) {
|
|
103
|
+
sub.options.onClose?.();
|
|
104
|
+
sub.active = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
subscriptions.clear();
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (maxReconnectAttempts > 0 && reconnectAttempts >= maxReconnectAttempts) {
|
|
112
|
+
setState("closed");
|
|
113
|
+
for (const sub of subscriptions) {
|
|
114
|
+
if (sub.active) {
|
|
115
|
+
sub.options.onClose?.();
|
|
116
|
+
sub.active = false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
subscriptions.clear();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
setState("closed");
|
|
123
|
+
reconnectAttempts++;
|
|
124
|
+
for (const sub of subscriptions) {
|
|
125
|
+
if (sub.active) sub.options.onReconnect?.(reconnectAttempts);
|
|
126
|
+
}
|
|
127
|
+
reconnectTimer = setTimeout(() => connect(), reconnectDelay);
|
|
128
|
+
}
|
|
129
|
+
function onMessage(evt) {
|
|
130
|
+
let msg;
|
|
131
|
+
try {
|
|
132
|
+
msg = JSON.parse(evt.data);
|
|
133
|
+
} catch {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const { type, data } = msg;
|
|
137
|
+
if (!type || type === "__connected") return;
|
|
138
|
+
dispatch(type, data);
|
|
139
|
+
}
|
|
140
|
+
async function connect() {
|
|
141
|
+
if (destroyed) return;
|
|
142
|
+
if (state === "connecting") return;
|
|
143
|
+
const events = mergeEventNames();
|
|
144
|
+
if (events.length === 0) return;
|
|
145
|
+
sentEvents = new Set(events);
|
|
146
|
+
setState("connecting");
|
|
147
|
+
let token;
|
|
148
|
+
if (acquireToken) {
|
|
149
|
+
try {
|
|
150
|
+
token = await acquireToken();
|
|
151
|
+
} catch {
|
|
152
|
+
setState("error");
|
|
153
|
+
const errorEvent = new Event("error");
|
|
154
|
+
for (const sub of subscriptions) {
|
|
155
|
+
if (sub.active) sub.options.onError?.(errorEvent);
|
|
156
|
+
}
|
|
157
|
+
const permanentlyClose = destroyed || !reconnect || maxReconnectAttempts > 0 && reconnectAttempts >= maxReconnectAttempts;
|
|
158
|
+
if (permanentlyClose) {
|
|
159
|
+
setState("closed");
|
|
160
|
+
sentEvents = /* @__PURE__ */ new Set();
|
|
161
|
+
for (const sub of subscriptions) {
|
|
162
|
+
if (sub.active) {
|
|
163
|
+
sub.options.onClose?.();
|
|
164
|
+
sub.active = false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
subscriptions.clear();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
reconnectAttempts++;
|
|
171
|
+
for (const sub of subscriptions) {
|
|
172
|
+
if (sub.active) sub.options.onReconnect?.(reconnectAttempts);
|
|
173
|
+
}
|
|
174
|
+
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
175
|
+
reconnectTimer = setTimeout(() => connect(), reconnectDelay);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (destroyed) {
|
|
179
|
+
setState("closed");
|
|
180
|
+
sentEvents = /* @__PURE__ */ new Set();
|
|
181
|
+
for (const sub of subscriptions) {
|
|
182
|
+
if (sub.active) {
|
|
183
|
+
sub.options.onClose?.();
|
|
184
|
+
sub.active = false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
subscriptions.clear();
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const currentEvents = mergeEventNames();
|
|
191
|
+
if (currentEvents.length === 0) {
|
|
192
|
+
setState("closed");
|
|
193
|
+
sentEvents = /* @__PURE__ */ new Set();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (currentEvents.some((e) => !sentEvents.has(e)) || sentEvents.size !== currentEvents.length) {
|
|
197
|
+
sentEvents = new Set(currentEvents);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const url = buildURL([...sentEvents], token);
|
|
201
|
+
const thisSocket = new WebSocket(url);
|
|
202
|
+
socket = thisSocket;
|
|
203
|
+
socket.onopen = onOpen;
|
|
204
|
+
socket.onerror = onError;
|
|
205
|
+
socket.onclose = () => {
|
|
206
|
+
if (socket === thisSocket) onClose();
|
|
207
|
+
};
|
|
208
|
+
socket.onmessage = onMessage;
|
|
209
|
+
}
|
|
210
|
+
function subscribe(options) {
|
|
211
|
+
const sub = { options, active: true };
|
|
212
|
+
subscriptions.add(sub);
|
|
213
|
+
if (!destroyed && (state === "closed" || state === "error") && !socket) {
|
|
214
|
+
if (reconnectTimer) {
|
|
215
|
+
clearTimeout(reconnectTimer);
|
|
216
|
+
reconnectTimer = null;
|
|
217
|
+
}
|
|
218
|
+
connect();
|
|
219
|
+
} else if (state === "open") {
|
|
220
|
+
const hasNewEvents = options.events.some((e) => !connectedEvents.has(e));
|
|
221
|
+
if (hasNewEvents) {
|
|
222
|
+
intentionalReconnect = true;
|
|
223
|
+
socket.close();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return () => {
|
|
227
|
+
if (!sub.active) return;
|
|
228
|
+
sub.active = false;
|
|
229
|
+
subscriptions.delete(sub);
|
|
230
|
+
if (!destroyed) {
|
|
231
|
+
const hasActive = [...subscriptions].some((s) => s.active);
|
|
232
|
+
if (!hasActive && socket) {
|
|
233
|
+
socket.close();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
options.onClose?.();
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function send(type, payload) {
|
|
240
|
+
if (destroyed || !socket || socket.readyState !== WebSocket.OPEN) return;
|
|
241
|
+
socket.send(JSON.stringify({ type, data: payload }));
|
|
242
|
+
}
|
|
243
|
+
function close() {
|
|
244
|
+
destroyed = true;
|
|
245
|
+
if (reconnectTimer) {
|
|
246
|
+
clearTimeout(reconnectTimer);
|
|
247
|
+
reconnectTimer = null;
|
|
248
|
+
}
|
|
249
|
+
if (socket) {
|
|
250
|
+
socket.close();
|
|
251
|
+
} else {
|
|
252
|
+
setState("closed");
|
|
253
|
+
sentEvents = /* @__PURE__ */ new Set();
|
|
254
|
+
for (const sub of subscriptions) {
|
|
255
|
+
if (sub.active) {
|
|
256
|
+
sub.options.onClose?.();
|
|
257
|
+
sub.active = false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
subscriptions.clear();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
subscribe,
|
|
265
|
+
send,
|
|
266
|
+
getState: () => state,
|
|
267
|
+
close
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export { createWSClient };
|
|
272
|
+
//# sourceMappingURL=client.js.map
|
|
273
|
+
//# sourceMappingURL=client.js.map
|