opinionated-machine 5.1.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +966 -2
  2. package/dist/index.d.ts +3 -2
  3. package/dist/index.js +5 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/AbstractController.d.ts +3 -3
  6. package/dist/lib/AbstractController.js.map +1 -1
  7. package/dist/lib/AbstractModule.d.ts +23 -1
  8. package/dist/lib/AbstractModule.js +25 -0
  9. package/dist/lib/AbstractModule.js.map +1 -1
  10. package/dist/lib/DIContext.d.ts +35 -0
  11. package/dist/lib/DIContext.js +108 -1
  12. package/dist/lib/DIContext.js.map +1 -1
  13. package/dist/lib/resolverFunctions.d.ts +34 -0
  14. package/dist/lib/resolverFunctions.js +47 -0
  15. package/dist/lib/resolverFunctions.js.map +1 -1
  16. package/dist/lib/sse/AbstractSSEController.d.ts +163 -0
  17. package/dist/lib/sse/AbstractSSEController.js +228 -0
  18. package/dist/lib/sse/AbstractSSEController.js.map +1 -0
  19. package/dist/lib/sse/SSEConnectionSpy.d.ts +55 -0
  20. package/dist/lib/sse/SSEConnectionSpy.js +136 -0
  21. package/dist/lib/sse/SSEConnectionSpy.js.map +1 -0
  22. package/dist/lib/sse/index.d.ts +5 -0
  23. package/dist/lib/sse/index.js +6 -0
  24. package/dist/lib/sse/index.js.map +1 -0
  25. package/dist/lib/sse/sseContracts.d.ts +132 -0
  26. package/dist/lib/sse/sseContracts.js +102 -0
  27. package/dist/lib/sse/sseContracts.js.map +1 -0
  28. package/dist/lib/sse/sseParser.d.ts +167 -0
  29. package/dist/lib/sse/sseParser.js +225 -0
  30. package/dist/lib/sse/sseParser.js.map +1 -0
  31. package/dist/lib/sse/sseRouteBuilder.d.ts +47 -0
  32. package/dist/lib/sse/sseRouteBuilder.js +114 -0
  33. package/dist/lib/sse/sseRouteBuilder.js.map +1 -0
  34. package/dist/lib/sse/sseTypes.d.ts +164 -0
  35. package/dist/lib/sse/sseTypes.js +2 -0
  36. package/dist/lib/sse/sseTypes.js.map +1 -0
  37. package/dist/lib/testing/index.d.ts +5 -0
  38. package/dist/lib/testing/index.js +5 -0
  39. package/dist/lib/testing/index.js.map +1 -0
  40. package/dist/lib/testing/sseHttpClient.d.ts +203 -0
  41. package/dist/lib/testing/sseHttpClient.js +262 -0
  42. package/dist/lib/testing/sseHttpClient.js.map +1 -0
  43. package/dist/lib/testing/sseInjectClient.d.ts +173 -0
  44. package/dist/lib/testing/sseInjectClient.js +234 -0
  45. package/dist/lib/testing/sseInjectClient.js.map +1 -0
  46. package/dist/lib/testing/sseInjectHelpers.d.ts +59 -0
  47. package/dist/lib/testing/sseInjectHelpers.js +117 -0
  48. package/dist/lib/testing/sseInjectHelpers.js.map +1 -0
  49. package/dist/lib/testing/sseTestServer.d.ts +93 -0
  50. package/dist/lib/testing/sseTestServer.js +108 -0
  51. package/dist/lib/testing/sseTestServer.js.map +1 -0
  52. package/dist/lib/testing/sseTestTypes.d.ts +106 -0
  53. package/dist/lib/testing/sseTestTypes.js +2 -0
  54. package/dist/lib/testing/sseTestTypes.js.map +1 -0
  55. package/package.json +13 -11
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Build a GET SSE route definition (traditional SSE).
3
+ *
4
+ * Use this for long-lived connections where the client subscribes
5
+ * to receive events over time (e.g., notifications, real-time updates).
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const notificationsStream = buildSSERoute({
10
+ * path: '/api/notifications/stream',
11
+ * params: z.object({}),
12
+ * query: z.object({ userId: z.string().uuid() }),
13
+ * requestHeaders: z.object({ authorization: z.string() }),
14
+ * events: {
15
+ * notification: z.object({ id: z.string(), message: z.string() }),
16
+ * },
17
+ * })
18
+ * ```
19
+ */
20
+ export function buildSSERoute(config) {
21
+ return {
22
+ method: 'GET',
23
+ path: config.path,
24
+ params: config.params,
25
+ query: config.query,
26
+ requestHeaders: config.requestHeaders,
27
+ body: undefined,
28
+ events: config.events,
29
+ isSSE: true,
30
+ };
31
+ }
32
+ /**
33
+ * Build a POST/PUT/PATCH SSE route definition (OpenAI-style streaming API).
34
+ *
35
+ * Use this for request-response streaming where the client sends a request
36
+ * body and receives a stream of events in response (e.g., chat completions).
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const chatCompletionStream = buildPayloadSSERoute({
41
+ * method: 'POST',
42
+ * path: '/api/ai/chat/completions',
43
+ * params: z.object({}),
44
+ * query: z.object({}),
45
+ * requestHeaders: z.object({ authorization: z.string() }),
46
+ * body: z.object({
47
+ * model: z.string(),
48
+ * messages: z.array(z.object({ role: z.string(), content: z.string() })),
49
+ * stream: z.literal(true),
50
+ * }),
51
+ * events: {
52
+ * chunk: z.object({ content: z.string() }),
53
+ * done: z.object({ usage: z.object({ tokens: z.number() }) }),
54
+ * },
55
+ * })
56
+ * ```
57
+ */
58
+ export function buildPayloadSSERoute(config) {
59
+ return {
60
+ method: config.method ?? 'POST',
61
+ path: config.path,
62
+ params: config.params,
63
+ query: config.query,
64
+ requestHeaders: config.requestHeaders,
65
+ body: config.body,
66
+ events: config.events,
67
+ isSSE: true,
68
+ };
69
+ }
70
+ /**
71
+ * Type-inference helper for SSE handlers.
72
+ *
73
+ * Similar to `buildFastifyPayloadRoute`, this function provides automatic
74
+ * type inference for the request and connection parameters based on the contract.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * class MyController extends AbstractSSEController<{ stream: typeof streamContract }> {
79
+ * private handleStream = buildSSEHandler(
80
+ * streamContract,
81
+ * async (request, connection) => {
82
+ * // request.body is typed from contract
83
+ * // request.query is typed from contract
84
+ * const { message } = request.body
85
+ * },
86
+ * )
87
+ *
88
+ * buildSSERoutes() {
89
+ * return {
90
+ * stream: {
91
+ * contract: streamContract,
92
+ * handler: this.handleStream,
93
+ * },
94
+ * }
95
+ * }
96
+ * }
97
+ * ```
98
+ */
99
+ export function buildSSEHandler(_contract, handler) {
100
+ return handler;
101
+ }
102
+ //# sourceMappingURL=sseContracts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sseContracts.js","sourceRoot":"","sources":["../../../lib/sse/sseContracts.ts"],"names":[],"mappings":"AA0FA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAO3B,MAAmE;IAEnE,OAAO;QACL,MAAM,EAAE,KAAK;QACb,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,IAAI,EAAE,SAAS;QACf,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,IAAI;KACZ,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,oBAAoB,CAQlC,MAAgF;IAEhF,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,MAAM;QAC/B,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,cAAc,EAAE,MAAM,CAAC,cAAc;QACrC,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,KAAK,EAAE,IAAI;KACZ,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,eAAe,CAC7B,SAAmB,EACnB,OAKC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -0,0 +1,167 @@
1
+ /**
2
+ * SSE (Server-Sent Events) parsing utilities.
3
+ *
4
+ * This module provides utilities for parsing SSE event streams according
5
+ * to the W3C Server-Sent Events specification.
6
+ *
7
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
8
+ *
9
+ * @module sseParser
10
+ */
11
+ export type ParsedSSEEvent = {
12
+ /**
13
+ * Event ID for client reconnection via Last-Event-ID header.
14
+ * When the client reconnects, it can send this ID to resume from where it left off.
15
+ */
16
+ id?: string;
17
+ /**
18
+ * Event type name that maps to EventSource event listeners.
19
+ * Defaults to 'message' when not specified.
20
+ */
21
+ event?: string;
22
+ /**
23
+ * Event data payload as a string.
24
+ * For multi-line data, lines are joined with newlines.
25
+ * Typically contains JSON that should be parsed by the consumer.
26
+ */
27
+ data: string;
28
+ /**
29
+ * Reconnection delay hint in milliseconds.
30
+ * Suggests how long the client should wait before reconnecting.
31
+ */
32
+ retry?: number;
33
+ };
34
+ /**
35
+ * Parse SSE events from a complete text response.
36
+ *
37
+ * This function parses a complete SSE response body into individual events.
38
+ * SSE events are separated by blank lines, and each event can have multiple fields.
39
+ *
40
+ * **SSE Format:**
41
+ * ```
42
+ * id: event-id
43
+ * event: event-name
44
+ * data: line1
45
+ * data: line2
46
+ * retry: 3000
47
+ *
48
+ * ```
49
+ *
50
+ * **Field Rules:**
51
+ * - `id:` - Event ID for Last-Event-ID reconnection
52
+ * - `event:` - Event type (defaults to 'message')
53
+ * - `data:` - Event payload (multiple data lines are joined with newlines)
54
+ * - `retry:` - Reconnection delay in milliseconds
55
+ * - Lines starting with `:` are comments and ignored
56
+ *
57
+ * @param text - Raw SSE text to parse
58
+ * @returns Array of parsed events
59
+ *
60
+ * @example
61
+ * ```typescript
62
+ * // Parse a simple SSE response
63
+ * const text = `event: message
64
+ * data: {"text":"hello"}
65
+ *
66
+ * event: done
67
+ * data: {"status":"complete"}
68
+ *
69
+ * `
70
+ * const events = parseSSEEvents(text)
71
+ * // events = [
72
+ * // { event: 'message', data: '{"text":"hello"}' },
73
+ * // { event: 'done', data: '{"status":"complete"}' }
74
+ * // ]
75
+ * ```
76
+ *
77
+ * @example
78
+ * ```typescript
79
+ * // Parse events with IDs (for reconnection)
80
+ * const text = `id: 1
81
+ * event: update
82
+ * data: {"value":42}
83
+ *
84
+ * id: 2
85
+ * event: update
86
+ * data: {"value":43}
87
+ *
88
+ * `
89
+ * const events = parseSSEEvents(text)
90
+ * // Store last ID for reconnection: events[events.length - 1].id
91
+ * ```
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // Multi-line data
96
+ * const text = `event: log
97
+ * data: Line 1
98
+ * data: Line 2
99
+ * data: Line 3
100
+ *
101
+ * `
102
+ * const events = parseSSEEvents(text)
103
+ * // events[0].data === "Line 1\nLine 2\nLine 3"
104
+ * ```
105
+ */
106
+ export declare function parseSSEEvents(text: string): ParsedSSEEvent[];
107
+ /**
108
+ * Result of incremental SSE buffer parsing.
109
+ */
110
+ export type ParseSSEBufferResult = {
111
+ /** Complete events parsed from the buffer */
112
+ events: ParsedSSEEvent[];
113
+ /** Remaining incomplete data to prepend to next chunk */
114
+ remaining: string;
115
+ };
116
+ /**
117
+ * Parse SSE events incrementally from a buffer.
118
+ *
119
+ * This function is designed for streaming scenarios where data arrives
120
+ * in chunks. It parses complete events and returns any incomplete data
121
+ * that should be prepended to the next chunk.
122
+ *
123
+ * **Usage Pattern:**
124
+ * 1. Append new chunk to buffer
125
+ * 2. Call parseSSEBuffer(buffer)
126
+ * 3. Process the events
127
+ * 4. Set buffer = remaining for next iteration
128
+ *
129
+ * @param buffer - Current buffer containing SSE data
130
+ * @returns Object with parsed events and remaining incomplete buffer
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * // Streaming SSE parsing with fetch
135
+ * const response = await fetch(url)
136
+ * const reader = response.body.getReader()
137
+ * const decoder = new TextDecoder()
138
+ * let buffer = ''
139
+ *
140
+ * while (true) {
141
+ * const { done, value } = await reader.read()
142
+ * if (done) break
143
+ *
144
+ * buffer += decoder.decode(value, { stream: true })
145
+ * const { events, remaining } = parseSSEBuffer(buffer)
146
+ * buffer = remaining
147
+ *
148
+ * for (const event of events) {
149
+ * console.log('Received:', event.event, JSON.parse(event.data))
150
+ * }
151
+ * }
152
+ * ```
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * // Node.js readable stream
157
+ * let buffer = ''
158
+ * stream.on('data', (chunk: Buffer) => {
159
+ * buffer += chunk.toString()
160
+ * const { events, remaining } = parseSSEBuffer(buffer)
161
+ * buffer = remaining
162
+ *
163
+ * events.forEach(event => emit('sse-event', event))
164
+ * })
165
+ * ```
166
+ */
167
+ export declare function parseSSEBuffer(buffer: string): ParseSSEBufferResult;
@@ -0,0 +1,225 @@
1
+ /**
2
+ * SSE (Server-Sent Events) parsing utilities.
3
+ *
4
+ * This module provides utilities for parsing SSE event streams according
5
+ * to the W3C Server-Sent Events specification.
6
+ *
7
+ * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
8
+ *
9
+ * @module sseParser
10
+ */
11
+ /**
12
+ * A parsed SSE event.
13
+ *
14
+ * SSE events consist of optional id, event type, data, and retry fields.
15
+ * The data field is always present and contains the event payload as a string.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const event: ParsedSSEEvent = {
20
+ * id: 'msg-123',
21
+ * event: 'message',
22
+ * data: '{"text":"Hello, world!"}',
23
+ * retry: 3000,
24
+ * }
25
+ *
26
+ * // Parse the JSON data
27
+ * const payload = JSON.parse(event.data)
28
+ * ```
29
+ */
30
+ /**
31
+ * Parse a single SSE line and update the event state.
32
+ * Returns true if a complete event was found (empty line with data).
33
+ */
34
+ function parseSSELine(line, currentEvent, dataLines) {
35
+ if (line.startsWith('id:')) {
36
+ currentEvent.id = line.slice(3).trim();
37
+ }
38
+ else if (line.startsWith('event:')) {
39
+ currentEvent.event = line.slice(6).trim();
40
+ }
41
+ else if (line.startsWith('data:')) {
42
+ dataLines.push(line.slice(5).trim());
43
+ }
44
+ else if (line.startsWith('retry:')) {
45
+ currentEvent.retry = Number.parseInt(line.slice(6).trim(), 10);
46
+ }
47
+ else if (line === '' && dataLines.length > 0) {
48
+ return true; // Event complete
49
+ }
50
+ // Comment lines (starting with :) are implicitly ignored
51
+ return false;
52
+ }
53
+ /**
54
+ * Parse SSE events from a complete text response.
55
+ *
56
+ * This function parses a complete SSE response body into individual events.
57
+ * SSE events are separated by blank lines, and each event can have multiple fields.
58
+ *
59
+ * **SSE Format:**
60
+ * ```
61
+ * id: event-id
62
+ * event: event-name
63
+ * data: line1
64
+ * data: line2
65
+ * retry: 3000
66
+ *
67
+ * ```
68
+ *
69
+ * **Field Rules:**
70
+ * - `id:` - Event ID for Last-Event-ID reconnection
71
+ * - `event:` - Event type (defaults to 'message')
72
+ * - `data:` - Event payload (multiple data lines are joined with newlines)
73
+ * - `retry:` - Reconnection delay in milliseconds
74
+ * - Lines starting with `:` are comments and ignored
75
+ *
76
+ * @param text - Raw SSE text to parse
77
+ * @returns Array of parsed events
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // Parse a simple SSE response
82
+ * const text = `event: message
83
+ * data: {"text":"hello"}
84
+ *
85
+ * event: done
86
+ * data: {"status":"complete"}
87
+ *
88
+ * `
89
+ * const events = parseSSEEvents(text)
90
+ * // events = [
91
+ * // { event: 'message', data: '{"text":"hello"}' },
92
+ * // { event: 'done', data: '{"status":"complete"}' }
93
+ * // ]
94
+ * ```
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * // Parse events with IDs (for reconnection)
99
+ * const text = `id: 1
100
+ * event: update
101
+ * data: {"value":42}
102
+ *
103
+ * id: 2
104
+ * event: update
105
+ * data: {"value":43}
106
+ *
107
+ * `
108
+ * const events = parseSSEEvents(text)
109
+ * // Store last ID for reconnection: events[events.length - 1].id
110
+ * ```
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * // Multi-line data
115
+ * const text = `event: log
116
+ * data: Line 1
117
+ * data: Line 2
118
+ * data: Line 3
119
+ *
120
+ * `
121
+ * const events = parseSSEEvents(text)
122
+ * // events[0].data === "Line 1\nLine 2\nLine 3"
123
+ * ```
124
+ */
125
+ export function parseSSEEvents(text) {
126
+ const events = [];
127
+ const lines = text.split('\n');
128
+ let currentEvent = {};
129
+ let dataLines = [];
130
+ for (const line of lines) {
131
+ if (parseSSELine(line, currentEvent, dataLines)) {
132
+ events.push({
133
+ ...currentEvent,
134
+ data: dataLines.join('\n'),
135
+ });
136
+ currentEvent = {};
137
+ dataLines = [];
138
+ }
139
+ }
140
+ // Handle case where stream doesn't end with double newline
141
+ if (dataLines.length > 0) {
142
+ events.push({
143
+ ...currentEvent,
144
+ data: dataLines.join('\n'),
145
+ });
146
+ }
147
+ return events;
148
+ }
149
+ /**
150
+ * Parse SSE events incrementally from a buffer.
151
+ *
152
+ * This function is designed for streaming scenarios where data arrives
153
+ * in chunks. It parses complete events and returns any incomplete data
154
+ * that should be prepended to the next chunk.
155
+ *
156
+ * **Usage Pattern:**
157
+ * 1. Append new chunk to buffer
158
+ * 2. Call parseSSEBuffer(buffer)
159
+ * 3. Process the events
160
+ * 4. Set buffer = remaining for next iteration
161
+ *
162
+ * @param buffer - Current buffer containing SSE data
163
+ * @returns Object with parsed events and remaining incomplete buffer
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * // Streaming SSE parsing with fetch
168
+ * const response = await fetch(url)
169
+ * const reader = response.body.getReader()
170
+ * const decoder = new TextDecoder()
171
+ * let buffer = ''
172
+ *
173
+ * while (true) {
174
+ * const { done, value } = await reader.read()
175
+ * if (done) break
176
+ *
177
+ * buffer += decoder.decode(value, { stream: true })
178
+ * const { events, remaining } = parseSSEBuffer(buffer)
179
+ * buffer = remaining
180
+ *
181
+ * for (const event of events) {
182
+ * console.log('Received:', event.event, JSON.parse(event.data))
183
+ * }
184
+ * }
185
+ * ```
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * // Node.js readable stream
190
+ * let buffer = ''
191
+ * stream.on('data', (chunk: Buffer) => {
192
+ * buffer += chunk.toString()
193
+ * const { events, remaining } = parseSSEBuffer(buffer)
194
+ * buffer = remaining
195
+ *
196
+ * events.forEach(event => emit('sse-event', event))
197
+ * })
198
+ * ```
199
+ */
200
+ export function parseSSEBuffer(buffer) {
201
+ const events = [];
202
+ const lines = buffer.split('\n');
203
+ let currentEvent = {};
204
+ let dataLines = [];
205
+ let lastCompleteEventEnd = 0;
206
+ let currentPosition = 0;
207
+ for (const line of lines) {
208
+ currentPosition += line.length + 1; // +1 for the \n
209
+ if (parseSSELine(line, currentEvent, dataLines)) {
210
+ events.push({
211
+ ...currentEvent,
212
+ data: dataLines.join('\n'),
213
+ });
214
+ currentEvent = {};
215
+ dataLines = [];
216
+ lastCompleteEventEnd = currentPosition;
217
+ }
218
+ }
219
+ // Return remaining incomplete data
220
+ // Preserve any unconsumed content after the last complete event,
221
+ // including incomplete events with only id:/event:/retry: lines (not just data: lines)
222
+ const remaining = lastCompleteEventEnd < buffer.length ? buffer.slice(lastCompleteEventEnd) : '';
223
+ return { events, remaining };
224
+ }
225
+ //# sourceMappingURL=sseParser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sseParser.js","sourceRoot":"","sources":["../../../lib/sse/sseParser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ import type { RouteOptions } from 'fastify';
2
+ import type { AbstractSSEController } from './AbstractSSEController.ts';
3
+ import type { AnySSERouteDefinition } from './sseContracts.ts';
4
+ import type { SSEHandlerConfig } from './sseTypes.ts';
5
+ /**
6
+ * Build a Fastify route configuration for an SSE endpoint.
7
+ *
8
+ * This function creates a route that integrates with @fastify/sse
9
+ * and the AbstractSSEController connection management.
10
+ *
11
+ * @param controller - The SSE controller instance
12
+ * @param config - The SSE handler configuration
13
+ * @returns Fastify route options
14
+ */
15
+ export declare function buildFastifySSERoute<Contract extends AnySSERouteDefinition>(controller: AbstractSSEController<Record<string, AnySSERouteDefinition>>, config: SSEHandlerConfig<Contract>): RouteOptions;
16
+ /**
17
+ * Options for registering SSE routes globally.
18
+ */
19
+ export type RegisterSSERoutesOptions = {
20
+ /**
21
+ * Heartbeat interval in milliseconds.
22
+ * @default 30000
23
+ */
24
+ heartbeatInterval?: number;
25
+ /**
26
+ * Custom serializer for SSE message data.
27
+ * @default JSON.stringify
28
+ */
29
+ serializer?: (data: unknown) => string;
30
+ /**
31
+ * Global preHandler hooks applied to all SSE routes.
32
+ * Use for authentication that should apply to all SSE endpoints.
33
+ */
34
+ preHandler?: RouteOptions['preHandler'];
35
+ /**
36
+ * Rate limit configuration (requires @fastify/rate-limit to be registered).
37
+ * If @fastify/rate-limit is not registered, this config is ignored.
38
+ */
39
+ rateLimit?: {
40
+ /** Maximum number of connections */
41
+ max: number;
42
+ /** Time window for rate limiting */
43
+ timeWindow: string | number;
44
+ /** Custom key generator (e.g., for per-user limits) */
45
+ keyGenerator?: (request: unknown) => string;
46
+ };
47
+ };
@@ -0,0 +1,114 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * Send replay events from either sync or async iterables.
4
+ */
5
+ async function sendReplayEvents(sseReply, replayEvents) {
6
+ // biome-ignore lint/suspicious/noExplicitAny: checking for iterator symbols
7
+ const iterable = replayEvents;
8
+ if (typeof iterable[Symbol.asyncIterator] === 'function') {
9
+ for await (const event of replayEvents) {
10
+ await sseReply.sse.send(event);
11
+ }
12
+ }
13
+ else if (typeof iterable[Symbol.iterator] === 'function') {
14
+ for (const event of replayEvents) {
15
+ await sseReply.sse.send(event);
16
+ }
17
+ }
18
+ }
19
+ /**
20
+ * Build a Fastify route configuration for an SSE endpoint.
21
+ *
22
+ * This function creates a route that integrates with @fastify/sse
23
+ * and the AbstractSSEController connection management.
24
+ *
25
+ * @param controller - The SSE controller instance
26
+ * @param config - The SSE handler configuration
27
+ * @returns Fastify route options
28
+ */
29
+ export function buildFastifySSERoute(controller, config) {
30
+ const { contract, handler, options } = config;
31
+ const routeOptions = {
32
+ method: contract.method,
33
+ url: contract.path,
34
+ sse: true,
35
+ schema: {
36
+ params: contract.params,
37
+ querystring: contract.query,
38
+ headers: contract.requestHeaders,
39
+ ...(contract.body && { body: contract.body }),
40
+ },
41
+ handler: async (request, reply) => {
42
+ const connectionId = randomUUID();
43
+ // Create connection wrapper
44
+ const connection = {
45
+ id: connectionId,
46
+ request,
47
+ reply,
48
+ context: {},
49
+ connectedAt: new Date(),
50
+ };
51
+ // Create a promise that will resolve when the client disconnects
52
+ // Using request.socket.on('close') as per @fastify/sse documentation
53
+ const connectionClosed = new Promise((resolve) => {
54
+ request.socket.on('close', async () => {
55
+ try {
56
+ await options?.onDisconnect?.(connection);
57
+ }
58
+ catch (err) {
59
+ // Log the error but don't let it prevent cleanup
60
+ options?.logger?.error({ err }, 'Error in SSE onDisconnect handler');
61
+ }
62
+ finally {
63
+ // Always unregister the connection and resolve, even if onDisconnect throws
64
+ controller.unregisterConnection(connectionId);
65
+ resolve();
66
+ }
67
+ });
68
+ });
69
+ // Register connection with controller
70
+ controller.registerConnection(connection);
71
+ // Tell @fastify/sse to keep the connection open after handler returns
72
+ // Without this, the plugin closes the connection immediately
73
+ const sseReply = reply;
74
+ sseReply.sse.keepAlive();
75
+ // Send headers + an SSE comment to establish the stream
76
+ // Without sending initial data, the client may not receive headers properly
77
+ sseReply.sse.sendHeaders();
78
+ // SSE comments (lines starting with :) are ignored by clients but establish the stream
79
+ reply.raw.write(': connected\n\n');
80
+ // Handle reconnection with Last-Event-ID
81
+ const lastEventId = request.headers['last-event-id'];
82
+ if (lastEventId && options?.onReconnect) {
83
+ try {
84
+ const replayEvents = await options.onReconnect(connection, lastEventId);
85
+ if (replayEvents) {
86
+ await sendReplayEvents(sseReply, replayEvents);
87
+ }
88
+ }
89
+ catch (err) {
90
+ options?.logger?.error({ err, lastEventId }, 'Error in SSE onReconnect handler');
91
+ }
92
+ }
93
+ // Notify connection established
94
+ try {
95
+ await options?.onConnect?.(connection);
96
+ }
97
+ catch (err) {
98
+ options?.logger?.error({ err }, 'Error in SSE onConnect handler');
99
+ }
100
+ // Call user handler for setup (subscriptions, initial data, etc.)
101
+ // biome-ignore lint/suspicious/noExplicitAny: Handler types are validated by SSEHandlerConfig
102
+ await handler(request, connection);
103
+ // Block the handler until the connection closes
104
+ // This prevents Fastify from ending the response prematurely
105
+ await connectionClosed;
106
+ },
107
+ };
108
+ // Add preHandler hooks for authentication
109
+ if (options?.preHandler) {
110
+ routeOptions.preHandler = options.preHandler;
111
+ }
112
+ return routeOptions;
113
+ }
114
+ //# sourceMappingURL=sseRouteBuilder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sseRouteBuilder.js","sourceRoot":"","sources":["../../../lib/sse/sseRouteBuilder.ts"],"names":[],"mappings":"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"}