@spfn/core 0.1.0-alpha.88 → 0.2.0-beta.10
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 +298 -466
- package/dist/boss-DI1r4kTS.d.ts +244 -0
- package/dist/cache/index.d.ts +13 -33
- package/dist/cache/index.js +14 -703
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +214 -17
- package/dist/codegen/index.js +231 -1420
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +1227 -0
- package/dist/config/index.js +273 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +741 -59
- package/dist/db/index.js +1063 -1226
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +658 -308
- package/dist/env/index.js +503 -928
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +87 -0
- package/dist/env/loader.js +70 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/errors/index.d.ts +417 -29
- package/dist/errors/index.js +359 -98
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +41 -0
- package/dist/event/index.js +131 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/sse/client.d.ts +82 -0
- package/dist/event/sse/client.js +115 -0
- package/dist/event/sse/client.js.map +1 -0
- package/dist/event/sse/index.d.ts +40 -0
- package/dist/event/sse/index.js +92 -0
- package/dist/event/sse/index.js.map +1 -0
- package/dist/job/index.d.ts +218 -0
- package/dist/job/index.js +410 -0
- package/dist/job/index.js.map +1 -0
- package/dist/logger/index.d.ts +20 -79
- package/dist/logger/index.js +82 -387
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +102 -20
- package/dist/middleware/index.js +51 -705
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +120 -0
- package/dist/nextjs/index.js +448 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +335 -262
- package/dist/nextjs/server.js +637 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/route/index.d.ts +879 -25
- package/dist/route/index.js +697 -1271
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +9 -0
- package/dist/route/types.js +3 -0
- package/dist/route/types.js.map +1 -0
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +345 -64
- package/dist/server/index.js +1174 -3233
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/types-BGl4QL1w.d.ts +77 -0
- package/dist/types-BOPTApC2.d.ts +245 -0
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +346 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +477 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +116 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +241 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +307 -0
- package/package.json +68 -48
- package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
- package/dist/client/index.d.ts +0 -358
- package/dist/client/index.js +0 -357
- package/dist/client/index.js.map +0 -1
- package/dist/client/nextjs/index.js +0 -371
- package/dist/client/nextjs/index.js.map +0 -1
- package/dist/codegen/generators/index.d.ts +0 -19
- package/dist/codegen/generators/index.js +0 -1404
- package/dist/codegen/generators/index.js.map +0 -1
- package/dist/database-errors-BNNmLTJE.d.ts +0 -86
- package/dist/events/index.d.ts +0 -183
- package/dist/events/index.js +0 -77
- package/dist/events/index.js.map +0 -1
- package/dist/index-DHiAqhKv.d.ts +0 -101
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -3674
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -121
- package/dist/types/index.js +0 -38
- package/dist/types/index.js.map +0 -1
- package/dist/types-BXibIEyj.d.ts +0 -60
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { TSchema, Static } from '@sinclair/typebox';
|
|
2
|
+
import { a as EventDef } from '../router-Di7ENoah.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 '../router-Di7ENoah.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Event System
|
|
7
|
+
*
|
|
8
|
+
* Decoupled pub/sub event system with optional cache integration for multi-instance support
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* // Define event
|
|
13
|
+
* const userCreated = defineEvent('user.created', Type.Object({
|
|
14
|
+
* userId: Type.String(),
|
|
15
|
+
* }));
|
|
16
|
+
*
|
|
17
|
+
* // Subscribe (in-memory)
|
|
18
|
+
* userCreated.subscribe((payload) => {
|
|
19
|
+
* console.log('User created:', payload.userId);
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Emit
|
|
23
|
+
* await userCreated.emit({ userId: '123' });
|
|
24
|
+
*
|
|
25
|
+
* // With cache for multi-instance
|
|
26
|
+
* const event = defineEvent('user.created', schema);
|
|
27
|
+
* await event.useCache(cache); // Must await before emitting
|
|
28
|
+
* await event.emit({ userId: '123' }); // Broadcast to all instances
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Define an event without payload
|
|
34
|
+
*/
|
|
35
|
+
declare function defineEvent(name: string): EventDef<void>;
|
|
36
|
+
/**
|
|
37
|
+
* Define an event with typed payload
|
|
38
|
+
*/
|
|
39
|
+
declare function defineEvent<T extends TSchema>(name: string, schema: T): EventDef<Static<T>>;
|
|
40
|
+
|
|
41
|
+
export { EventDef, defineEvent };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { logger } from '@spfn/core/logger';
|
|
2
|
+
|
|
3
|
+
// src/event/event.ts
|
|
4
|
+
var eventLogger = logger.child("@spfn/core:event");
|
|
5
|
+
function logHandlerError(eventName, error) {
|
|
6
|
+
eventLogger.error(`Event handler error: ${eventName}`, {
|
|
7
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function logJobQueueError(queueName, error) {
|
|
11
|
+
eventLogger.error(`Failed to send event to job queue: ${queueName}`, {
|
|
12
|
+
error: error instanceof Error ? error.message : String(error)
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function createHandlerManager(name) {
|
|
16
|
+
const handlers = /* @__PURE__ */ new Set();
|
|
17
|
+
return {
|
|
18
|
+
add: (handler) => {
|
|
19
|
+
handlers.add(handler);
|
|
20
|
+
eventLogger.debug(`Subscribed to event: ${name}`, { handlerCount: handlers.size });
|
|
21
|
+
return () => {
|
|
22
|
+
handlers.delete(handler);
|
|
23
|
+
eventLogger.debug(`Unsubscribed from event: ${name}`, { handlerCount: handlers.size });
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
clear: () => {
|
|
27
|
+
handlers.clear();
|
|
28
|
+
eventLogger.debug(`Unsubscribed all from event: ${name}`);
|
|
29
|
+
},
|
|
30
|
+
trigger: async (payload) => {
|
|
31
|
+
const results = await Promise.allSettled(
|
|
32
|
+
[...handlers].map((handler) => handler(payload))
|
|
33
|
+
);
|
|
34
|
+
for (const result of results) {
|
|
35
|
+
if (result.status === "rejected") {
|
|
36
|
+
logHandlerError(name, result.reason);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function createJobQueueManager(name) {
|
|
43
|
+
const jobQueues = /* @__PURE__ */ new Map();
|
|
44
|
+
return {
|
|
45
|
+
register: (queueName, sender) => {
|
|
46
|
+
jobQueues.set(queueName, sender);
|
|
47
|
+
eventLogger.debug(`Registered job queue for event: ${name}`, { queueName });
|
|
48
|
+
},
|
|
49
|
+
send: async (payload) => {
|
|
50
|
+
if (jobQueues.size === 0) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const entries = [...jobQueues.entries()];
|
|
54
|
+
const results = await Promise.allSettled(
|
|
55
|
+
entries.map(([queueName, sender]) => sender(queueName, payload))
|
|
56
|
+
);
|
|
57
|
+
for (const [i, result] of results.entries()) {
|
|
58
|
+
if (result.status === "rejected") {
|
|
59
|
+
logJobQueueError(entries[i][0], result.reason);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
get size() {
|
|
64
|
+
return jobQueues.size;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function createEventImpl(name, schema) {
|
|
69
|
+
const handlerManager = createHandlerManager(name);
|
|
70
|
+
const jobQueueManager = createJobQueueManager(name);
|
|
71
|
+
let cache;
|
|
72
|
+
let cacheSubscribed = false;
|
|
73
|
+
const emit = async (payload) => {
|
|
74
|
+
eventLogger.debug(`Emitting event: ${name}`, {
|
|
75
|
+
payload,
|
|
76
|
+
hasCache: !!cache,
|
|
77
|
+
jobQueueCount: jobQueueManager.size
|
|
78
|
+
});
|
|
79
|
+
if (cache) {
|
|
80
|
+
await cache.publish(name, payload);
|
|
81
|
+
} else {
|
|
82
|
+
await handlerManager.trigger(payload);
|
|
83
|
+
}
|
|
84
|
+
await jobQueueManager.send(payload);
|
|
85
|
+
eventLogger.debug(`Event emitted: ${name}`);
|
|
86
|
+
};
|
|
87
|
+
const useCache = async (newCache) => {
|
|
88
|
+
if (cacheSubscribed) {
|
|
89
|
+
eventLogger.warn(`Cache already configured for event: ${name}`);
|
|
90
|
+
return self;
|
|
91
|
+
}
|
|
92
|
+
cache = newCache;
|
|
93
|
+
cacheSubscribed = true;
|
|
94
|
+
await newCache.subscribe(name, async (message) => {
|
|
95
|
+
eventLogger.debug(`Received event from cache: ${name}`);
|
|
96
|
+
await handlerManager.trigger(message);
|
|
97
|
+
});
|
|
98
|
+
eventLogger.debug(`Cache subscription ready for event: ${name}`);
|
|
99
|
+
return self;
|
|
100
|
+
};
|
|
101
|
+
const self = {
|
|
102
|
+
name,
|
|
103
|
+
schema,
|
|
104
|
+
subscribe: handlerManager.add,
|
|
105
|
+
unsubscribeAll: handlerManager.clear,
|
|
106
|
+
emit,
|
|
107
|
+
useCache,
|
|
108
|
+
_registerJobQueue: jobQueueManager.register,
|
|
109
|
+
_payload: void 0
|
|
110
|
+
};
|
|
111
|
+
return self;
|
|
112
|
+
}
|
|
113
|
+
function defineEvent(name, schema) {
|
|
114
|
+
if (schema) {
|
|
115
|
+
return createEventImpl(name, schema);
|
|
116
|
+
}
|
|
117
|
+
return createEventImpl(name);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/event/router.ts
|
|
121
|
+
function defineEventRouter(events) {
|
|
122
|
+
return {
|
|
123
|
+
events,
|
|
124
|
+
eventNames: Object.keys(events),
|
|
125
|
+
_types: {}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export { defineEvent, defineEventRouter };
|
|
130
|
+
//# sourceMappingURL=index.js.map
|
|
131
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/event/event.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 * 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}"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { E as EventRouterDef, I as InferEventNames } from '../../router-Di7ENoah.js';
|
|
2
|
+
import { e as SSESubscribeOptions, g as SSEUnsubscribe, f as SSEConnectionState, b as SSEClientConfig } from '../../types-B-e_f2dQ.js';
|
|
3
|
+
import '@sinclair/typebox';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SSE Client
|
|
7
|
+
*
|
|
8
|
+
* Type-safe EventSource wrapper for event subscription
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { createSSEClient } from '@spfn/core/event/sse/client';
|
|
13
|
+
* import type { EventRouter } from '@/server/events';
|
|
14
|
+
*
|
|
15
|
+
* // Uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream
|
|
16
|
+
* const client = createSSEClient<EventRouter>();
|
|
17
|
+
*
|
|
18
|
+
* // Or with custom host/pathname
|
|
19
|
+
* const client = createSSEClient<EventRouter>({
|
|
20
|
+
* host: 'https://api.example.com',
|
|
21
|
+
* pathname: '/sse',
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* const unsubscribe = client.subscribe({
|
|
25
|
+
* events: ['userCreated', 'orderPlaced'],
|
|
26
|
+
* handlers: {
|
|
27
|
+
* userCreated: (payload) => console.log('User:', payload.userId),
|
|
28
|
+
* orderPlaced: (payload) => console.log('Order:', payload.orderId),
|
|
29
|
+
* },
|
|
30
|
+
* });
|
|
31
|
+
*
|
|
32
|
+
* // Later: cleanup
|
|
33
|
+
* unsubscribe();
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* SSE Client instance
|
|
39
|
+
*/
|
|
40
|
+
interface SSEClient<TRouter extends EventRouterDef<any>> {
|
|
41
|
+
/**
|
|
42
|
+
* Subscribe to events
|
|
43
|
+
*/
|
|
44
|
+
subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe;
|
|
45
|
+
/**
|
|
46
|
+
* Get current connection state
|
|
47
|
+
*/
|
|
48
|
+
getState(): SSEConnectionState;
|
|
49
|
+
/**
|
|
50
|
+
* Close all connections
|
|
51
|
+
*/
|
|
52
|
+
close(): void;
|
|
53
|
+
}
|
|
54
|
+
declare function createSSEClient<TRouter extends EventRouterDef<any>>(config?: SSEClientConfig): SSEClient<TRouter>;
|
|
55
|
+
/**
|
|
56
|
+
* Simple subscribe function for one-off subscriptions
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* import { subscribeToEvents } from '@spfn/core/event/sse/client';
|
|
61
|
+
* import type { EventRouter } from '@/server/events';
|
|
62
|
+
*
|
|
63
|
+
* // Using defaults
|
|
64
|
+
* const unsubscribe = subscribeToEvents<EventRouter>(
|
|
65
|
+
* ['userCreated', 'orderPlaced'],
|
|
66
|
+
* {
|
|
67
|
+
* userCreated: (payload) => console.log('User:', payload),
|
|
68
|
+
* orderPlaced: (payload) => console.log('Order:', payload),
|
|
69
|
+
* }
|
|
70
|
+
* );
|
|
71
|
+
*
|
|
72
|
+
* // With custom host
|
|
73
|
+
* const unsubscribe = subscribeToEvents<EventRouter>(
|
|
74
|
+
* ['userCreated'],
|
|
75
|
+
* { userCreated: (payload) => console.log('User:', payload) },
|
|
76
|
+
* { host: 'https://api.example.com' }
|
|
77
|
+
* );
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare function subscribeToEvents<TRouter extends EventRouterDef<any>>(events: InferEventNames<TRouter>[], handlers: SSESubscribeOptions<TRouter>['handlers'], options?: SSEClientConfig): SSEUnsubscribe;
|
|
81
|
+
|
|
82
|
+
export { type SSEClient, createSSEClient, subscribeToEvents };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/event/sse/client.ts
|
|
2
|
+
var SSE_DEFAULTS = {
|
|
3
|
+
host: typeof process !== "undefined" ? process.env.NEXT_PUBLIC_SPFN_API_URL || "http://localhost:8790" : "http://localhost:8790",
|
|
4
|
+
pathname: "/events/stream"
|
|
5
|
+
};
|
|
6
|
+
function createSSEClient(config = {}) {
|
|
7
|
+
const {
|
|
8
|
+
url,
|
|
9
|
+
host = SSE_DEFAULTS.host,
|
|
10
|
+
pathname = SSE_DEFAULTS.pathname,
|
|
11
|
+
reconnect = true,
|
|
12
|
+
reconnectDelay = 3e3,
|
|
13
|
+
maxReconnectAttempts = 0,
|
|
14
|
+
withCredentials = false
|
|
15
|
+
} = config;
|
|
16
|
+
const baseUrl = url || `${host}${pathname}`;
|
|
17
|
+
let eventSource = null;
|
|
18
|
+
let state = "closed";
|
|
19
|
+
let reconnectAttempts = 0;
|
|
20
|
+
let reconnectTimer = null;
|
|
21
|
+
function subscribe(options) {
|
|
22
|
+
const { events, handlers, onOpen, onError, onReconnect } = options;
|
|
23
|
+
const eventNames = events;
|
|
24
|
+
const streamUrl = `${baseUrl}?events=${eventNames.join(",")}`;
|
|
25
|
+
function connect() {
|
|
26
|
+
state = "connecting";
|
|
27
|
+
eventSource = new EventSource(streamUrl, {
|
|
28
|
+
withCredentials
|
|
29
|
+
});
|
|
30
|
+
eventSource.onopen = () => {
|
|
31
|
+
state = "open";
|
|
32
|
+
reconnectAttempts = 0;
|
|
33
|
+
onOpen?.();
|
|
34
|
+
};
|
|
35
|
+
eventSource.onerror = (error) => {
|
|
36
|
+
state = "error";
|
|
37
|
+
onError?.(error);
|
|
38
|
+
if (reconnect && eventSource?.readyState === EventSource.CLOSED) {
|
|
39
|
+
if (maxReconnectAttempts === 0 || reconnectAttempts < maxReconnectAttempts) {
|
|
40
|
+
reconnectAttempts++;
|
|
41
|
+
onReconnect?.(reconnectAttempts);
|
|
42
|
+
reconnectTimer = setTimeout(() => {
|
|
43
|
+
connect();
|
|
44
|
+
}, reconnectDelay);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
eventSource.addEventListener("connected", (e) => {
|
|
49
|
+
try {
|
|
50
|
+
const data = JSON.parse(e.data);
|
|
51
|
+
console.debug("[SSE] Connected:", data);
|
|
52
|
+
} catch {
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
eventSource.addEventListener("ping", () => {
|
|
56
|
+
});
|
|
57
|
+
for (const eventName of eventNames) {
|
|
58
|
+
const handler = handlers[eventName];
|
|
59
|
+
if (!handler) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
eventSource.addEventListener(eventName, (e) => {
|
|
63
|
+
try {
|
|
64
|
+
const message = JSON.parse(e.data);
|
|
65
|
+
handler(message.data);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(`[SSE] Failed to parse event "${eventName}":`, err);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
connect();
|
|
73
|
+
return () => {
|
|
74
|
+
if (reconnectTimer) {
|
|
75
|
+
clearTimeout(reconnectTimer);
|
|
76
|
+
reconnectTimer = null;
|
|
77
|
+
}
|
|
78
|
+
if (eventSource) {
|
|
79
|
+
eventSource.close();
|
|
80
|
+
eventSource = null;
|
|
81
|
+
}
|
|
82
|
+
state = "closed";
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function getState() {
|
|
86
|
+
return state;
|
|
87
|
+
}
|
|
88
|
+
function close() {
|
|
89
|
+
if (reconnectTimer) {
|
|
90
|
+
clearTimeout(reconnectTimer);
|
|
91
|
+
reconnectTimer = null;
|
|
92
|
+
}
|
|
93
|
+
if (eventSource) {
|
|
94
|
+
eventSource.close();
|
|
95
|
+
eventSource = null;
|
|
96
|
+
}
|
|
97
|
+
state = "closed";
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
subscribe,
|
|
101
|
+
getState,
|
|
102
|
+
close
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function subscribeToEvents(events, handlers, options) {
|
|
106
|
+
const client = createSSEClient(options);
|
|
107
|
+
return client.subscribe({
|
|
108
|
+
events,
|
|
109
|
+
handlers
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { createSSEClient, subscribeToEvents };
|
|
114
|
+
//# sourceMappingURL=client.js.map
|
|
115
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/event/sse/client.ts"],"names":[],"mappings":"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","file":"client.js","sourcesContent":["/**\n * SSE Client\n *\n * Type-safe EventSource wrapper for event subscription\n *\n * @example\n * ```typescript\n * import { createSSEClient } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Uses defaults: NEXT_PUBLIC_SPFN_API_URL + /events/stream\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom host/pathname\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * });\n *\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => console.log('User:', payload.userId),\n * orderPlaced: (payload) => console.log('Order:', payload.orderId),\n * },\n * });\n *\n * // Later: cleanup\n * unsubscribe();\n * ```\n */\n\nimport type { EventRouterDef, InferEventNames } from '../router';\nimport type {\n SSEClientConfig,\n SSESubscribeOptions,\n SSEUnsubscribe,\n SSEConnectionState,\n SSEMessage,\n} from './types';\n\n/**\n * SSE Client instance\n */\nexport interface SSEClient<TRouter extends EventRouterDef<any>>\n{\n /**\n * Subscribe to events\n */\n subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe;\n\n /**\n * Get current connection state\n */\n getState(): SSEConnectionState;\n\n /**\n * Close all connections\n */\n close(): void;\n}\n\n/**\n * Create type-safe SSE client\n *\n * @example\n * ```typescript\n * // Uses defaults (NEXT_PUBLIC_SPFN_API_URL + /events/stream)\n * const client = createSSEClient<EventRouter>();\n *\n * // Or with custom configuration\n * const client = createSSEClient<EventRouter>({\n * host: 'https://api.example.com',\n * pathname: '/sse',\n * reconnect: true,\n * reconnectDelay: 3000,\n * });\n *\n * // Subscribe to events\n * const unsubscribe = client.subscribe({\n * events: ['userCreated', 'orderPlaced'],\n * handlers: {\n * userCreated: (payload) => {\n * console.log('New user:', payload.userId);\n * },\n * orderPlaced: (payload) => {\n * console.log('New order:', payload.orderId);\n * },\n * },\n * onOpen: () => console.log('Connected'),\n * onError: (err) => console.error('Error:', err),\n * onReconnect: (attempt) => console.log('Reconnecting...', attempt),\n * });\n *\n * // Cleanup\n * unsubscribe();\n * ```\n */\n/**\n * Default SSE configuration\n */\nconst SSE_DEFAULTS = {\n host: typeof process !== 'undefined'\n ? (process.env.NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790')\n : 'http://localhost:8790',\n pathname: '/events/stream',\n} as const;\n\nexport function createSSEClient<TRouter extends EventRouterDef<any>>(\n config: SSEClientConfig = {}\n): SSEClient<TRouter>\n{\n const {\n url,\n host = SSE_DEFAULTS.host,\n pathname = SSE_DEFAULTS.pathname,\n reconnect = true,\n reconnectDelay = 3000,\n maxReconnectAttempts = 0,\n withCredentials = false,\n } = config;\n\n // Build base URL: url takes precedence, otherwise host + pathname\n const baseUrl = url || `${host}${pathname}`;\n\n let eventSource: EventSource | null = null;\n let state: SSEConnectionState = 'closed';\n let reconnectAttempts = 0;\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\n function subscribe(options: SSESubscribeOptions<TRouter>): SSEUnsubscribe\n {\n const { events, handlers, onOpen, onError, onReconnect } = options;\n\n // Build URL with events query parameter\n const eventNames = events as string[];\n const streamUrl = `${baseUrl}?events=${eventNames.join(',')}`;\n\n function connect()\n {\n state = 'connecting';\n\n eventSource = new EventSource(streamUrl, {\n withCredentials,\n });\n\n // Handle open\n eventSource.onopen = () =>\n {\n state = 'open';\n reconnectAttempts = 0;\n onOpen?.();\n };\n\n // Handle errors\n eventSource.onerror = (error) =>\n {\n state = 'error';\n onError?.(error);\n\n // Auto reconnect\n if (reconnect && eventSource?.readyState === EventSource.CLOSED)\n {\n if (maxReconnectAttempts === 0 || reconnectAttempts < maxReconnectAttempts)\n {\n reconnectAttempts++;\n onReconnect?.(reconnectAttempts);\n\n reconnectTimer = setTimeout(() =>\n {\n connect();\n }, reconnectDelay);\n }\n }\n };\n\n // Handle connected event (server sends this on connection)\n eventSource.addEventListener('connected', (e: MessageEvent) =>\n {\n try\n {\n const data = JSON.parse(e.data);\n console.debug('[SSE] Connected:', data);\n }\n catch\n {\n // Ignore parse errors\n }\n });\n\n // Handle ping (keep-alive)\n eventSource.addEventListener('ping', () =>\n {\n // Ping received, connection is alive\n });\n\n // Register handlers for each event\n for (const eventName of eventNames)\n {\n // Type assertion needed here - runtime type safety is ensured by EventRouter\n const handler = (handlers as Record<string, ((payload: unknown) => void) | undefined>)[eventName];\n\n if (!handler)\n {\n continue;\n }\n\n eventSource.addEventListener(eventName, (e: MessageEvent) =>\n {\n try\n {\n const message: SSEMessage = JSON.parse(e.data);\n handler(message.data);\n }\n catch (err)\n {\n console.error(`[SSE] Failed to parse event \"${eventName}\":`, err);\n }\n });\n }\n }\n\n // Start connection\n connect();\n\n // Return unsubscribe function\n return () =>\n {\n if (reconnectTimer)\n {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n\n if (eventSource)\n {\n eventSource.close();\n eventSource = null;\n }\n\n state = 'closed';\n };\n }\n\n function getState(): SSEConnectionState\n {\n return state;\n }\n\n function close()\n {\n if (reconnectTimer)\n {\n clearTimeout(reconnectTimer);\n reconnectTimer = null;\n }\n\n if (eventSource)\n {\n eventSource.close();\n eventSource = null;\n }\n\n state = 'closed';\n }\n\n return {\n subscribe,\n getState,\n close,\n };\n}\n\n/**\n * Simple subscribe function for one-off subscriptions\n *\n * @example\n * ```typescript\n * import { subscribeToEvents } from '@spfn/core/event/sse/client';\n * import type { EventRouter } from '@/server/events';\n *\n * // Using defaults\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated', 'orderPlaced'],\n * {\n * userCreated: (payload) => console.log('User:', payload),\n * orderPlaced: (payload) => console.log('Order:', payload),\n * }\n * );\n *\n * // With custom host\n * const unsubscribe = subscribeToEvents<EventRouter>(\n * ['userCreated'],\n * { userCreated: (payload) => console.log('User:', payload) },\n * { host: 'https://api.example.com' }\n * );\n * ```\n */\nexport function subscribeToEvents<TRouter extends EventRouterDef<any>>(\n events: InferEventNames<TRouter>[],\n handlers: SSESubscribeOptions<TRouter>['handlers'],\n options?: SSEClientConfig\n): SSEUnsubscribe\n{\n const client = createSSEClient<TRouter>(options);\n\n return client.subscribe({\n events,\n handlers,\n });\n}\n"]}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Context } from 'hono';
|
|
2
|
+
import { E as EventRouterDef } from '../../router-Di7ENoah.js';
|
|
3
|
+
import { S as SSEHandlerConfig } from '../../types-B-e_f2dQ.js';
|
|
4
|
+
export { b as SSEClientConfig, f as SSEConnectionState, c as SSEEventHandler, d as SSEEventHandlers, a as SSEMessage, e as SSESubscribeOptions, g as SSEUnsubscribe } from '../../types-B-e_f2dQ.js';
|
|
5
|
+
import '@sinclair/typebox';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SSE Handler for Hono
|
|
9
|
+
*
|
|
10
|
+
* Creates SSE stream endpoint for event subscription
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* import { Hono } from 'hono';
|
|
15
|
+
* import { createSSEHandler } from '@spfn/core/event/sse';
|
|
16
|
+
* import { eventRouter } from './events';
|
|
17
|
+
*
|
|
18
|
+
* const app = new Hono();
|
|
19
|
+
*
|
|
20
|
+
* // GET /events/stream?events=userCreated,orderPlaced
|
|
21
|
+
* app.get('/events/stream', createSSEHandler(eventRouter));
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create SSE handler for Hono
|
|
27
|
+
*
|
|
28
|
+
* Query parameters:
|
|
29
|
+
* - events: Comma-separated list of event names to subscribe
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* app.get('/events/stream', createSSEHandler(eventRouter, {
|
|
34
|
+
* pingInterval: 30000,
|
|
35
|
+
* }));
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function createSSEHandler<TRouter extends EventRouterDef<any>>(router: TRouter, config?: SSEHandlerConfig): (c: Context) => Promise<Response>;
|
|
39
|
+
|
|
40
|
+
export { SSEHandlerConfig, createSSEHandler };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { streamSSE } from 'hono/streaming';
|
|
2
|
+
import { logger } from '@spfn/core/logger';
|
|
3
|
+
|
|
4
|
+
// src/event/sse/handler.ts
|
|
5
|
+
var sseLogger = logger.child("@spfn/core:sse");
|
|
6
|
+
function createSSEHandler(router, config = {}) {
|
|
7
|
+
const {
|
|
8
|
+
pingInterval = 3e4
|
|
9
|
+
// headers: customHeaders = {}, // Reserved for future use
|
|
10
|
+
} = config;
|
|
11
|
+
return async (c) => {
|
|
12
|
+
const eventsParam = c.req.query("events");
|
|
13
|
+
if (!eventsParam) {
|
|
14
|
+
return c.json({ error: "Missing events parameter" }, 400);
|
|
15
|
+
}
|
|
16
|
+
const requestedEvents = eventsParam.split(",").map((e) => e.trim());
|
|
17
|
+
const validEventNames = router.eventNames;
|
|
18
|
+
const invalidEvents = requestedEvents.filter((e) => !validEventNames.includes(e));
|
|
19
|
+
if (invalidEvents.length > 0) {
|
|
20
|
+
return c.json({
|
|
21
|
+
error: "Invalid event names",
|
|
22
|
+
invalidEvents,
|
|
23
|
+
validEvents: validEventNames
|
|
24
|
+
}, 400);
|
|
25
|
+
}
|
|
26
|
+
sseLogger.debug("SSE connection requested", {
|
|
27
|
+
events: requestedEvents,
|
|
28
|
+
clientIp: c.req.header("x-forwarded-for") || c.req.header("x-real-ip")
|
|
29
|
+
});
|
|
30
|
+
return streamSSE(c, async (stream) => {
|
|
31
|
+
const unsubscribes = [];
|
|
32
|
+
let messageId = 0;
|
|
33
|
+
for (const eventName of requestedEvents) {
|
|
34
|
+
const eventDef = router.events[eventName];
|
|
35
|
+
if (!eventDef) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const unsubscribe = eventDef.subscribe((payload) => {
|
|
39
|
+
messageId++;
|
|
40
|
+
const message = {
|
|
41
|
+
event: eventName,
|
|
42
|
+
data: payload
|
|
43
|
+
};
|
|
44
|
+
sseLogger.debug("SSE sending event", {
|
|
45
|
+
event: eventName,
|
|
46
|
+
messageId
|
|
47
|
+
});
|
|
48
|
+
void stream.writeSSE({
|
|
49
|
+
id: String(messageId),
|
|
50
|
+
event: eventName,
|
|
51
|
+
data: JSON.stringify(message)
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
unsubscribes.push(unsubscribe);
|
|
55
|
+
}
|
|
56
|
+
sseLogger.info("SSE connection established", {
|
|
57
|
+
events: requestedEvents,
|
|
58
|
+
subscriptionCount: unsubscribes.length
|
|
59
|
+
});
|
|
60
|
+
await stream.writeSSE({
|
|
61
|
+
event: "connected",
|
|
62
|
+
data: JSON.stringify({
|
|
63
|
+
subscribedEvents: requestedEvents,
|
|
64
|
+
timestamp: Date.now()
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
const pingTimer = setInterval(() => {
|
|
68
|
+
void stream.writeSSE({
|
|
69
|
+
event: "ping",
|
|
70
|
+
data: JSON.stringify({ timestamp: Date.now() })
|
|
71
|
+
});
|
|
72
|
+
}, pingInterval);
|
|
73
|
+
const abortSignal = c.req.raw.signal;
|
|
74
|
+
while (!abortSignal.aborted) {
|
|
75
|
+
await stream.sleep(pingInterval);
|
|
76
|
+
}
|
|
77
|
+
clearInterval(pingTimer);
|
|
78
|
+
unsubscribes.forEach((fn) => fn());
|
|
79
|
+
sseLogger.info("SSE connection closed", {
|
|
80
|
+
events: requestedEvents
|
|
81
|
+
});
|
|
82
|
+
}, async (err) => {
|
|
83
|
+
sseLogger.error("SSE stream error", {
|
|
84
|
+
error: err.message
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export { createSSEHandler };
|
|
91
|
+
//# sourceMappingURL=index.js.map
|
|
92
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/event/sse/handler.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 } from './types';\n\nconst sseLogger = logger.child('@spfn/core:sse');\n\n/**\n * Create SSE handler for Hono\n *\n * Query parameters:\n * - events: Comma-separated list of event names to subscribe\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)\n{\n const {\n pingInterval = 30000,\n // headers: customHeaders = {}, // Reserved for future use\n } = config;\n\n return async (c: Context) =>\n {\n // Parse events from query parameter\n const eventsParam = c.req.query('events');\n\n if (!eventsParam)\n {\n return c.json({ error: 'Missing events parameter' }, 400);\n }\n\n const requestedEvents = eventsParam.split(',').map(e => e.trim());\n\n // 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 sseLogger.debug('SSE connection requested', {\n events: requestedEvents,\n clientIp: c.req.header('x-forwarded-for') || c.req.header('x-real-ip'),\n });\n\n // Start SSE stream\n return streamSSE(c, async (stream) =>\n {\n const unsubscribes: (() => void)[] = [];\n let messageId = 0;\n\n // Subscribe to each requested event\n for (const eventName of requestedEvents 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 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 // Fire-and-forget in sync callback\n void stream.writeSSE({\n id: String(messageId),\n event: eventName as string,\n data: JSON.stringify(message),\n });\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n sseLogger.info('SSE connection established', {\n events: requestedEvents,\n subscriptionCount: unsubscribes.length,\n });\n\n // Send initial connection message\n await stream.writeSSE({\n event: 'connected',\n data: JSON.stringify({\n subscribedEvents: requestedEvents,\n timestamp: Date.now(),\n }),\n });\n\n // Keep-alive ping\n const pingTimer = setInterval(() =>\n {\n // Fire-and-forget in sync callback\n void stream.writeSSE({\n event: 'ping',\n data: JSON.stringify({ timestamp: Date.now() }),\n });\n }, pingInterval);\n\n // Wait for client disconnect using abort signal\n const abortSignal = c.req.raw.signal;\n\n while (!abortSignal.aborted)\n {\n await stream.sleep(pingInterval);\n }\n\n // Cleanup\n clearInterval(pingTimer);\n unsubscribes.forEach(fn => fn());\n\n sseLogger.info('SSE connection closed', {\n events: requestedEvents,\n });\n }, async (err: Error) =>\n {\n sseLogger.error('SSE stream error', {\n error: err.message,\n });\n });\n };\n}\n\n"]}
|