@spfn/core 0.3.0-beta.24 → 0.3.0-beta.25
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 +1 -0
- package/dist/contract/index.d.ts +1 -1
- package/dist/event/index.d.ts +3 -3
- package/dist/event/index.js +30 -10
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +2 -2
- package/dist/event/sse/index.d.ts +3 -3
- package/dist/event/sse/index.js +19 -9
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.d.ts +2 -2
- package/dist/event/ws/index.d.ts +3 -3
- package/dist/{router-ZDV1ISTo.d.ts → router-D5B0cpVN.d.ts} +39 -10
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +19 -9
- package/dist/server/index.js.map +1 -1
- package/dist/state/index.d.ts +380 -0
- package/dist/state/index.js +571 -0
- package/dist/state/index.js.map +1 -0
- package/dist/{types-cwMX0cyJ.d.ts → types-CqTsDETV.d.ts} +1 -1
- package/dist/{types-C_g6gD-w.d.ts → types-DqXYtI69.d.ts} +1 -1
- package/package.json +6 -1
- package/src/event/README.md +27 -10
- package/src/state/README.md +350 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/event/sse/bounded-writer.ts","../../../src/event/sse/connection-registry.ts","../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":["logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Bounded SSE outbound writer\n *\n * Serializes writes to one SSE stream through a single drain loop that `await`s\n * each `writeSSE` — so a slow client applies real backpressure instead of letting\n * frames pile up unbounded in memory (OOM under a fast producer + slow consumer).\n *\n * The queue is per-connection, so a slow stream never blocks other connections.\n * When the queue exceeds `maxQueue`, the connection is closed (via `onClose`)\n * rather than silently dropping frames — dropping a chunk would corrupt an\n * ordered token/chat stream; the client reconnects and re-fetches instead.\n */\n\n/** A frame to write to the SSE stream (subset of Hono's SSEMessage). */\nexport interface SSEFrame\n{\n data: string;\n event?: string;\n id?: string;\n}\n\n/** Minimal stream surface this writer needs (Hono's SSEStreamingApi satisfies it). */\nexport interface SSEWritable\n{\n writeSSE(message: SSEFrame): Promise<void>;\n}\n\nexport interface BoundedWriter\n{\n /** Queue a frame for delivery; closes the connection if the queue overflows. */\n enqueue(frame: SSEFrame): void;\n\n /** Stop draining and drop any queued frames (called on connection cleanup). */\n close(): void;\n\n /** Current queue depth (for tests/diagnostics). */\n readonly queued: number;\n}\n\n/**\n * Create a bounded, backpressure-aware writer for one SSE stream.\n *\n * @param stream the SSE stream (`writeSSE` returns a promise that resolves when\n * the socket has drained — that is what gives us backpressure)\n * @param maxQueue max frames buffered before the connection is closed\n * @param onClose called once when the writer closes the connection (overflow or\n * a write error); the caller cleans up and logs\n */\nexport function createBoundedWriter(\n stream: SSEWritable,\n maxQueue: number,\n onClose: (reason: string) => void,\n): BoundedWriter\n{\n const pending: SSEFrame[] = [];\n let flushing = false;\n let closed = false;\n\n const flush = async (): Promise<void> =>\n {\n if (flushing || closed)\n {\n return;\n }\n\n flushing = true;\n\n while (pending.length > 0 && !closed)\n {\n const frame = pending.shift() as SSEFrame;\n try\n {\n await stream.writeSSE(frame);\n }\n catch (err)\n {\n flushing = false;\n onClose(err instanceof Error ? err.message : String(err));\n\n return;\n }\n }\n\n flushing = false;\n };\n\n return {\n enqueue(frame: SSEFrame): void\n {\n if (closed)\n {\n return;\n }\n\n pending.push(frame);\n\n // Slow consumer: the drain loop can't keep up, so the queue grew past\n // the bound. Close rather than buffer unboundedly or drop silently.\n if (pending.length > maxQueue)\n {\n onClose('outbound queue overflow (slow consumer)');\n\n return;\n }\n\n void flush();\n },\n\n close(): void\n {\n closed = true;\n pending.length = 0;\n },\n\n get queued(): number\n {\n return pending.length;\n },\n };\n}\n","/**\n * SSE connection registry\n *\n * What one `createSSEHandler` knows about its open connections, so that\n * `EventDef.grant` / `EventDef.revoke` can change a connection after it opened.\n * One registry per handler: two handlers in one process (tests) never share one.\n *\n * Neither operation consults `canSubscribe` or `authorize`: they are an\n * operator's statement about an open connection. Permission lives in app data\n * and is re-read at the next connect.\n *\n * A connect still reading app data (`authorize`, `resolvePrincipal`,\n * `canSubscribe`) is a pending admission: a `grant` / `revoke` that lands then\n * is recorded on it and applied, without a frame, when the connection opens —\n * so a call the app made is never lost to that window.\n */\n\nimport { logger } from '@spfn/core/logger';\nimport type { SubscribeRequest } from '../types';\n\nconst registryLogger = logger.child('@spfn/core:sse');\n\n/** Stream-owned frame: an event was activated on the open connection. */\nexport const SUBSCRIPTION_GRANTED = 'subscription.granted';\n\n/** Stream-owned frame: an event was deactivated on the open connection. */\nexport const SUBSCRIPTION_REVOKED = 'subscription.revoked';\n\nexport type SubscriptionFrame = typeof SUBSCRIPTION_GRANTED | typeof SUBSCRIPTION_REVOKED;\n\n/**\n * One open SSE connection. `allowed` and `denied` partition `requested`\n * (plus anything `authorize` added); only `allowed` events hold a live\n * subscription in `unsubscribes`.\n */\nexport interface SSEConnection\n{\n /** `undefined` when the stream runs without auth — such a connection is never registered. */\n readonly subject: string | undefined;\n readonly requested: readonly string[];\n readonly allowed: Set<string>;\n readonly denied: Set<string>;\n readonly unsubscribes: Map<string, () => void>;\n readonly request: SubscribeRequest;\n\n /** The per-event subscribe the connection opened with — `grant` reuses it, never a copy. */\n readonly subscribe: (eventName: string) => () => void;\n\n /** Enqueue a protocol frame about one event (dropped once the connection is dead). */\n readonly notify: (frame: SubscriptionFrame, eventName: string) => void;\n}\n\nexport type ConnectionRegistry = ReturnType<typeof createConnectionRegistry>;\n\ntype SubscriptionChange = 'grant' | 'revoke';\n\n/**\n * A connect of one subject between its first app-data read and its open.\n * The first of `open` / `cancel` ends it; both are synchronous. A client abort\n * cancels it at once — even while an app predicate never settles.\n */\nexport interface PendingAdmission\n{\n /**\n * Apply the recorded changes to `connection`, then swap this entry for the\n * open connection. `false`, with nothing applied or registered, once cancelled.\n */\n readonly open: (connection: SSEConnection) => boolean;\n\n /** Drop this entry (refused, aborted, or thrown); idempotent. */\n readonly cancel: () => void;\n}\n\n/** The changes recorded on one pending admission: event → last call (last one wins). */\ntype RecordedChanges = Map<string, SubscriptionChange>;\n\n/**\n * Create the registry of one SSE handler, keyed by subject → open connections\n * (a subject with two tabs has two).\n */\nexport function createConnectionRegistry()\n{\n const bySubject = new Map<string, Set<SSEConnection>>();\n const pendingBySubject = new Map<string, Set<RecordedChanges>>();\n\n /**\n * Register a pending admission of `subject`; `signal` is the connect's\n * abort signal, listened to until the admission ends either way.\n */\n const beginAdmission = (subject: string, signal: AbortSignal): PendingAdmission =>\n {\n const changes: RecordedChanges = new Map();\n addToSet(pendingBySubject, subject, changes);\n const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));\n\n return {\n cancel: end,\n open: (connection: SSEConnection) =>\n {\n if (!end())\n {\n return false;\n }\n\n applyRecorded(connection, changes);\n addToSet(bySubject, subject, connection);\n\n return true;\n },\n };\n };\n\n const change = (kind: SubscriptionChange, subject: string, eventName: string): void =>\n {\n const pending = pendingBySubject.get(subject);\n const connections = bySubject.get(subject);\n pending?.forEach(changes => changes.set(eventName, kind));\n connections?.forEach(connection => (kind === 'grant' ? grantOn : revokeOn)(connection, eventName));\n\n if (!pending && !connections)\n {\n registryLogger.debug('SSE subscription change for a subject with no open connection ignored', {\n event: eventName,\n subject,\n });\n }\n };\n\n return {\n beginAdmission,\n\n remove: (connection: SSEConnection): void =>\n {\n if (connection.subject)\n {\n removeFromSet(bySubject, connection.subject, connection);\n }\n },\n\n /** Open connections only — a pending admission is not counted. */\n count: (subject: string): number => bySubject.get(subject)?.size ?? 0,\n\n pendingCount: (subject: string): number => pendingBySubject.get(subject)?.size ?? 0,\n\n grant: (subject: string, eventName: string): void => change('grant', subject, eventName),\n\n revoke: (subject: string, eventName: string): void => change('revoke', subject, eventName),\n };\n}\n\n/**\n * The end of one pending admission: `remove` runs on the first call only,\n * which returns `true`; later calls return `false`. An abort of `signal` is a\n * first call; the listener is dropped on every end, so it never outlives the\n * admission (an already aborted signal ends it at once).\n */\nfunction endOnce(signal: AbortSignal, remove: () => void): () => boolean\n{\n let ended = false;\n const end = (): boolean =>\n {\n if (ended)\n {\n return false;\n }\n\n ended = true;\n signal.removeEventListener('abort', end);\n remove();\n\n return true;\n };\n\n signal.addEventListener('abort', end, { once: true });\n if (signal.aborted)\n {\n end();\n }\n\n return end;\n}\n\nfunction addToSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key) ?? new Set<T>();\n set.add(value);\n map.set(key, set);\n}\n\n/** Remove `value` and drop the key once its set is empty, so neither map grows on reconnects. */\nfunction removeFromSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key);\n set?.delete(value);\n\n if (set?.size === 0)\n {\n map.delete(key);\n }\n}\n\n/**\n * Apply what was recorded during admission to a connection that has not\n * opened yet: move the event between `allowed` and `denied`, no frame. An\n * event the connection never requested is in neither set and stays out.\n */\nfunction applyRecorded(connection: SSEConnection, changes: RecordedChanges): void\n{\n changes.forEach((kind, eventName) =>\n {\n const [from, to] = kind === 'grant'\n ? [connection.denied, connection.allowed]\n : [connection.allowed, connection.denied];\n\n if (from.delete(eventName))\n {\n to.add(eventName);\n }\n });\n}\n\n/**\n * Activate a denied event on one connection; allowed or never requested → no change.\n */\nfunction grantOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.denied.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n connection.denied.delete(eventName);\n connection.allowed.add(eventName);\n connection.notify(SUBSCRIPTION_GRANTED, eventName);\n}\n\n/**\n * Deactivate an allowed event on one connection; denied or never requested → no change.\n *\n * A frame the event already enqueued before this call may still be written —\n * the queue is not rewritten — but no frame after the unsubscribe is.\n */\nfunction revokeOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.allowed.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.get(eventName)?.();\n connection.unsubscribes.delete(eventName);\n connection.allowed.delete(eventName);\n connection.denied.add(eventName);\n connection.notify(SUBSCRIPTION_REVOKED, eventName);\n}\n\n/**\n * Drop every live subscription of a closing connection. A revoked event was\n * already removed from `unsubscribes`, so nothing is unsubscribed twice.\n */\nexport function unsubscribeAll(connection: SSEConnection): void\n{\n connection.unsubscribes.forEach(unsubscribe => unsubscribe());\n connection.unsubscribes.clear();\n}\n","/**\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 type { SSEStreamingApi } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef } from '../router';\nimport type { EventDef, SubscribeContext, SubscribeRequest } from '../types';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\nimport { createBoundedWriter } from './bounded-writer';\nimport type { BoundedWriter } from './bounded-writer';\nimport { createConnectionRegistry, unsubscribeAll } from './connection-registry';\nimport type { ConnectionRegistry, PendingAdmission, SSEConnection } from './connection-registry';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n/**\n * Serialize a fan-out frame once per (event, payload).\n *\n * One `emit` delivers the same payload object to every subscribed connection, so\n * without memoization each connection re-`JSON.stringify`s the identical payload\n * — O(N) for N subscribers on a hot broadcast. Keyed by the payload object\n * (WeakMap → auto-GC after the emit); primitives are cheap to stringify directly.\n */\nconst frameCache = new WeakMap<object, Map<string, string>>();\n\nexport function serializeFrame(eventName: string, payload: unknown): string\n{\n if (payload === null || typeof payload !== 'object')\n {\n return JSON.stringify({ event: eventName, data: payload });\n }\n\n let byEvent = frameCache.get(payload);\n if (!byEvent)\n {\n byEvent = new Map<string, string>();\n frameCache.set(payload, byEvent);\n }\n\n let serialized = byEvent.get(eventName);\n if (serialized === undefined)\n {\n serialized = JSON.stringify({ event: eventName, data: payload });\n byEvent.set(eventName, serialized);\n }\n\n return serialized;\n}\n\n/** Default outbound queue cap per connection before a slow consumer is dropped. */\nconst DEFAULT_MAX_QUEUE = 1000;\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 * Who receives what, in order: the global `authorize` (once, at connect) →\n * each requested event's `.canSubscribe()` (once per event, at connect) → the\n * global `filter` map and the event's `.filter()` (sync, per frame). An event\n * denied at connect stays in `deniedEvents`, so `EventDef.grant` can activate\n * it later; `EventDef.revoke` deactivates an allowed one. The handler\n * registers that control on every event of the router.\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 // 10s — keep-alive가 중간 프록시(GCP LB/nginx)의 idle 타임아웃(흔히 ~15-30s)보다 자주 와야\n // 첫 ping 전 침묵 구간에 연결이 끊기지 않는다. 30s 기본은 idle 타임아웃보다 길어 SSE가\n // 데이터 없는 동안 끊기던 문제가 있었다.\n pingInterval = 10000,\n auth: authConfig,\n maxQueue = DEFAULT_MAX_QUEUE,\n } = config;\n\n const registry = createConnectionRegistry();\n registerSubscriptionControls(router, registry);\n\n const streamConfig: StreamConfig = { router, authConfig, registry, pingInterval, maxQueue };\n\n const handler = async (c: Context) =>\n {\n const admission = await admitConnection(c, streamConfig, tokenManager);\n if (admission instanceof Response)\n {\n return admission;\n }\n\n if (c.req.raw.signal.aborted)\n {\n // The client left during admission: nobody reads this response.\n // Its abort listener already cancelled `pending`; cancel is idempotent.\n admission.pending?.cancel();\n\n return new Response(null, { status: 499 });\n }\n\n // ── 6. SSE Stream ──\n c.header('X-Accel-Buffering', 'no');\n\n return streamSSE(c, stream => runStream(c, stream, admission, streamConfig), async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n\n return Object.assign(handler, {\n /** @internal Open registered connections of a subject (tests). */\n _openConnections: (subject: string): number => registry.count(subject),\n\n /** @internal Connects of a subject still in admission (tests). */\n _pendingAdmissions: (subject: string): number => registry.pendingCount(subject),\n });\n}\n\n// ============================================================================\n// Connect\n// ============================================================================\n\n/** What a request that passed every connect check opens the stream with. */\ninterface Admission\n{\n subject: string | undefined;\n requested: string[];\n allowed: string[];\n denied: string[];\n request: SubscribeRequest;\n\n /** Registered before the first app-data read; `runStream` opens it. Absent without a subject. */\n pending?: PendingAdmission;\n}\n\n/** What every stream of one handler shares. */\ninterface StreamConfig\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n registry: ConnectionRegistry;\n pingInterval: number;\n maxQueue: number;\n}\n\n/**\n * Let `EventDef.grant` / `revoke` reach this handler's connections, once per\n * router key (an event under two keys gets two controls, each naming its key).\n */\nfunction registerSubscriptionControls(router: EventRouterDef<any>, registry: ConnectionRegistry): void\n{\n for (const eventName of router.eventNames as string[])\n {\n router.events[eventName]._registerSubscriptionControl({\n grant: (subject: string) => registry.grant(subject, eventName),\n revoke: (subject: string) => registry.revoke(subject, eventName),\n });\n }\n}\n\n/**\n * Run the connect checks; a `Response` is the refusal to return.\n */\nasync function admitConnection(\n c: Context,\n config: StreamConfig,\n tokenManager: SSETokenManager | undefined,\n): Promise<Admission | Response>\n{\n const { router, authConfig, registry } = config;\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-3. Requested events, validated against the router ──\n const requested = parseRequestedEvents(c);\n if (!requested)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requested.filter(e => !validEventNames.includes(e));\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 return whilePending(\n subject ? registry.beginAdmission(subject, c.req.raw.signal) : undefined,\n () => authorizeConnection(c, router, authConfig, subject, requested),\n );\n}\n\n/**\n * Run the app-data checks with the admission already registered — `pending`\n * is written synchronously, before the first `await`, so a `grant` / `revoke`\n * issued meanwhile is recorded on it. A refusal or a throw unregisters it; a\n * client abort already did, the moment it happened; an admitted connection\n * carries it to `runStream`.\n */\nasync function whilePending(\n pending: PendingAdmission | undefined,\n check: () => Promise<Admission | Response>,\n): Promise<Admission | Response>\n{\n let outcome: Admission | Response | undefined;\n\n try\n {\n outcome = await check();\n\n return outcome instanceof Response ? outcome : { ...outcome, pending };\n }\n finally\n {\n if (!outcome || outcome instanceof Response)\n {\n pending?.cancel();\n }\n }\n}\n\n/**\n * Steps 4-5: the global `authorize`, then the principal and each event's\n * `canSubscribe`. Only an empty `authorize` result or a failed principal\n * refuses; a `canSubscribe` denial only lands the event in `denied`.\n */\nasync function authorizeConnection(\n c: Context,\n router: EventRouterDef<any>,\n authConfig: SSEHandlerAuthConfig | undefined,\n subject: string | undefined,\n requested: string[],\n): Promise<Admission | Response>\n{\n // ── 4. Subscription Authorization ──\n const authorized = await authorizeEvents(subject, requested, authConfig);\n if (authorized === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n // ── 5. Principal + per-event canSubscribe ──\n const request: SubscribeRequest = { query: c.req.query(), headers: c.req.header() };\n const resolved = await resolveConnectionPrincipal(subject, request, authConfig);\n if (!resolved)\n {\n return c.json({ error: 'Principal could not be resolved' }, 403);\n }\n\n const allowed = subject\n ? await checkCanSubscribe(router, authorized, { subject, principal: resolved.principal, request })\n : authorized;\n const denied = requested.filter(e => !allowed.includes(e));\n\n sseLogger.debug('SSE connection requested', {\n events: allowed,\n deniedEvents: denied,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n return { subject, requested, allowed, denied, request };\n}\n\n/**\n * Resolve the principal once per connection; `null` refuses the connection.\n * The warning names the subject only — never the thrown error's text, which\n * may carry what the resolver read.\n */\nasync function resolveConnectionPrincipal(\n subject: string | undefined,\n request: SubscribeRequest,\n authConfig: SSEHandlerAuthConfig | undefined,\n): Promise<{ principal: unknown } | null>\n{\n if (!subject || !authConfig?.resolvePrincipal)\n {\n return { principal: undefined };\n }\n\n try\n {\n return { principal: await authConfig.resolvePrincipal(subject, request) };\n }\n catch\n {\n sseLogger.warn('SSE principal could not be resolved — connection refused', { subject });\n\n return null;\n }\n}\n\n/**\n * Ask each authorized event's `canSubscribe`, in parallel; returns the events\n * it allows. An event without a predicate is allowed.\n */\nasync function checkCanSubscribe(\n router: EventRouterDef<any>,\n events: string[],\n ctx: SubscribeContext,\n): Promise<string[]>\n{\n const verdicts = await Promise.all(events.map(eventName => askCanSubscribe(eventName, router.events[eventName], ctx)));\n\n return events.filter((_, i) => verdicts[i]);\n}\n\n/**\n * `true` → allowed; `false` or a throw → denied. A throw is warned with the\n * event name and never fails the connection.\n */\nasync function askCanSubscribe(\n eventName: string,\n eventDef: EventDef<any> | undefined,\n ctx: SubscribeContext,\n): Promise<boolean>\n{\n if (!eventDef)\n {\n return false;\n }\n\n if (!eventDef._canSubscribe)\n {\n return true;\n }\n\n try\n {\n return await eventDef._canSubscribe(ctx) === true;\n }\n catch\n {\n sseLogger.warn('SSE canSubscribe threw — event denied on this connection', {\n event: eventName,\n subject: ctx.subject,\n });\n\n return false;\n }\n}\n\n// ============================================================================\n// Stream\n// ============================================================================\n\n/**\n * The life of one open connection: connected frame, subscriptions, registry\n * entry, keep-alive, and teardown on disconnect or a server-side close.\n */\nasync function runStream(\n c: Context,\n stream: SSEStreamingApi,\n admission: Admission,\n config: StreamConfig,\n): Promise<void>\n{\n const { allowed, denied, subject, request } = admission;\n let connectionDead = false;\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n // Resolves the disconnect wait below when the connection is closed\n // server-side (overflow/error), so teardown doesn't wait for the loop.\n let onClosed: (() => void) | undefined;\n\n const cleanup = (reason?: string) =>\n {\n if (connectionDead) return;\n connectionDead = true;\n clearInterval(pingTimer);\n unsubscribeAll(connection);\n config.registry.remove(connection);\n writer.close();\n onClosed?.();\n sseLogger.info('SSE dead connection cleaned up', {\n events: [...connection.allowed],\n reason,\n });\n };\n\n // All writes go through one bounded, backpressure-aware drain loop so a\n // slow client can't pile frames up in memory (it's closed on overflow).\n const writer = createBoundedWriter(stream, config.maxQueue, (reason) =>\n {\n sseLogger.warn('SSE connection closed', { events: [...connection.allowed], reason });\n cleanup(reason);\n });\n\n const connection: SSEConnection = {\n subject,\n requested: admission.requested,\n allowed: new Set(allowed),\n denied: new Set(denied),\n unsubscribes: new Map(),\n request,\n subscribe: createEventSubscriber({\n router: config.router,\n authConfig: config.authConfig,\n subject,\n request,\n writer,\n isDead: () => connectionDead,\n }),\n notify: (frame, eventName) =>\n {\n if (connectionDead) return;\n\n // A small literal — not routed through serializeFrame's per-payload cache.\n writer.enqueue({ event: frame, data: JSON.stringify({ event: eventName }) });\n },\n };\n\n // Synchronously before the `connected` frame: the changes recorded during\n // admission land in `allowed` / `denied` first, and the entry turns open.\n // A cancelled admission (the client left) opens nothing and sends no frame.\n // Defensive: unreachable today, since `streamSSE` runs this synchronously up\n // to here; the handler's `signal.aborted` → 499 check is the real path.\n if (admission.pending?.open(connection) === false)\n {\n cleanup('aborted during admission');\n\n return;\n }\n\n openSubscriptions(connection, writer);\n\n // Keep-alive ping\n pingTimer = setInterval(() =>\n {\n if (connectionDead) return;\n\n writer.enqueue({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n });\n }, config.pingInterval);\n\n await waitForDisconnect(c.req.raw.signal, () => connectionDead, (resolve) =>\n {\n onClosed = resolve;\n });\n\n // Cleanup (normal disconnect path; idempotent if already closed)\n cleanup();\n}\n\n/**\n * Send the `connected` frame (first in the queue) and subscribe every allowed event.\n */\nfunction openSubscriptions(connection: SSEConnection, writer: BoundedWriter): void\n{\n writer.enqueue({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: [...connection.allowed],\n deniedEvents: [...connection.denied],\n timestamp: Date.now(),\n }),\n });\n\n for (const eventName of connection.allowed)\n {\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n }\n\n sseLogger.info('SSE connection established', {\n events: [...connection.allowed],\n deniedEvents: [...connection.denied],\n subscriptionCount: connection.unsubscribes.size,\n });\n}\n\n/**\n * Wait for client disconnect (abort) or a server-side close. Waiting on the\n * abort event instead of polling sleep(pingInterval) means a dropped client is\n * torn down immediately, not up to one ping interval later.\n */\nasync function waitForDisconnect(\n abortSignal: AbortSignal,\n isDead: () => boolean,\n onServerClose: (resolve: () => void) => void,\n): Promise<void>\n{\n if (abortSignal.aborted || isDead())\n {\n return;\n }\n\n await new Promise<void>((resolve) =>\n {\n const onAbort = () => resolve();\n abortSignal.addEventListener('abort', onAbort, { once: true });\n // Server-side close (cleanup) resolves the wait and drops the listener.\n onServerClose(() =>\n {\n abortSignal.removeEventListener('abort', onAbort);\n resolve();\n });\n });\n}\n\n/** What the per-event subscribe of one connection closes over. */\ninterface SubscriberContext\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n subject: string | undefined;\n request: SubscribeRequest;\n writer: BoundedWriter;\n isDead: () => boolean;\n}\n\n/**\n * The per-event subscribe of one connection, used at connect and by `grant`\n * alike, so filter order and `messageId` numbering are one code path.\n *\n * The returned unsubscribe also deactivates the callback, so a frame of an\n * `emit` already fanning out when `revoke` lands is not enqueued after it.\n */\nfunction createEventSubscriber(ctx: SubscriberContext): (eventName: string) => () => void\n{\n let messageId = 0;\n\n return (eventName: string) =>\n {\n const eventDef = ctx.router.events[eventName];\n let active = true;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (!active || ctx.isDead() || !passesFrameFilters(eventName, eventDef, payload, ctx))\n {\n return;\n }\n\n messageId++;\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n ctx.writer.enqueue({\n id: String(messageId),\n event: eventName,\n data: serializeFrame(eventName, payload),\n });\n });\n\n return () =>\n {\n active = false;\n unsubscribe();\n };\n };\n}\n\n/**\n * The global `filter` map (with a subject, as before) and the event's own\n * `.filter()` — both must pass.\n */\nfunction passesFrameFilters(\n eventName: string,\n eventDef: EventDef<any>,\n payload: unknown,\n ctx: SubscriberContext,\n): boolean\n{\n const globalFilter = ctx.authConfig?.filter?.[eventName];\n if (ctx.subject && globalFilter && !globalFilter(ctx.subject, payload))\n {\n return false;\n }\n\n if (!eventDef._filter)\n {\n return true;\n }\n\n try\n {\n return eventDef._filter(payload, { subject: ctx.subject, request: ctx.request }) === true;\n }\n catch\n {\n sseLogger.warn('SSE event filter threw — frame dropped', { event: eventName, subject: ctx.subject });\n\n return false;\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 // Deduplicated: one subscription per event, keyed by name in the registry.\n return [...new Set(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, createHash } from 'crypto';\n\n/**\n * Hash a token for storage. The raw token is a bearer secret — storing it at rest\n * (e.g. as a Redis key/value) means a store dump hands out live tokens. We persist\n * only sha256(token) and look up by the same hash; the raw token exists only in\n * transit and in the issue() return value.\n */\nfunction hashToken(token: string): string\n{\n return createHash('sha256').update(token).digest('hex');\n}\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 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(hashToken(token), data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = hashToken(token);\n const data = this.tokens.get(key);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(key);\n\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [key, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(key);\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\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 + hashToken(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 + hashToken(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 * How long an issued token stays valid, in milliseconds.\n *\n * Exposed so a caller that has to report an expiry to its client computes the same\n * number this manager stamps on the token, instead of assuming the default.\n */\n get ttlMs(): number\n {\n return this.ttl;\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 // The store keys by sha256(token); the raw token is never persisted.\n await this.store.set(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/bounded-writer.ts","../../../src/event/sse/connection-registry.ts","../../../src/event/sse/handler.ts","../../../src/event/sse/token-manager.ts"],"names":["logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Bounded SSE outbound writer\n *\n * Serializes writes to one SSE stream through a single drain loop that `await`s\n * each `writeSSE` — so a slow client applies real backpressure instead of letting\n * frames pile up unbounded in memory (OOM under a fast producer + slow consumer).\n *\n * The queue is per-connection, so a slow stream never blocks other connections.\n * When the queue exceeds `maxQueue`, the connection is closed (via `onClose`)\n * rather than silently dropping frames — dropping a chunk would corrupt an\n * ordered token/chat stream; the client reconnects and re-fetches instead.\n */\n\n/** A frame to write to the SSE stream (subset of Hono's SSEMessage). */\nexport interface SSEFrame\n{\n data: string;\n event?: string;\n id?: string;\n}\n\n/** Minimal stream surface this writer needs (Hono's SSEStreamingApi satisfies it). */\nexport interface SSEWritable\n{\n writeSSE(message: SSEFrame): Promise<void>;\n}\n\nexport interface BoundedWriter\n{\n /** Queue a frame for delivery; closes the connection if the queue overflows. */\n enqueue(frame: SSEFrame): void;\n\n /** Stop draining and drop any queued frames (called on connection cleanup). */\n close(): void;\n\n /** Current queue depth (for tests/diagnostics). */\n readonly queued: number;\n}\n\n/**\n * Create a bounded, backpressure-aware writer for one SSE stream.\n *\n * @param stream the SSE stream (`writeSSE` returns a promise that resolves when\n * the socket has drained — that is what gives us backpressure)\n * @param maxQueue max frames buffered before the connection is closed\n * @param onClose called once when the writer closes the connection (overflow or\n * a write error); the caller cleans up and logs\n */\nexport function createBoundedWriter(\n stream: SSEWritable,\n maxQueue: number,\n onClose: (reason: string) => void,\n): BoundedWriter\n{\n const pending: SSEFrame[] = [];\n let flushing = false;\n let closed = false;\n\n const flush = async (): Promise<void> =>\n {\n if (flushing || closed)\n {\n return;\n }\n\n flushing = true;\n\n while (pending.length > 0 && !closed)\n {\n const frame = pending.shift() as SSEFrame;\n try\n {\n await stream.writeSSE(frame);\n }\n catch (err)\n {\n flushing = false;\n onClose(err instanceof Error ? err.message : String(err));\n\n return;\n }\n }\n\n flushing = false;\n };\n\n return {\n enqueue(frame: SSEFrame): void\n {\n if (closed)\n {\n return;\n }\n\n pending.push(frame);\n\n // Slow consumer: the drain loop can't keep up, so the queue grew past\n // the bound. Close rather than buffer unboundedly or drop silently.\n if (pending.length > maxQueue)\n {\n onClose('outbound queue overflow (slow consumer)');\n\n return;\n }\n\n void flush();\n },\n\n close(): void\n {\n closed = true;\n pending.length = 0;\n },\n\n get queued(): number\n {\n return pending.length;\n },\n };\n}\n","/**\n * SSE connection registry\n *\n * What one `createSSEHandler` knows about its open connections, so that\n * `EventDef.grant` / `EventDef.revoke` can change a connection after it opened.\n * One registry per handler: two handlers in one process (tests) never share one.\n *\n * Neither operation consults `canSubscribe` or `authorize`: they are an\n * operator's statement about an open connection. Permission lives in app data\n * and is re-read at the next connect. A `match` narrows either to the\n * connections whose connect query carries its fields.\n *\n * A connect still reading app data (`authorize`, `resolvePrincipal`,\n * `canSubscribe`) is a pending admission: a `grant` / `revoke` that lands then\n * is recorded on it and applied, without a frame, when the connection opens —\n * so a call the app made is never lost to that window.\n */\n\nimport { logger } from '@spfn/core/logger';\nimport type { SubscribeRequest, SubscriptionMatch } from '../types';\n\nconst registryLogger = logger.child('@spfn/core:sse');\n\n/** Stream-owned frame: an event was activated on the open connection. */\nexport const SUBSCRIPTION_GRANTED = 'subscription.granted';\n\n/** Stream-owned frame: an event was deactivated on the open connection. */\nexport const SUBSCRIPTION_REVOKED = 'subscription.revoked';\n\nexport type SubscriptionFrame = typeof SUBSCRIPTION_GRANTED | typeof SUBSCRIPTION_REVOKED;\n\n/**\n * One open SSE connection. `allowed` and `denied` partition `requested`\n * (plus anything `authorize` added); only `allowed` events hold a live\n * subscription in `unsubscribes`.\n */\nexport interface SSEConnection\n{\n /** `undefined` when the stream runs without auth — such a connection is never registered. */\n readonly subject: string | undefined;\n readonly requested: readonly string[];\n readonly allowed: Set<string>;\n readonly denied: Set<string>;\n readonly unsubscribes: Map<string, () => void>;\n readonly request: SubscribeRequest;\n\n /** The per-event subscribe the connection opened with — `grant` reuses it, never a copy. */\n readonly subscribe: (eventName: string) => () => void;\n\n /** Enqueue a protocol frame about one event (dropped once the connection is dead). */\n readonly notify: (frame: SubscriptionFrame, eventName: string) => void;\n}\n\nexport type ConnectionRegistry = ReturnType<typeof createConnectionRegistry>;\n\ntype SubscriptionChange = 'grant' | 'revoke';\n\n/**\n * A connect of one subject between its first app-data read and its open.\n * The first of `open` / `cancel` ends it; both are synchronous. A client abort\n * cancels it at once — even while an app predicate never settles.\n */\nexport interface PendingAdmission\n{\n /**\n * Apply the recorded changes to `connection`, then swap this entry for the\n * open connection. `false`, with nothing applied or registered, once cancelled.\n */\n readonly open: (connection: SSEConnection) => boolean;\n\n /** Drop this entry (refused, aborted, or thrown); idempotent. */\n readonly cancel: () => void;\n}\n\n/** One `grant` / `revoke` recorded on a pending admission. */\ninterface RecordedChange\n{\n kind: SubscriptionChange;\n eventName: string;\n match?: SubscriptionMatch;\n}\n\n/** The changes recorded on one pending admission, in call order (the last one wins). */\ntype RecordedChanges = RecordedChange[];\n\n/**\n * Create the registry of one SSE handler, keyed by subject → open connections\n * (a subject with two tabs has two).\n */\nexport function createConnectionRegistry()\n{\n const bySubject = new Map<string, Set<SSEConnection>>();\n const pendingBySubject = new Map<string, Set<RecordedChanges>>();\n\n /**\n * Register a pending admission of `subject`; `signal` is the connect's\n * abort signal, listened to until the admission ends either way.\n */\n const beginAdmission = (subject: string, signal: AbortSignal): PendingAdmission =>\n {\n const changes: RecordedChanges = [];\n addToSet(pendingBySubject, subject, changes);\n const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));\n\n return {\n cancel: end,\n open: (connection: SSEConnection) =>\n {\n if (!end())\n {\n return false;\n }\n\n applyRecorded(connection, changes);\n addToSet(bySubject, subject, connection);\n\n return true;\n },\n };\n };\n\n const change = (kind: SubscriptionChange, subject: string, eventName: string, match?: SubscriptionMatch): void =>\n {\n const pending = pendingBySubject.get(subject);\n const connections = bySubject.get(subject);\n pending?.forEach(changes => changes.push({ kind, eventName, match }));\n connections?.forEach(connection =>\n {\n if (matchesRequest(match, connection.request))\n {\n (kind === 'grant' ? grantOn : revokeOn)(connection, eventName);\n }\n });\n\n if (!pending && !connections)\n {\n registryLogger.debug('SSE subscription change for a subject with no open connection ignored', {\n event: eventName,\n subject,\n });\n }\n };\n\n return {\n beginAdmission,\n\n remove: (connection: SSEConnection): void =>\n {\n if (connection.subject)\n {\n removeFromSet(bySubject, connection.subject, connection);\n }\n },\n\n /** Open connections only — a pending admission is not counted. */\n count: (subject: string): number => bySubject.get(subject)?.size ?? 0,\n\n pendingCount: (subject: string): number => pendingBySubject.get(subject)?.size ?? 0,\n\n grant: (subject: string, eventName: string, match?: SubscriptionMatch): void =>\n change('grant', subject, eventName, match),\n\n revoke: (subject: string, eventName: string, match?: SubscriptionMatch): void =>\n change('revoke', subject, eventName, match),\n };\n}\n\n/**\n * The end of one pending admission: `remove` runs on the first call only,\n * which returns `true`; later calls return `false`. An abort of `signal` is a\n * first call; the listener is dropped on every end, so it never outlives the\n * admission (an already aborted signal ends it at once).\n */\nfunction endOnce(signal: AbortSignal, remove: () => void): () => boolean\n{\n let ended = false;\n const end = (): boolean =>\n {\n if (ended)\n {\n return false;\n }\n\n ended = true;\n signal.removeEventListener('abort', end);\n remove();\n\n return true;\n };\n\n signal.addEventListener('abort', end, { once: true });\n if (signal.aborted)\n {\n end();\n }\n\n return end;\n}\n\nfunction addToSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key) ?? new Set<T>();\n set.add(value);\n map.set(key, set);\n}\n\n/** Remove `value` and drop the key once its set is empty, so neither map grows on reconnects. */\nfunction removeFromSet<T>(map: Map<string, Set<T>>, key: string, value: T): void\n{\n const set = map.get(key);\n set?.delete(value);\n\n if (set?.size === 0)\n {\n map.delete(key);\n }\n}\n\n/**\n * Whether `match` reaches a connection: every named field equals the connect\n * query's value, string to string. A field the connection never declared\n * never matches, not even an empty string. No `match` reaches every one.\n */\nexport function matchesRequest(match: SubscriptionMatch | undefined, request: SubscribeRequest): boolean\n{\n return match === undefined || Object.entries(match.query).every(([field, value]) =>\n Object.hasOwn(request.query, field) && request.query[field] === value);\n}\n\n/**\n * Apply what was recorded during admission to a connection that has not\n * opened yet: move the event between `allowed` and `denied`, no frame. An\n * event the connection never requested is in neither set and stays out; a\n * change whose `match` the connection's query does not meet is skipped.\n */\nfunction applyRecorded(connection: SSEConnection, changes: RecordedChanges): void\n{\n changes.forEach(({ kind, eventName, match }) =>\n {\n if (!matchesRequest(match, connection.request))\n {\n return;\n }\n\n const [from, to] = kind === 'grant'\n ? [connection.denied, connection.allowed]\n : [connection.allowed, connection.denied];\n\n if (from.delete(eventName))\n {\n to.add(eventName);\n }\n });\n}\n\n/**\n * Activate a denied event on one connection; allowed or never requested → no change.\n */\nfunction grantOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.denied.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n connection.denied.delete(eventName);\n connection.allowed.add(eventName);\n connection.notify(SUBSCRIPTION_GRANTED, eventName);\n}\n\n/**\n * Deactivate an allowed event on one connection; denied or never requested → no change.\n *\n * A frame the event already enqueued before this call may still be written —\n * the queue is not rewritten — but no frame after the unsubscribe is.\n */\nfunction revokeOn(connection: SSEConnection, eventName: string): void\n{\n if (!connection.allowed.has(eventName))\n {\n return;\n }\n\n connection.unsubscribes.get(eventName)?.();\n connection.unsubscribes.delete(eventName);\n connection.allowed.delete(eventName);\n connection.denied.add(eventName);\n connection.notify(SUBSCRIPTION_REVOKED, eventName);\n}\n\n/**\n * Drop every live subscription of a closing connection. A revoked event was\n * already removed from `unsubscribes`, so nothing is unsubscribed twice.\n */\nexport function unsubscribeAll(connection: SSEConnection): void\n{\n connection.unsubscribes.forEach(unsubscribe => unsubscribe());\n connection.unsubscribes.clear();\n}\n","/**\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 type { SSEStreamingApi } from 'hono/streaming';\nimport { logger } from '@spfn/core/logger';\nimport type { EventRouterDef } from '../router';\nimport type { EventDef, SubscribeContext, SubscribeRequest, SubscriptionMatch } from '../types';\nimport type { SSEHandlerConfig, SSEHandlerAuthConfig } from './types';\nimport type { SSETokenManager } from './token-manager';\nimport { createBoundedWriter } from './bounded-writer';\nimport type { BoundedWriter } from './bounded-writer';\nimport { createConnectionRegistry, unsubscribeAll } from './connection-registry';\nimport type { ConnectionRegistry, PendingAdmission, SSEConnection } from './connection-registry';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n/**\n * Serialize a fan-out frame once per (event, payload).\n *\n * One `emit` delivers the same payload object to every subscribed connection, so\n * without memoization each connection re-`JSON.stringify`s the identical payload\n * — O(N) for N subscribers on a hot broadcast. Keyed by the payload object\n * (WeakMap → auto-GC after the emit); primitives are cheap to stringify directly.\n */\nconst frameCache = new WeakMap<object, Map<string, string>>();\n\nexport function serializeFrame(eventName: string, payload: unknown): string\n{\n if (payload === null || typeof payload !== 'object')\n {\n return JSON.stringify({ event: eventName, data: payload });\n }\n\n let byEvent = frameCache.get(payload);\n if (!byEvent)\n {\n byEvent = new Map<string, string>();\n frameCache.set(payload, byEvent);\n }\n\n let serialized = byEvent.get(eventName);\n if (serialized === undefined)\n {\n serialized = JSON.stringify({ event: eventName, data: payload });\n byEvent.set(eventName, serialized);\n }\n\n return serialized;\n}\n\n/** Default outbound queue cap per connection before a slow consumer is dropped. */\nconst DEFAULT_MAX_QUEUE = 1000;\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 * Who receives what, in order: the global `authorize` (once, at connect) →\n * each requested event's `.canSubscribe()` (once per event, at connect) → the\n * global `filter` map and the event's `.filter()` (sync, per frame). An event\n * denied at connect stays in `deniedEvents`, so `EventDef.grant` can activate\n * it later; `EventDef.revoke` deactivates an allowed one. The handler\n * registers that control on every event of the router.\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 // 10s — keep-alive가 중간 프록시(GCP LB/nginx)의 idle 타임아웃(흔히 ~15-30s)보다 자주 와야\n // 첫 ping 전 침묵 구간에 연결이 끊기지 않는다. 30s 기본은 idle 타임아웃보다 길어 SSE가\n // 데이터 없는 동안 끊기던 문제가 있었다.\n pingInterval = 10000,\n auth: authConfig,\n maxQueue = DEFAULT_MAX_QUEUE,\n } = config;\n\n const registry = createConnectionRegistry();\n registerSubscriptionControls(router, registry);\n\n const streamConfig: StreamConfig = { router, authConfig, registry, pingInterval, maxQueue };\n\n const handler = async (c: Context) =>\n {\n const admission = await admitConnection(c, streamConfig, tokenManager);\n if (admission instanceof Response)\n {\n return admission;\n }\n\n if (c.req.raw.signal.aborted)\n {\n // The client left during admission: nobody reads this response.\n // Its abort listener already cancelled `pending`; cancel is idempotent.\n admission.pending?.cancel();\n\n return new Response(null, { status: 499 });\n }\n\n // ── 6. SSE Stream ──\n c.header('X-Accel-Buffering', 'no');\n\n return streamSSE(c, stream => runStream(c, stream, admission, streamConfig), async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n\n return Object.assign(handler, {\n /** @internal Open registered connections of a subject (tests). */\n _openConnections: (subject: string): number => registry.count(subject),\n\n /** @internal Connects of a subject still in admission (tests). */\n _pendingAdmissions: (subject: string): number => registry.pendingCount(subject),\n });\n}\n\n// ============================================================================\n// Connect\n// ============================================================================\n\n/** What a request that passed every connect check opens the stream with. */\ninterface Admission\n{\n subject: string | undefined;\n requested: string[];\n allowed: string[];\n denied: string[];\n request: SubscribeRequest;\n\n /** Registered before the first app-data read; `runStream` opens it. Absent without a subject. */\n pending?: PendingAdmission;\n}\n\n/** What every stream of one handler shares. */\ninterface StreamConfig\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n registry: ConnectionRegistry;\n pingInterval: number;\n maxQueue: number;\n}\n\n/**\n * Let `EventDef.grant` / `revoke` reach this handler's connections, once per\n * router key (an event under two keys gets two controls, each naming its key).\n */\nfunction registerSubscriptionControls(router: EventRouterDef<any>, registry: ConnectionRegistry): void\n{\n for (const eventName of router.eventNames as string[])\n {\n router.events[eventName]._registerSubscriptionControl({\n grant: (subject: string, match?: SubscriptionMatch) => registry.grant(subject, eventName, match),\n revoke: (subject: string, match?: SubscriptionMatch) => registry.revoke(subject, eventName, match),\n });\n }\n}\n\n/**\n * Run the connect checks; a `Response` is the refusal to return.\n */\nasync function admitConnection(\n c: Context,\n config: StreamConfig,\n tokenManager: SSETokenManager | undefined,\n): Promise<Admission | Response>\n{\n const { router, authConfig, registry } = config;\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-3. Requested events, validated against the router ──\n const requested = parseRequestedEvents(c);\n if (!requested)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n const validEventNames = router.eventNames as string[];\n const invalidEvents = requested.filter(e => !validEventNames.includes(e));\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 return whilePending(\n subject ? registry.beginAdmission(subject, c.req.raw.signal) : undefined,\n () => authorizeConnection(c, router, authConfig, subject, requested),\n );\n}\n\n/**\n * Run the app-data checks with the admission already registered — `pending`\n * is written synchronously, before the first `await`, so a `grant` / `revoke`\n * issued meanwhile is recorded on it. A refusal or a throw unregisters it; a\n * client abort already did, the moment it happened; an admitted connection\n * carries it to `runStream`.\n */\nasync function whilePending(\n pending: PendingAdmission | undefined,\n check: () => Promise<Admission | Response>,\n): Promise<Admission | Response>\n{\n let outcome: Admission | Response | undefined;\n\n try\n {\n outcome = await check();\n\n return outcome instanceof Response ? outcome : { ...outcome, pending };\n }\n finally\n {\n if (!outcome || outcome instanceof Response)\n {\n pending?.cancel();\n }\n }\n}\n\n/**\n * Steps 4-5: the global `authorize`, then the principal and each event's\n * `canSubscribe`. Only an empty `authorize` result or a failed principal\n * refuses; a `canSubscribe` denial only lands the event in `denied`.\n */\nasync function authorizeConnection(\n c: Context,\n router: EventRouterDef<any>,\n authConfig: SSEHandlerAuthConfig | undefined,\n subject: string | undefined,\n requested: string[],\n): Promise<Admission | Response>\n{\n // ── 4. Subscription Authorization ──\n const authorized = await authorizeEvents(subject, requested, authConfig);\n if (authorized === null)\n {\n return c.json({ error: 'Not authorized for any requested events' }, 403);\n }\n\n // ── 5. Principal + per-event canSubscribe ──\n const request: SubscribeRequest = { query: c.req.query(), headers: c.req.header() };\n const resolved = await resolveConnectionPrincipal(subject, request, authConfig);\n if (!resolved)\n {\n return c.json({ error: 'Principal could not be resolved' }, 403);\n }\n\n const allowed = subject\n ? await checkCanSubscribe(router, authorized, { subject, principal: resolved.principal, request })\n : authorized;\n const denied = requested.filter(e => !allowed.includes(e));\n\n sseLogger.debug('SSE connection requested', {\n events: allowed,\n deniedEvents: denied,\n subject: subject || undefined,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n return { subject, requested, allowed, denied, request };\n}\n\n/**\n * Resolve the principal once per connection; `null` refuses the connection.\n * The warning names the subject only — never the thrown error's text, which\n * may carry what the resolver read.\n */\nasync function resolveConnectionPrincipal(\n subject: string | undefined,\n request: SubscribeRequest,\n authConfig: SSEHandlerAuthConfig | undefined,\n): Promise<{ principal: unknown } | null>\n{\n if (!subject || !authConfig?.resolvePrincipal)\n {\n return { principal: undefined };\n }\n\n try\n {\n return { principal: await authConfig.resolvePrincipal(subject, request) };\n }\n catch\n {\n sseLogger.warn('SSE principal could not be resolved — connection refused', { subject });\n\n return null;\n }\n}\n\n/**\n * Ask each authorized event's `canSubscribe`, in parallel; returns the events\n * it allows. An event without a predicate is allowed.\n */\nasync function checkCanSubscribe(\n router: EventRouterDef<any>,\n events: string[],\n ctx: SubscribeContext,\n): Promise<string[]>\n{\n const verdicts = await Promise.all(events.map(eventName => askCanSubscribe(eventName, router.events[eventName], ctx)));\n\n return events.filter((_, i) => verdicts[i]);\n}\n\n/**\n * `true` → allowed; `false` or a throw → denied. A throw is warned with the\n * event name and never fails the connection.\n */\nasync function askCanSubscribe(\n eventName: string,\n eventDef: EventDef<any> | undefined,\n ctx: SubscribeContext,\n): Promise<boolean>\n{\n if (!eventDef)\n {\n return false;\n }\n\n if (!eventDef._canSubscribe)\n {\n return true;\n }\n\n try\n {\n return await eventDef._canSubscribe(ctx) === true;\n }\n catch\n {\n sseLogger.warn('SSE canSubscribe threw — event denied on this connection', {\n event: eventName,\n subject: ctx.subject,\n });\n\n return false;\n }\n}\n\n// ============================================================================\n// Stream\n// ============================================================================\n\n/**\n * The life of one open connection: connected frame, subscriptions, registry\n * entry, keep-alive, and teardown on disconnect or a server-side close.\n */\nasync function runStream(\n c: Context,\n stream: SSEStreamingApi,\n admission: Admission,\n config: StreamConfig,\n): Promise<void>\n{\n const { allowed, denied, subject, request } = admission;\n let connectionDead = false;\n let pingTimer: ReturnType<typeof setInterval> | undefined;\n // Resolves the disconnect wait below when the connection is closed\n // server-side (overflow/error), so teardown doesn't wait for the loop.\n let onClosed: (() => void) | undefined;\n\n const cleanup = (reason?: string) =>\n {\n if (connectionDead) return;\n connectionDead = true;\n clearInterval(pingTimer);\n unsubscribeAll(connection);\n config.registry.remove(connection);\n writer.close();\n onClosed?.();\n sseLogger.info('SSE dead connection cleaned up', {\n events: [...connection.allowed],\n reason,\n });\n };\n\n // All writes go through one bounded, backpressure-aware drain loop so a\n // slow client can't pile frames up in memory (it's closed on overflow).\n const writer = createBoundedWriter(stream, config.maxQueue, (reason) =>\n {\n sseLogger.warn('SSE connection closed', { events: [...connection.allowed], reason });\n cleanup(reason);\n });\n\n const connection: SSEConnection = {\n subject,\n requested: admission.requested,\n allowed: new Set(allowed),\n denied: new Set(denied),\n unsubscribes: new Map(),\n request,\n subscribe: createEventSubscriber({\n router: config.router,\n authConfig: config.authConfig,\n subject,\n request,\n writer,\n isDead: () => connectionDead,\n }),\n notify: (frame, eventName) =>\n {\n if (connectionDead) return;\n\n // A small literal — not routed through serializeFrame's per-payload cache.\n writer.enqueue({ event: frame, data: JSON.stringify({ event: eventName }) });\n },\n };\n\n // Synchronously before the `connected` frame: the changes recorded during\n // admission land in `allowed` / `denied` first, and the entry turns open.\n // A cancelled admission (the client left) opens nothing and sends no frame.\n // Defensive: unreachable today, since `streamSSE` runs this synchronously up\n // to here; the handler's `signal.aborted` → 499 check is the real path.\n if (admission.pending?.open(connection) === false)\n {\n cleanup('aborted during admission');\n\n return;\n }\n\n openSubscriptions(connection, writer);\n\n // Keep-alive ping\n pingTimer = setInterval(() =>\n {\n if (connectionDead) return;\n\n writer.enqueue({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n });\n }, config.pingInterval);\n\n await waitForDisconnect(c.req.raw.signal, () => connectionDead, (resolve) =>\n {\n onClosed = resolve;\n });\n\n // Cleanup (normal disconnect path; idempotent if already closed)\n cleanup();\n}\n\n/**\n * Send the `connected` frame (first in the queue) and subscribe every allowed event.\n */\nfunction openSubscriptions(connection: SSEConnection, writer: BoundedWriter): void\n{\n writer.enqueue({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: [...connection.allowed],\n deniedEvents: [...connection.denied],\n timestamp: Date.now(),\n }),\n });\n\n for (const eventName of connection.allowed)\n {\n connection.unsubscribes.set(eventName, connection.subscribe(eventName));\n }\n\n sseLogger.info('SSE connection established', {\n events: [...connection.allowed],\n deniedEvents: [...connection.denied],\n subscriptionCount: connection.unsubscribes.size,\n });\n}\n\n/**\n * Wait for client disconnect (abort) or a server-side close. Waiting on the\n * abort event instead of polling sleep(pingInterval) means a dropped client is\n * torn down immediately, not up to one ping interval later.\n */\nasync function waitForDisconnect(\n abortSignal: AbortSignal,\n isDead: () => boolean,\n onServerClose: (resolve: () => void) => void,\n): Promise<void>\n{\n if (abortSignal.aborted || isDead())\n {\n return;\n }\n\n await new Promise<void>((resolve) =>\n {\n const onAbort = () => resolve();\n abortSignal.addEventListener('abort', onAbort, { once: true });\n // Server-side close (cleanup) resolves the wait and drops the listener.\n onServerClose(() =>\n {\n abortSignal.removeEventListener('abort', onAbort);\n resolve();\n });\n });\n}\n\n/** What the per-event subscribe of one connection closes over. */\ninterface SubscriberContext\n{\n router: EventRouterDef<any>;\n authConfig: SSEHandlerAuthConfig | undefined;\n subject: string | undefined;\n request: SubscribeRequest;\n writer: BoundedWriter;\n isDead: () => boolean;\n}\n\n/**\n * The per-event subscribe of one connection, used at connect and by `grant`\n * alike, so filter order and `messageId` numbering are one code path.\n *\n * The returned unsubscribe also deactivates the callback, so a frame of an\n * `emit` already fanning out when `revoke` lands is not enqueued after it.\n */\nfunction createEventSubscriber(ctx: SubscriberContext): (eventName: string) => () => void\n{\n let messageId = 0;\n\n return (eventName: string) =>\n {\n const eventDef = ctx.router.events[eventName];\n let active = true;\n\n const unsubscribe = eventDef.subscribe((payload: unknown) =>\n {\n if (!active || ctx.isDead() || !passesFrameFilters(eventName, eventDef, payload, ctx))\n {\n return;\n }\n\n messageId++;\n\n sseLogger.debug('SSE sending event', {\n event: eventName,\n messageId,\n });\n\n ctx.writer.enqueue({\n id: String(messageId),\n event: eventName,\n data: serializeFrame(eventName, payload),\n });\n });\n\n return () =>\n {\n active = false;\n unsubscribe();\n };\n };\n}\n\n/**\n * The global `filter` map (with a subject, as before) and the event's own\n * `.filter()` — both must pass.\n */\nfunction passesFrameFilters(\n eventName: string,\n eventDef: EventDef<any>,\n payload: unknown,\n ctx: SubscriberContext,\n): boolean\n{\n const globalFilter = ctx.authConfig?.filter?.[eventName];\n if (ctx.subject && globalFilter && !globalFilter(ctx.subject, payload))\n {\n return false;\n }\n\n if (!eventDef._filter)\n {\n return true;\n }\n\n try\n {\n return eventDef._filter(payload, { subject: ctx.subject, request: ctx.request }) === true;\n }\n catch\n {\n sseLogger.warn('SSE event filter threw — frame dropped', { event: eventName, subject: ctx.subject });\n\n return false;\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 // Deduplicated: one subscription per event, keyed by name in the registry.\n return [...new Set(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, createHash } from 'crypto';\n\n/**\n * Hash a token for storage. The raw token is a bearer secret — storing it at rest\n * (e.g. as a Redis key/value) means a store dump hands out live tokens. We persist\n * only sha256(token) and look up by the same hash; the raw token exists only in\n * transit and in the issue() return value.\n */\nfunction hashToken(token: string): string\n{\n return createHash('sha256').update(token).digest('hex');\n}\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 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(hashToken(token), data);\n }\n\n async consume(token: string): Promise<SSEToken | null>\n {\n const key = hashToken(token);\n const data = this.tokens.get(key);\n if (!data)\n {\n return null;\n }\n\n this.tokens.delete(key);\n\n return data;\n }\n\n async cleanup(): Promise<void>\n {\n const now = Date.now();\n\n for (const [key, data] of this.tokens)\n {\n if (data.expiresAt <= now)\n {\n this.tokens.delete(key);\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\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 + hashToken(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 + hashToken(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 * How long an issued token stays valid, in milliseconds.\n *\n * Exposed so a caller that has to report an expiry to its client computes the same\n * number this manager stamps on the token, instead of assuming the default.\n */\n get ttlMs(): number\n {\n return this.ttl;\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 // The store keys by sha256(token); the raw token is never persisted.\n await this.store.set(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,6 +1,6 @@
|
|
|
1
|
-
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-
|
|
1
|
+
import { W as WSRouterDef, a as WSSubscribeOptions, b as WSUnsubscribe, c as WSConnectionState, d as WSClientConfig } from '../../types-CqTsDETV.js';
|
|
2
2
|
import 'hono';
|
|
3
|
-
import '../../router-
|
|
3
|
+
import '../../router-D5B0cpVN.js';
|
|
4
4
|
import '@sinclair/typebox';
|
|
5
5
|
import '../../token-manager-Ckjt2-qq.js';
|
|
6
6
|
|
package/dist/event/ws/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as EventDef } from '../../router-
|
|
2
|
-
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-
|
|
3
|
-
export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-
|
|
1
|
+
import { a as EventDef } from '../../router-D5B0cpVN.js';
|
|
2
|
+
import { e as WSMessageHandlers, W as WSRouterDef, f as WSHandlerConfig } from '../../types-CqTsDETV.js';
|
|
3
|
+
export { g as WSAuthConfig, d as WSClientConfig, c as WSConnectionState, h as WSEventHandlers, i as WSHandlerAuthConfig, j as WSMessageContext, k as WSMessageHandlerFn, l as WSRawConnection, a as WSSubscribeOptions, b as WSUnsubscribe } from '../../types-CqTsDETV.js';
|
|
4
4
|
import { Server } from 'node:http';
|
|
5
5
|
import { S as SSETokenManager } from '../../token-manager-Ckjt2-qq.js';
|
|
6
6
|
import '@sinclair/typebox';
|
|
@@ -21,6 +21,18 @@ interface PubSubCache {
|
|
|
21
21
|
* Event handler function type
|
|
22
22
|
*/
|
|
23
23
|
type EventHandler<TPayload> = (payload: TPayload) => void | Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Options for `EventDef.subscribe`.
|
|
26
|
+
*/
|
|
27
|
+
interface EventSubscribeOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Run only for an `emit` made in this process, never for a delivery the
|
|
30
|
+
* cache transport received from another pod. For a handler whose work must
|
|
31
|
+
* happen once per emit, not once per pod (it would otherwise run on every
|
|
32
|
+
* pod the event is cached to). Without a cache it runs on every `emit`.
|
|
33
|
+
*/
|
|
34
|
+
localOnly?: boolean;
|
|
35
|
+
}
|
|
24
36
|
/**
|
|
25
37
|
* Job queue sender function type (used by job module)
|
|
26
38
|
*/
|
|
@@ -87,15 +99,24 @@ interface FrameContext {
|
|
|
87
99
|
* the connection asked for and never reads storage.
|
|
88
100
|
*/
|
|
89
101
|
type FramePredicate<TPayload> = (payload: TPayload, ctx: FrameContext) => boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Which of a subject's connections a `grant` / `revoke` reaches: those whose
|
|
104
|
+
* connect query carries every named field with exactly that value. A
|
|
105
|
+
* connection that declared no such field never matches. Plain JSON, so it
|
|
106
|
+
* travels to other pods unchanged.
|
|
107
|
+
*/
|
|
108
|
+
interface SubscriptionMatch {
|
|
109
|
+
query: Record<string, string>;
|
|
110
|
+
}
|
|
90
111
|
/**
|
|
91
112
|
* How an SSE handler lets `grant` / `revoke` reach its open connections.
|
|
92
113
|
* Each handler registers one per router key of the event.
|
|
93
114
|
*/
|
|
94
115
|
interface SubscriptionControl {
|
|
95
|
-
/** Activate the event on every open connection of `subject` where it is denied. */
|
|
96
|
-
grant: (subject: string) => void;
|
|
97
|
-
/** Deactivate the event on every open connection of `subject` where it is allowed. */
|
|
98
|
-
revoke: (subject: string) => void;
|
|
116
|
+
/** Activate the event on every open connection of `subject` (matching `match`, when given) where it is denied. */
|
|
117
|
+
grant: (subject: string, match?: SubscriptionMatch) => void;
|
|
118
|
+
/** Deactivate the event on every open connection of `subject` (matching `match`, when given) where it is allowed. */
|
|
119
|
+
revoke: (subject: string, match?: SubscriptionMatch) => void;
|
|
99
120
|
}
|
|
100
121
|
/**
|
|
101
122
|
* Event definition interface
|
|
@@ -163,11 +184,13 @@ interface EventDef<TPayload = void> {
|
|
|
163
184
|
*/
|
|
164
185
|
filter: (predicate: FramePredicate<TPayload>) => EventDef<TPayload>;
|
|
165
186
|
/**
|
|
166
|
-
* Subscribe to this event (in-memory handler)
|
|
187
|
+
* Subscribe to this event (in-memory handler). With the cache transport a
|
|
188
|
+
* handler runs on every pod for every emit, unless `{ localOnly: true }`
|
|
189
|
+
* keeps it to emits made in this process.
|
|
167
190
|
*/
|
|
168
|
-
subscribe: (handler: EventHandler<TPayload
|
|
191
|
+
subscribe: (handler: EventHandler<TPayload>, options?: EventSubscribeOptions) => () => void;
|
|
169
192
|
/**
|
|
170
|
-
* Unsubscribe all handlers
|
|
193
|
+
* Unsubscribe all handlers, `localOnly` ones included
|
|
171
194
|
*/
|
|
172
195
|
unsubscribeAll: () => void;
|
|
173
196
|
/**
|
|
@@ -181,16 +204,22 @@ interface EventDef<TPayload = void> {
|
|
|
181
204
|
* Reaches every pod when the cache transport is attached. Never re-runs
|
|
182
205
|
* `canSubscribe`: it is an operator's statement about an open connection.
|
|
183
206
|
* An event the connection never requested cannot be granted.
|
|
207
|
+
*
|
|
208
|
+
* With `match`, only the connections whose connect query carries every
|
|
209
|
+
* field of `match.query` with that value are activated.
|
|
184
210
|
*/
|
|
185
|
-
grant: (subject: string) => Promise<void>;
|
|
211
|
+
grant: (subject: string, match?: SubscriptionMatch) => Promise<void>;
|
|
186
212
|
/**
|
|
187
213
|
* Deactivate this event on every open SSE connection of `subject` where it
|
|
188
214
|
* is allowed; the client receives a `subscription.revoked` frame.
|
|
189
215
|
*
|
|
190
216
|
* Reaches every pod when the cache transport is attached. Affects open
|
|
191
217
|
* connections only — the next connect asks `canSubscribe` afresh.
|
|
218
|
+
*
|
|
219
|
+
* With `match`, only the connections whose connect query carries every
|
|
220
|
+
* field of `match.query` with that value are deactivated.
|
|
192
221
|
*/
|
|
193
|
-
revoke: (subject: string) => Promise<void>;
|
|
222
|
+
revoke: (subject: string, match?: SubscriptionMatch) => Promise<void>;
|
|
194
223
|
/**
|
|
195
224
|
* Enable cache-based pub/sub for multi-instance support
|
|
196
225
|
* Must await before emitting events to ensure subscription is ready
|
|
@@ -346,4 +375,4 @@ type InferEventPayloads<T extends EventRouterBase<any>> = T['_types'];
|
|
|
346
375
|
*/
|
|
347
376
|
declare function defineEventRouter<TEvents extends Record<string, EventDef<any>>>(events: TEvents): EventRouterDef<TEvents>;
|
|
348
377
|
|
|
349
|
-
export { type EventRouterDef as E, type FrameContext as F, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type SubscribeContext as S, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type
|
|
378
|
+
export { type EventRouterDef as E, type FrameContext as F, type InferEventNames as I, type JobQueueSender as J, type PubSubCache as P, type SubscribeContext as S, type EventDef as a, type EventContract as b, type EventHandler as c, type EventRouterContract as d, type EventSubscribeOptions as e, type FramePredicate as f, type InferEventPayload$1 as g, type InferEventPayloads as h, type InferEventPayload as i, type SubscribePredicate as j, type SubscribeRequest as k, type SubscriptionMatch as l, defineEventRouter as m, type EventRouterBase as n };
|
package/dist/server/index.d.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { NamedMiddleware, Router } from '@spfn/core/route';
|
|
|
6
6
|
import { OnErrorContext, ProxyGuardConfig, RateLimitOptions } from '@spfn/core/middleware';
|
|
7
7
|
import { SafeFetchPolicy } from '@spfn/core/security';
|
|
8
8
|
import { c as JobRouter, f as BossOptions } from '../boss-BiDCbbAQ.js';
|
|
9
|
-
import { E as EventRouterDef, a as EventDef } from '../router-
|
|
10
|
-
import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-
|
|
11
|
-
import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-
|
|
9
|
+
import { E as EventRouterDef, a as EventDef } from '../router-D5B0cpVN.js';
|
|
10
|
+
import { d as SSEHandlerConfig, e as SSEAuthConfig } from '../types-DqXYtI69.js';
|
|
11
|
+
import { W as WSRouterDef, f as WSHandlerConfig, e as WSMessageHandlers, g as WSAuthConfig } from '../types-CqTsDETV.js';
|
|
12
12
|
import { DatabaseProvider, MigrationStatus, MigrationStatusDb } from '@spfn/core/db';
|
|
13
13
|
import * as _sinclair_typebox from '@sinclair/typebox';
|
|
14
14
|
import { Static } from '@sinclair/typebox';
|
package/dist/server/index.js
CHANGED
|
@@ -475,7 +475,7 @@ function createConnectionRegistry() {
|
|
|
475
475
|
const bySubject = /* @__PURE__ */ new Map();
|
|
476
476
|
const pendingBySubject = /* @__PURE__ */ new Map();
|
|
477
477
|
const beginAdmission = (subject, signal) => {
|
|
478
|
-
const changes =
|
|
478
|
+
const changes = [];
|
|
479
479
|
addToSet(pendingBySubject, subject, changes);
|
|
480
480
|
const end = endOnce(signal, () => removeFromSet(pendingBySubject, subject, changes));
|
|
481
481
|
return {
|
|
@@ -490,11 +490,15 @@ function createConnectionRegistry() {
|
|
|
490
490
|
}
|
|
491
491
|
};
|
|
492
492
|
};
|
|
493
|
-
const change = (kind, subject, eventName) => {
|
|
493
|
+
const change = (kind, subject, eventName, match) => {
|
|
494
494
|
const pending = pendingBySubject.get(subject);
|
|
495
495
|
const connections = bySubject.get(subject);
|
|
496
|
-
pending?.forEach((changes) => changes.
|
|
497
|
-
connections?.forEach((connection) =>
|
|
496
|
+
pending?.forEach((changes) => changes.push({ kind, eventName, match }));
|
|
497
|
+
connections?.forEach((connection) => {
|
|
498
|
+
if (matchesRequest(match, connection.request)) {
|
|
499
|
+
(kind === "grant" ? grantOn : revokeOn)(connection, eventName);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
498
502
|
if (!pending && !connections) {
|
|
499
503
|
registryLogger.debug("SSE subscription change for a subject with no open connection ignored", {
|
|
500
504
|
event: eventName,
|
|
@@ -512,8 +516,8 @@ function createConnectionRegistry() {
|
|
|
512
516
|
/** Open connections only — a pending admission is not counted. */
|
|
513
517
|
count: (subject) => bySubject.get(subject)?.size ?? 0,
|
|
514
518
|
pendingCount: (subject) => pendingBySubject.get(subject)?.size ?? 0,
|
|
515
|
-
grant: (subject, eventName) => change("grant", subject, eventName),
|
|
516
|
-
revoke: (subject, eventName) => change("revoke", subject, eventName)
|
|
519
|
+
grant: (subject, eventName, match) => change("grant", subject, eventName, match),
|
|
520
|
+
revoke: (subject, eventName, match) => change("revoke", subject, eventName, match)
|
|
517
521
|
};
|
|
518
522
|
}
|
|
519
523
|
function endOnce(signal, remove) {
|
|
@@ -545,8 +549,14 @@ function removeFromSet(map, key, value) {
|
|
|
545
549
|
map.delete(key);
|
|
546
550
|
}
|
|
547
551
|
}
|
|
552
|
+
function matchesRequest(match, request) {
|
|
553
|
+
return match === void 0 || Object.entries(match.query).every(([field, value]) => Object.hasOwn(request.query, field) && request.query[field] === value);
|
|
554
|
+
}
|
|
548
555
|
function applyRecorded(connection, changes) {
|
|
549
|
-
changes.forEach((kind, eventName) => {
|
|
556
|
+
changes.forEach(({ kind, eventName, match }) => {
|
|
557
|
+
if (!matchesRequest(match, connection.request)) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
550
560
|
const [from, to] = kind === "grant" ? [connection.denied, connection.allowed] : [connection.allowed, connection.denied];
|
|
551
561
|
if (from.delete(eventName)) {
|
|
552
562
|
to.add(eventName);
|
|
@@ -635,8 +645,8 @@ function createSSEHandler(router, config = {}, tokenManager) {
|
|
|
635
645
|
function registerSubscriptionControls(router, registry) {
|
|
636
646
|
for (const eventName of router.eventNames) {
|
|
637
647
|
router.events[eventName]._registerSubscriptionControl({
|
|
638
|
-
grant: (subject) => registry.grant(subject, eventName),
|
|
639
|
-
revoke: (subject) => registry.revoke(subject, eventName)
|
|
648
|
+
grant: (subject, match) => registry.grant(subject, eventName, match),
|
|
649
|
+
revoke: (subject, match) => registry.revoke(subject, eventName, match)
|
|
640
650
|
});
|
|
641
651
|
}
|
|
642
652
|
}
|