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.
- package/README.md +966 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/AbstractController.d.ts +3 -3
- package/dist/lib/AbstractController.js.map +1 -1
- package/dist/lib/AbstractModule.d.ts +23 -1
- package/dist/lib/AbstractModule.js +25 -0
- package/dist/lib/AbstractModule.js.map +1 -1
- package/dist/lib/DIContext.d.ts +35 -0
- package/dist/lib/DIContext.js +108 -1
- package/dist/lib/DIContext.js.map +1 -1
- package/dist/lib/resolverFunctions.d.ts +34 -0
- package/dist/lib/resolverFunctions.js +47 -0
- package/dist/lib/resolverFunctions.js.map +1 -1
- package/dist/lib/sse/AbstractSSEController.d.ts +163 -0
- package/dist/lib/sse/AbstractSSEController.js +228 -0
- package/dist/lib/sse/AbstractSSEController.js.map +1 -0
- package/dist/lib/sse/SSEConnectionSpy.d.ts +55 -0
- package/dist/lib/sse/SSEConnectionSpy.js +136 -0
- package/dist/lib/sse/SSEConnectionSpy.js.map +1 -0
- package/dist/lib/sse/index.d.ts +5 -0
- package/dist/lib/sse/index.js +6 -0
- package/dist/lib/sse/index.js.map +1 -0
- package/dist/lib/sse/sseContracts.d.ts +132 -0
- package/dist/lib/sse/sseContracts.js +102 -0
- package/dist/lib/sse/sseContracts.js.map +1 -0
- package/dist/lib/sse/sseParser.d.ts +167 -0
- package/dist/lib/sse/sseParser.js +225 -0
- package/dist/lib/sse/sseParser.js.map +1 -0
- package/dist/lib/sse/sseRouteBuilder.d.ts +47 -0
- package/dist/lib/sse/sseRouteBuilder.js +114 -0
- package/dist/lib/sse/sseRouteBuilder.js.map +1 -0
- package/dist/lib/sse/sseTypes.d.ts +164 -0
- package/dist/lib/sse/sseTypes.js +2 -0
- package/dist/lib/sse/sseTypes.js.map +1 -0
- package/dist/lib/testing/index.d.ts +5 -0
- package/dist/lib/testing/index.js +5 -0
- package/dist/lib/testing/index.js.map +1 -0
- package/dist/lib/testing/sseHttpClient.d.ts +203 -0
- package/dist/lib/testing/sseHttpClient.js +262 -0
- package/dist/lib/testing/sseHttpClient.js.map +1 -0
- package/dist/lib/testing/sseInjectClient.d.ts +173 -0
- package/dist/lib/testing/sseInjectClient.js +234 -0
- package/dist/lib/testing/sseInjectClient.js.map +1 -0
- package/dist/lib/testing/sseInjectHelpers.d.ts +59 -0
- package/dist/lib/testing/sseInjectHelpers.js +117 -0
- package/dist/lib/testing/sseInjectHelpers.js.map +1 -0
- package/dist/lib/testing/sseTestServer.d.ts +93 -0
- package/dist/lib/testing/sseTestServer.js +108 -0
- package/dist/lib/testing/sseTestServer.js.map +1 -0
- package/dist/lib/testing/sseTestTypes.d.ts +106 -0
- package/dist/lib/testing/sseTestTypes.js +2 -0
- package/dist/lib/testing/sseTestTypes.js.map +1 -0
- 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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"}
|