@orkestrel/sse 0.0.1
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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/src/core/index.cjs +304 -0
- package/dist/src/core/index.cjs.map +1 -0
- package/dist/src/core/index.d.cts +250 -0
- package/dist/src/core/index.d.ts +250 -0
- package/dist/src/core/index.js +298 -0
- package/dist/src/core/index.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The byte-order mark (`U+FEFF`), stripped from the very first chunk of an SSE
|
|
3
|
+
* stream (a leading BOM on later chunks is ordinary content). Spelled as a
|
|
4
|
+
* codepoint so the wire content is unambiguous in source.
|
|
5
|
+
*/
|
|
6
|
+
export declare const BOM: string;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a Server-Sent-Events (SSE) stream parser - a stateful handle that turns
|
|
10
|
+
* string chunks into the complete events dispatched so far.
|
|
11
|
+
*
|
|
12
|
+
* @param options - See {@link SSEParserOptions}.
|
|
13
|
+
* @remarks
|
|
14
|
+
* `options.limit` caps the total buffered characters (un-consumed line buffer plus
|
|
15
|
+
* the in-progress event's field lengths); unset → unbounded, the default. Feed
|
|
16
|
+
* chunks to `parse(chunk)` as they arrive; each blank line DISPATCHES an event whose
|
|
17
|
+
* `data` is its `data:` fields joined by `\n` (plus the last `event:` / `id:` /
|
|
18
|
+
* `retry:`), and an in-progress event split across chunk boundaries is buffered until
|
|
19
|
+
* its blank line arrives (or `flush()` forces it out at end-of-stream). The `id` /
|
|
20
|
+
* `retry` getters expose the persisted last-event-id / reconnection time, which
|
|
21
|
+
* survive dispatch and only clear on `reset()`. Generic and event-free - no
|
|
22
|
+
* server / agent coupling; never throws on malformed input, only on a configured
|
|
23
|
+
* `limit` being exceeded (an {@link import('./errors.js').SSEError} with code
|
|
24
|
+
* `'OVERFLOW'`). Pair it with a `TextDecoder({ stream: true })` to also handle
|
|
25
|
+
* multi-byte UTF-8 characters split across byte reads.
|
|
26
|
+
*
|
|
27
|
+
* @returns A working {@link SSEParserInterface}
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { createSSEParser, isSSEError } from '@src/core'
|
|
32
|
+
*
|
|
33
|
+
* const parser = createSSEParser({ limit: 1_000_000 })
|
|
34
|
+
* parser.parse('data: a\ndata: b\n\n') // [{ data: 'a\nb' }] - the two data lines joined
|
|
35
|
+
* parser.parse('event: ping\ndata: 1') // [] - buffered until its blank line
|
|
36
|
+
* parser.parse('\n\n') // [{ data: '1', event: 'ping' }]
|
|
37
|
+
* try {
|
|
38
|
+
* parser.parse('x'.repeat(2_000_000))
|
|
39
|
+
* } catch (error) {
|
|
40
|
+
* if (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function createSSEParser(options?: SSEParserOptions): SSEParserInterface;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Narrow an unknown caught value to an {@link SSEError}.
|
|
48
|
+
*
|
|
49
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
50
|
+
* @returns `true` when `value` is an {@link SSEError}
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { isSSEError } from '@src/core'
|
|
55
|
+
*
|
|
56
|
+
* try {
|
|
57
|
+
* parser.parse(chunk)
|
|
58
|
+
* } catch (error) {
|
|
59
|
+
* if (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function isSSEError(value: unknown): value is SSEError;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The NUL byte (`U+0000`). The SSE spec voids an `id:` field whose value contains
|
|
67
|
+
* it, so an `id` carrying a NUL is never surfaced. Spelled as a codepoint so the
|
|
68
|
+
* wire content is unambiguous in source.
|
|
69
|
+
*/
|
|
70
|
+
export declare const NUL: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An error thrown by the SSE parser.
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* Thrown for: a `parse(chunk)` call whose resulting buffered total (un-consumed
|
|
77
|
+
* line buffer + accumulated per-event field lengths + the incoming chunk) would
|
|
78
|
+
* exceed a configured {@link import('./types.js').SSEParserOptions.limit}
|
|
79
|
+
* (`OVERFLOW`). The parser's state is left UNCHANGED by the throwing call - the
|
|
80
|
+
* chunk is not appended - so a consumer may `reset()` and continue. `context`
|
|
81
|
+
* carries at least `{ limit, size }`: the configured limit and the size the
|
|
82
|
+
* buffer would have reached.
|
|
83
|
+
*/
|
|
84
|
+
export declare class SSEError extends Error {
|
|
85
|
+
readonly code: SSEErrorCode;
|
|
86
|
+
readonly context?: Readonly<Record<string, unknown>>;
|
|
87
|
+
constructor(code: SSEErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Machine-readable codes carried by an {@link import('./errors.js').SSEError}.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* `'OVERFLOW'` - a `parse(chunk)` call would push the buffered total over a
|
|
95
|
+
* configured {@link SSEParserOptions.limit}.
|
|
96
|
+
*/
|
|
97
|
+
export declare type SSEErrorCode = 'OVERFLOW';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* One dispatched Server-Sent Event - the value a blank line flushes from an
|
|
101
|
+
* {@link SSEParserInterface}.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* - `data` is the concatenation of every `data:` field in the event, joined by a
|
|
105
|
+
* single `\n` (the SSE rule: `data: a` + `data: b` → `"a\nb"`), with NO trailing
|
|
106
|
+
* newline. An event is dispatched only when its data buffer is non-empty, so `data`
|
|
107
|
+
* is always a string (possibly empty when an explicit empty `data:` line was sent).
|
|
108
|
+
* - `event` is the last `event:` field seen before the blank line (the event type);
|
|
109
|
+
* absent when no `event:` field appeared.
|
|
110
|
+
* - `id` is the last `id:` field seen (the last-event-id); absent when none appeared.
|
|
111
|
+
* A spec NUL inside an `id` voids it, so it is never surfaced.
|
|
112
|
+
* - `retry` is the `retry:` reconnection time in milliseconds - present only when the
|
|
113
|
+
* field's value was an integer (a non-integer `retry:` is ignored).
|
|
114
|
+
*/
|
|
115
|
+
export declare interface SSEEvent {
|
|
116
|
+
/** The event's concatenated data - each `data:` field joined by `\n`, no trailing newline. */
|
|
117
|
+
readonly data: string;
|
|
118
|
+
/** The event type - the last `event:` field's value, if any. */
|
|
119
|
+
readonly event?: string;
|
|
120
|
+
/** The last-event-id - the last `id:` field's value, if any. */
|
|
121
|
+
readonly id?: string;
|
|
122
|
+
/** The reconnection time in ms - the `retry:` field, present only when it was an integer. */
|
|
123
|
+
readonly retry?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A stateful Server-Sent-Events (SSE) stream parser - feed it string chunks, get
|
|
128
|
+
* back the complete events dispatched so far.
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* - **The wire format.** SSE is a UTF-8 text stream of events separated by a blank
|
|
132
|
+
* line. Within an event each line is a `field: value` (one optional space after the
|
|
133
|
+
* colon is stripped; a line with no colon is a field with an empty value; a line
|
|
134
|
+
* STARTING with a colon is a comment and is ignored). The fields: `data` is appended
|
|
135
|
+
* to the event's data buffer - MULTIPLE `data:` lines concatenate with `\n` between
|
|
136
|
+
* them (`data: a` + `data: b` → `"a\nb"`), with no trailing newline; `event` sets the
|
|
137
|
+
* event type (last wins); `id` sets the last-event-id (last wins; an `id` containing a
|
|
138
|
+
* NUL is ignored per spec); `retry` sets the reconnection time in ms (integer only -
|
|
139
|
+
* a non-integer is ignored). Unknown fields are ignored.
|
|
140
|
+
* - **Blank-line dispatch.** A blank line flushes the accumulated event: an
|
|
141
|
+
* {@link SSEEvent} is emitted ONLY when the data buffer is non-empty (a dispatch with
|
|
142
|
+
* an empty data buffer emits nothing - a comment-only or field-only "event" produces
|
|
143
|
+
* no event), and the data buffer + event type reset for the next event afterwards.
|
|
144
|
+
* `id` / `retry` ride on the emitted event for the consumer to track per-event - the
|
|
145
|
+
* emitted event's shape is unchanged - AND are separately persisted as connection
|
|
146
|
+
* state, exposed via the sticky `id` / `retry` getters (see below).
|
|
147
|
+
* - **Sticky connection state.** Each valid `id:` field updates a persisted
|
|
148
|
+
* last-event-id, exposed via the `id` getter; each valid `retry:` field updates a
|
|
149
|
+
* persisted reconnection time, exposed via the `retry` getter. Neither is cleared
|
|
150
|
+
* when an event dispatches - only `reset()` clears them - matching WHATWG
|
|
151
|
+
* last-event-id semantics. A NUL-voided `id` field does not alter the persisted
|
|
152
|
+
* value.
|
|
153
|
+
* - **Cross-chunk reassembly.** `parse(chunk)` appends `chunk` to an internal buffer,
|
|
154
|
+
* splits on the line terminator, processes every COMPLETE line, and retains the
|
|
155
|
+
* trailing partial line (and any in-progress event) for the next call - so an event
|
|
156
|
+
* split across chunk boundaries is reassembled once its blank line arrives. An
|
|
157
|
+
* un-terminated final event stays buffered and is never emitted until its blank
|
|
158
|
+
* line arrives - call `flush()` to force it out at end-of-stream instead.
|
|
159
|
+
* - **Line endings + BOM.** `\r\n`, `\r`, and `\n` are all valid terminators and are
|
|
160
|
+
* normalized; a CRLF split across two chunks is held safely (a trailing `\r` is
|
|
161
|
+
* retained, not flushed as a line, until its `\n` is known). A leading byte-order
|
|
162
|
+
* mark on the first NON-EMPTY chunk is stripped (an empty `parse('')` call before
|
|
163
|
+
* any content does not arm the BOM check).
|
|
164
|
+
* - **Bounded buffering.** Pass {@link SSEParserOptions.limit} to cap the total
|
|
165
|
+
* buffered characters (un-consumed line buffer + in-progress event field lengths);
|
|
166
|
+
* a `parse(chunk)` call that would exceed it throws an {@link SSEError} with code
|
|
167
|
+
* `'OVERFLOW'` and leaves parser state unchanged - the chunk is not appended.
|
|
168
|
+
* - **Total + event-free.** A pure functional primitive - no Emitter, no events, no
|
|
169
|
+
* server / HTTP / agent coupling. It never throws on malformed input (a bad `retry`
|
|
170
|
+
* is ignored, not thrown) - it throws only an {@link SSEError} when a configured
|
|
171
|
+
* `limit` is exceeded. Testable with plain strings. Pair it with a
|
|
172
|
+
* `TextDecoder({ stream: true })` when reading a byte stream so multi-byte UTF-8
|
|
173
|
+
* characters split across reads are handled (the decoder handles partial CHARS, this
|
|
174
|
+
* parser handles partial LINES + events).
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* const parser = new SSEParser({ limit: 1_000_000 })
|
|
179
|
+
* parser.parse('data: a\ndata: b\n\n') // [{ data: 'a\nb' }] - the two data lines joined
|
|
180
|
+
* parser.parse('event: ping\ndata: 1') // [] - the event is buffered until its blank line
|
|
181
|
+
* parser.parse('\n\n') // [{ data: '1', event: 'ping' }]
|
|
182
|
+
* parser.id // undefined - no `id:` field has been seen
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export declare class SSEParser implements SSEParserInterface {
|
|
186
|
+
#private;
|
|
187
|
+
constructor(options?: SSEParserOptions);
|
|
188
|
+
get id(): string | undefined;
|
|
189
|
+
get retry(): number | undefined;
|
|
190
|
+
parse(chunk: string): readonly SSEEvent[];
|
|
191
|
+
flush(): SSEEvent[];
|
|
192
|
+
reset(): void;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A stateful Server-Sent-Events (SSE) stream parser: feed it string chunks, get
|
|
197
|
+
* back the complete events dispatched so far. A trailing partial line / in-progress
|
|
198
|
+
* event is buffered until the rest arrives.
|
|
199
|
+
*/
|
|
200
|
+
export declare interface SSEParserInterface {
|
|
201
|
+
/**
|
|
202
|
+
* Append `chunk`, then return every event a blank line has DISPATCHED (its `data:`
|
|
203
|
+
* fields concatenated with `\n`, plus the last `event:` / `id:` / `retry:`); an
|
|
204
|
+
* in-progress event and a trailing partial line are retained for the next call.
|
|
205
|
+
*
|
|
206
|
+
* @throws {@link import('./errors.js').SSEError} with code `'OVERFLOW'` when a
|
|
207
|
+
* configured `limit` would be exceeded - the parser's state is left unchanged.
|
|
208
|
+
*/
|
|
209
|
+
parse(chunk: string): readonly SSEEvent[];
|
|
210
|
+
/**
|
|
211
|
+
* Treat any remaining buffered partial line as if it had been terminated, then
|
|
212
|
+
* dispatch the in-progress event if its data buffer is non-empty. A convenience
|
|
213
|
+
* beyond the WHATWG algorithm, which discards an unterminated final event at EOF
|
|
214
|
+
* - without calling `flush()`, that spec-faithful discard is this parser's
|
|
215
|
+
* default behavior.
|
|
216
|
+
*
|
|
217
|
+
* @returns The dispatched event as a single-element array, or `[]` when there was
|
|
218
|
+
* nothing to dispatch.
|
|
219
|
+
*/
|
|
220
|
+
flush(): SSEEvent[];
|
|
221
|
+
/** The persisted last-event-id (WHATWG last-event-id): set by each valid `id:`
|
|
222
|
+
* field and NOT cleared when an event dispatches; `undefined` until the first
|
|
223
|
+
* valid `id:` field arrives, or after `reset()`. */
|
|
224
|
+
readonly id: string | undefined;
|
|
225
|
+
/** The last valid `retry:` reconnection time seen, in ms; `undefined` until the
|
|
226
|
+
* first valid `retry:` field arrives, or after `reset()`. */
|
|
227
|
+
readonly retry: number | undefined;
|
|
228
|
+
/** Drop any buffered partial line, in-progress event, and persisted id/retry -
|
|
229
|
+
* reset for a fresh stream. */
|
|
230
|
+
reset(): void;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Options for {@link import('./factories.js').createSSEParser} / the
|
|
235
|
+
* {@link import('./SSEParser.js').SSEParser} constructor.
|
|
236
|
+
*
|
|
237
|
+
* @remarks
|
|
238
|
+
* `limit` - the maximum total buffered characters the parser will hold at once (the
|
|
239
|
+
* un-consumed line buffer plus the in-progress event's accumulated field lengths -
|
|
240
|
+
* data segments + event type + pending id). Unset → unbounded, the default and
|
|
241
|
+
* existing behavior: the parser then never throws. When set, a `parse(chunk)` call
|
|
242
|
+
* that would push the buffered total over `limit` throws an
|
|
243
|
+
* {@link import('./errors.js').SSEError} with code `'OVERFLOW'` instead of appending
|
|
244
|
+
* the chunk.
|
|
245
|
+
*/
|
|
246
|
+
export declare interface SSEParserOptions {
|
|
247
|
+
readonly limit?: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { }
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The byte-order mark (`U+FEFF`), stripped from the very first chunk of an SSE
|
|
3
|
+
* stream (a leading BOM on later chunks is ordinary content). Spelled as a
|
|
4
|
+
* codepoint so the wire content is unambiguous in source.
|
|
5
|
+
*/
|
|
6
|
+
export declare const BOM: string;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a Server-Sent-Events (SSE) stream parser - a stateful handle that turns
|
|
10
|
+
* string chunks into the complete events dispatched so far.
|
|
11
|
+
*
|
|
12
|
+
* @param options - See {@link SSEParserOptions}.
|
|
13
|
+
* @remarks
|
|
14
|
+
* `options.limit` caps the total buffered characters (un-consumed line buffer plus
|
|
15
|
+
* the in-progress event's field lengths); unset → unbounded, the default. Feed
|
|
16
|
+
* chunks to `parse(chunk)` as they arrive; each blank line DISPATCHES an event whose
|
|
17
|
+
* `data` is its `data:` fields joined by `\n` (plus the last `event:` / `id:` /
|
|
18
|
+
* `retry:`), and an in-progress event split across chunk boundaries is buffered until
|
|
19
|
+
* its blank line arrives (or `flush()` forces it out at end-of-stream). The `id` /
|
|
20
|
+
* `retry` getters expose the persisted last-event-id / reconnection time, which
|
|
21
|
+
* survive dispatch and only clear on `reset()`. Generic and event-free - no
|
|
22
|
+
* server / agent coupling; never throws on malformed input, only on a configured
|
|
23
|
+
* `limit` being exceeded (an {@link import('./errors.js').SSEError} with code
|
|
24
|
+
* `'OVERFLOW'`). Pair it with a `TextDecoder({ stream: true })` to also handle
|
|
25
|
+
* multi-byte UTF-8 characters split across byte reads.
|
|
26
|
+
*
|
|
27
|
+
* @returns A working {@link SSEParserInterface}
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { createSSEParser, isSSEError } from '@src/core'
|
|
32
|
+
*
|
|
33
|
+
* const parser = createSSEParser({ limit: 1_000_000 })
|
|
34
|
+
* parser.parse('data: a\ndata: b\n\n') // [{ data: 'a\nb' }] - the two data lines joined
|
|
35
|
+
* parser.parse('event: ping\ndata: 1') // [] - buffered until its blank line
|
|
36
|
+
* parser.parse('\n\n') // [{ data: '1', event: 'ping' }]
|
|
37
|
+
* try {
|
|
38
|
+
* parser.parse('x'.repeat(2_000_000))
|
|
39
|
+
* } catch (error) {
|
|
40
|
+
* if (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()
|
|
41
|
+
* }
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function createSSEParser(options?: SSEParserOptions): SSEParserInterface;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Narrow an unknown caught value to an {@link SSEError}.
|
|
48
|
+
*
|
|
49
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
50
|
+
* @returns `true` when `value` is an {@link SSEError}
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { isSSEError } from '@src/core'
|
|
55
|
+
*
|
|
56
|
+
* try {
|
|
57
|
+
* parser.parse(chunk)
|
|
58
|
+
* } catch (error) {
|
|
59
|
+
* if (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export declare function isSSEError(value: unknown): value is SSEError;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The NUL byte (`U+0000`). The SSE spec voids an `id:` field whose value contains
|
|
67
|
+
* it, so an `id` carrying a NUL is never surfaced. Spelled as a codepoint so the
|
|
68
|
+
* wire content is unambiguous in source.
|
|
69
|
+
*/
|
|
70
|
+
export declare const NUL: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* An error thrown by the SSE parser.
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* Thrown for: a `parse(chunk)` call whose resulting buffered total (un-consumed
|
|
77
|
+
* line buffer + accumulated per-event field lengths + the incoming chunk) would
|
|
78
|
+
* exceed a configured {@link import('./types.js').SSEParserOptions.limit}
|
|
79
|
+
* (`OVERFLOW`). The parser's state is left UNCHANGED by the throwing call - the
|
|
80
|
+
* chunk is not appended - so a consumer may `reset()` and continue. `context`
|
|
81
|
+
* carries at least `{ limit, size }`: the configured limit and the size the
|
|
82
|
+
* buffer would have reached.
|
|
83
|
+
*/
|
|
84
|
+
export declare class SSEError extends Error {
|
|
85
|
+
readonly code: SSEErrorCode;
|
|
86
|
+
readonly context?: Readonly<Record<string, unknown>>;
|
|
87
|
+
constructor(code: SSEErrorCode, message: string, context?: Readonly<Record<string, unknown>>);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Machine-readable codes carried by an {@link import('./errors.js').SSEError}.
|
|
92
|
+
*
|
|
93
|
+
* @remarks
|
|
94
|
+
* `'OVERFLOW'` - a `parse(chunk)` call would push the buffered total over a
|
|
95
|
+
* configured {@link SSEParserOptions.limit}.
|
|
96
|
+
*/
|
|
97
|
+
export declare type SSEErrorCode = 'OVERFLOW';
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* One dispatched Server-Sent Event - the value a blank line flushes from an
|
|
101
|
+
* {@link SSEParserInterface}.
|
|
102
|
+
*
|
|
103
|
+
* @remarks
|
|
104
|
+
* - `data` is the concatenation of every `data:` field in the event, joined by a
|
|
105
|
+
* single `\n` (the SSE rule: `data: a` + `data: b` → `"a\nb"`), with NO trailing
|
|
106
|
+
* newline. An event is dispatched only when its data buffer is non-empty, so `data`
|
|
107
|
+
* is always a string (possibly empty when an explicit empty `data:` line was sent).
|
|
108
|
+
* - `event` is the last `event:` field seen before the blank line (the event type);
|
|
109
|
+
* absent when no `event:` field appeared.
|
|
110
|
+
* - `id` is the last `id:` field seen (the last-event-id); absent when none appeared.
|
|
111
|
+
* A spec NUL inside an `id` voids it, so it is never surfaced.
|
|
112
|
+
* - `retry` is the `retry:` reconnection time in milliseconds - present only when the
|
|
113
|
+
* field's value was an integer (a non-integer `retry:` is ignored).
|
|
114
|
+
*/
|
|
115
|
+
export declare interface SSEEvent {
|
|
116
|
+
/** The event's concatenated data - each `data:` field joined by `\n`, no trailing newline. */
|
|
117
|
+
readonly data: string;
|
|
118
|
+
/** The event type - the last `event:` field's value, if any. */
|
|
119
|
+
readonly event?: string;
|
|
120
|
+
/** The last-event-id - the last `id:` field's value, if any. */
|
|
121
|
+
readonly id?: string;
|
|
122
|
+
/** The reconnection time in ms - the `retry:` field, present only when it was an integer. */
|
|
123
|
+
readonly retry?: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* A stateful Server-Sent-Events (SSE) stream parser - feed it string chunks, get
|
|
128
|
+
* back the complete events dispatched so far.
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* - **The wire format.** SSE is a UTF-8 text stream of events separated by a blank
|
|
132
|
+
* line. Within an event each line is a `field: value` (one optional space after the
|
|
133
|
+
* colon is stripped; a line with no colon is a field with an empty value; a line
|
|
134
|
+
* STARTING with a colon is a comment and is ignored). The fields: `data` is appended
|
|
135
|
+
* to the event's data buffer - MULTIPLE `data:` lines concatenate with `\n` between
|
|
136
|
+
* them (`data: a` + `data: b` → `"a\nb"`), with no trailing newline; `event` sets the
|
|
137
|
+
* event type (last wins); `id` sets the last-event-id (last wins; an `id` containing a
|
|
138
|
+
* NUL is ignored per spec); `retry` sets the reconnection time in ms (integer only -
|
|
139
|
+
* a non-integer is ignored). Unknown fields are ignored.
|
|
140
|
+
* - **Blank-line dispatch.** A blank line flushes the accumulated event: an
|
|
141
|
+
* {@link SSEEvent} is emitted ONLY when the data buffer is non-empty (a dispatch with
|
|
142
|
+
* an empty data buffer emits nothing - a comment-only or field-only "event" produces
|
|
143
|
+
* no event), and the data buffer + event type reset for the next event afterwards.
|
|
144
|
+
* `id` / `retry` ride on the emitted event for the consumer to track per-event - the
|
|
145
|
+
* emitted event's shape is unchanged - AND are separately persisted as connection
|
|
146
|
+
* state, exposed via the sticky `id` / `retry` getters (see below).
|
|
147
|
+
* - **Sticky connection state.** Each valid `id:` field updates a persisted
|
|
148
|
+
* last-event-id, exposed via the `id` getter; each valid `retry:` field updates a
|
|
149
|
+
* persisted reconnection time, exposed via the `retry` getter. Neither is cleared
|
|
150
|
+
* when an event dispatches - only `reset()` clears them - matching WHATWG
|
|
151
|
+
* last-event-id semantics. A NUL-voided `id` field does not alter the persisted
|
|
152
|
+
* value.
|
|
153
|
+
* - **Cross-chunk reassembly.** `parse(chunk)` appends `chunk` to an internal buffer,
|
|
154
|
+
* splits on the line terminator, processes every COMPLETE line, and retains the
|
|
155
|
+
* trailing partial line (and any in-progress event) for the next call - so an event
|
|
156
|
+
* split across chunk boundaries is reassembled once its blank line arrives. An
|
|
157
|
+
* un-terminated final event stays buffered and is never emitted until its blank
|
|
158
|
+
* line arrives - call `flush()` to force it out at end-of-stream instead.
|
|
159
|
+
* - **Line endings + BOM.** `\r\n`, `\r`, and `\n` are all valid terminators and are
|
|
160
|
+
* normalized; a CRLF split across two chunks is held safely (a trailing `\r` is
|
|
161
|
+
* retained, not flushed as a line, until its `\n` is known). A leading byte-order
|
|
162
|
+
* mark on the first NON-EMPTY chunk is stripped (an empty `parse('')` call before
|
|
163
|
+
* any content does not arm the BOM check).
|
|
164
|
+
* - **Bounded buffering.** Pass {@link SSEParserOptions.limit} to cap the total
|
|
165
|
+
* buffered characters (un-consumed line buffer + in-progress event field lengths);
|
|
166
|
+
* a `parse(chunk)` call that would exceed it throws an {@link SSEError} with code
|
|
167
|
+
* `'OVERFLOW'` and leaves parser state unchanged - the chunk is not appended.
|
|
168
|
+
* - **Total + event-free.** A pure functional primitive - no Emitter, no events, no
|
|
169
|
+
* server / HTTP / agent coupling. It never throws on malformed input (a bad `retry`
|
|
170
|
+
* is ignored, not thrown) - it throws only an {@link SSEError} when a configured
|
|
171
|
+
* `limit` is exceeded. Testable with plain strings. Pair it with a
|
|
172
|
+
* `TextDecoder({ stream: true })` when reading a byte stream so multi-byte UTF-8
|
|
173
|
+
* characters split across reads are handled (the decoder handles partial CHARS, this
|
|
174
|
+
* parser handles partial LINES + events).
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```ts
|
|
178
|
+
* const parser = new SSEParser({ limit: 1_000_000 })
|
|
179
|
+
* parser.parse('data: a\ndata: b\n\n') // [{ data: 'a\nb' }] - the two data lines joined
|
|
180
|
+
* parser.parse('event: ping\ndata: 1') // [] - the event is buffered until its blank line
|
|
181
|
+
* parser.parse('\n\n') // [{ data: '1', event: 'ping' }]
|
|
182
|
+
* parser.id // undefined - no `id:` field has been seen
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export declare class SSEParser implements SSEParserInterface {
|
|
186
|
+
#private;
|
|
187
|
+
constructor(options?: SSEParserOptions);
|
|
188
|
+
get id(): string | undefined;
|
|
189
|
+
get retry(): number | undefined;
|
|
190
|
+
parse(chunk: string): readonly SSEEvent[];
|
|
191
|
+
flush(): SSEEvent[];
|
|
192
|
+
reset(): void;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* A stateful Server-Sent-Events (SSE) stream parser: feed it string chunks, get
|
|
197
|
+
* back the complete events dispatched so far. A trailing partial line / in-progress
|
|
198
|
+
* event is buffered until the rest arrives.
|
|
199
|
+
*/
|
|
200
|
+
export declare interface SSEParserInterface {
|
|
201
|
+
/**
|
|
202
|
+
* Append `chunk`, then return every event a blank line has DISPATCHED (its `data:`
|
|
203
|
+
* fields concatenated with `\n`, plus the last `event:` / `id:` / `retry:`); an
|
|
204
|
+
* in-progress event and a trailing partial line are retained for the next call.
|
|
205
|
+
*
|
|
206
|
+
* @throws {@link import('./errors.js').SSEError} with code `'OVERFLOW'` when a
|
|
207
|
+
* configured `limit` would be exceeded - the parser's state is left unchanged.
|
|
208
|
+
*/
|
|
209
|
+
parse(chunk: string): readonly SSEEvent[];
|
|
210
|
+
/**
|
|
211
|
+
* Treat any remaining buffered partial line as if it had been terminated, then
|
|
212
|
+
* dispatch the in-progress event if its data buffer is non-empty. A convenience
|
|
213
|
+
* beyond the WHATWG algorithm, which discards an unterminated final event at EOF
|
|
214
|
+
* - without calling `flush()`, that spec-faithful discard is this parser's
|
|
215
|
+
* default behavior.
|
|
216
|
+
*
|
|
217
|
+
* @returns The dispatched event as a single-element array, or `[]` when there was
|
|
218
|
+
* nothing to dispatch.
|
|
219
|
+
*/
|
|
220
|
+
flush(): SSEEvent[];
|
|
221
|
+
/** The persisted last-event-id (WHATWG last-event-id): set by each valid `id:`
|
|
222
|
+
* field and NOT cleared when an event dispatches; `undefined` until the first
|
|
223
|
+
* valid `id:` field arrives, or after `reset()`. */
|
|
224
|
+
readonly id: string | undefined;
|
|
225
|
+
/** The last valid `retry:` reconnection time seen, in ms; `undefined` until the
|
|
226
|
+
* first valid `retry:` field arrives, or after `reset()`. */
|
|
227
|
+
readonly retry: number | undefined;
|
|
228
|
+
/** Drop any buffered partial line, in-progress event, and persisted id/retry -
|
|
229
|
+
* reset for a fresh stream. */
|
|
230
|
+
reset(): void;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Options for {@link import('./factories.js').createSSEParser} / the
|
|
235
|
+
* {@link import('./SSEParser.js').SSEParser} constructor.
|
|
236
|
+
*
|
|
237
|
+
* @remarks
|
|
238
|
+
* `limit` - the maximum total buffered characters the parser will hold at once (the
|
|
239
|
+
* un-consumed line buffer plus the in-progress event's accumulated field lengths -
|
|
240
|
+
* data segments + event type + pending id). Unset → unbounded, the default and
|
|
241
|
+
* existing behavior: the parser then never throws. When set, a `parse(chunk)` call
|
|
242
|
+
* that would push the buffered total over `limit` throws an
|
|
243
|
+
* {@link import('./errors.js').SSEError} with code `'OVERFLOW'` instead of appending
|
|
244
|
+
* the chunk.
|
|
245
|
+
*/
|
|
246
|
+
export declare interface SSEParserOptions {
|
|
247
|
+
readonly limit?: number;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export { }
|