@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Orkestrel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# @orkestrel/sse
|
|
2
|
+
|
|
3
|
+
A typed Server-Sent Events parser — incremental, spec-compliant parsing of
|
|
4
|
+
event-stream chunks into typed events with `data`, `event`, `id`, and `retry`
|
|
5
|
+
fields. Feed it string chunks as they arrive; a blank line dispatches the
|
|
6
|
+
accumulated event, and a partial line or in-progress event split across
|
|
7
|
+
chunk boundaries is buffered until the rest arrives. The `id` / `retry`
|
|
8
|
+
fields also persist as sticky connection state — surfaced via the `id` /
|
|
9
|
+
`retry` getters for reconnection — and an optional `limit` bounds total
|
|
10
|
+
buffered characters. A pure functional primitive — no Emitter, no events, no
|
|
11
|
+
server / HTTP / agent coupling; it never throws on malformed input, only a
|
|
12
|
+
typed `SSEError('OVERFLOW')` when a configured `limit` is exceeded. Part of
|
|
13
|
+
the `@orkestrel` line.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install @orkestrel/sse
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- Node.js >= 24
|
|
24
|
+
- ESM + CJS (dual-format build)
|
|
25
|
+
- No runtime dependencies
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { createSSEParser, isSSEError } from '@orkestrel/sse'
|
|
31
|
+
|
|
32
|
+
const parser = createSSEParser({ limit: 1_000_000 })
|
|
33
|
+
parser.parse('data: a\ndata: b\n\n') // [{ data: 'a\nb' }] - the two data lines joined
|
|
34
|
+
parser.parse('event: ping\ndata: 1') // [] - buffered until its blank line
|
|
35
|
+
parser.parse('\n\n') // [{ data: '1', event: 'ping' }]
|
|
36
|
+
|
|
37
|
+
parser.id // '1' - sticky last-event-id, survives dispatch
|
|
38
|
+
parser.retry // undefined - sticky reconnection time, until a retry: field arrives
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
parser.parse('x'.repeat(2_000_000))
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
parser.flush() // force out a trailing unterminated event at end-of-stream
|
|
47
|
+
parser.reset() // full reset - drops buffered state and sticky id/retry
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Pair it with a `TextDecoder({ stream: true })` when reading a byte stream so
|
|
51
|
+
multi-byte UTF-8 characters split across reads are handled — the decoder
|
|
52
|
+
handles partial characters, this parser handles partial lines and events.
|
|
53
|
+
|
|
54
|
+
The optional `limit` option caps total buffered characters; when set, a
|
|
55
|
+
`parse(chunk)` call that would exceed it throws a typed `SSEError('OVERFLOW')`
|
|
56
|
+
instead of growing unbounded, leaving parser state unchanged. Without
|
|
57
|
+
`flush()`, a stream that ends without a final blank line has its last event
|
|
58
|
+
discarded per spec — call `flush()` at end-of-stream to force it out.
|
|
59
|
+
|
|
60
|
+
## Guide
|
|
61
|
+
|
|
62
|
+
For the full surface — the `SSEParser` class, its `SSEEvent` shape, the wire
|
|
63
|
+
format it implements, and the `createSSEParser` factory — see
|
|
64
|
+
[`guides/src/sse.md`](guides/src/sse.md).
|
|
65
|
+
|
|
66
|
+
## Package
|
|
67
|
+
|
|
68
|
+
Published as a single typed entry point per the `exports` field in
|
|
69
|
+
`package.json`.
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT © [Orkestrel](https://github.com/orkestrel) — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/core/constants.ts
|
|
3
|
+
/**
|
|
4
|
+
* The NUL byte (`U+0000`). The SSE spec voids an `id:` field whose value contains
|
|
5
|
+
* it, so an `id` carrying a NUL is never surfaced. Spelled as a codepoint so the
|
|
6
|
+
* wire content is unambiguous in source.
|
|
7
|
+
*/
|
|
8
|
+
var NUL = String.fromCharCode(0);
|
|
9
|
+
/**
|
|
10
|
+
* The byte-order mark (`U+FEFF`), stripped from the very first chunk of an SSE
|
|
11
|
+
* stream (a leading BOM on later chunks is ordinary content). Spelled as a
|
|
12
|
+
* codepoint so the wire content is unambiguous in source.
|
|
13
|
+
*/
|
|
14
|
+
var BOM = String.fromCharCode(65279);
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/core/errors.ts
|
|
17
|
+
/**
|
|
18
|
+
* An error thrown by the SSE parser.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Thrown for: a `parse(chunk)` call whose resulting buffered total (un-consumed
|
|
22
|
+
* line buffer + accumulated per-event field lengths + the incoming chunk) would
|
|
23
|
+
* exceed a configured {@link import('./types.js').SSEParserOptions.limit}
|
|
24
|
+
* (`OVERFLOW`). The parser's state is left UNCHANGED by the throwing call - the
|
|
25
|
+
* chunk is not appended - so a consumer may `reset()` and continue. `context`
|
|
26
|
+
* carries at least `{ limit, size }`: the configured limit and the size the
|
|
27
|
+
* buffer would have reached.
|
|
28
|
+
*/
|
|
29
|
+
var SSEError = class extends Error {
|
|
30
|
+
code;
|
|
31
|
+
context;
|
|
32
|
+
constructor(code, message, context) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "SSEError";
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.context = context;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Narrow an unknown caught value to an {@link SSEError}.
|
|
41
|
+
*
|
|
42
|
+
* @param value - The value to test (typically a `catch` binding)
|
|
43
|
+
* @returns `true` when `value` is an {@link SSEError}
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* import { isSSEError } from '@src/core'
|
|
48
|
+
*
|
|
49
|
+
* try {
|
|
50
|
+
* parser.parse(chunk)
|
|
51
|
+
* } catch (error) {
|
|
52
|
+
* if (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
function isSSEError(value) {
|
|
57
|
+
return value instanceof SSEError;
|
|
58
|
+
}
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/core/SSEParser.ts
|
|
61
|
+
/**
|
|
62
|
+
* A stateful Server-Sent-Events (SSE) stream parser - feed it string chunks, get
|
|
63
|
+
* back the complete events dispatched so far.
|
|
64
|
+
*
|
|
65
|
+
* @remarks
|
|
66
|
+
* - **The wire format.** SSE is a UTF-8 text stream of events separated by a blank
|
|
67
|
+
* line. Within an event each line is a `field: value` (one optional space after the
|
|
68
|
+
* colon is stripped; a line with no colon is a field with an empty value; a line
|
|
69
|
+
* STARTING with a colon is a comment and is ignored). The fields: `data` is appended
|
|
70
|
+
* to the event's data buffer - MULTIPLE `data:` lines concatenate with `\n` between
|
|
71
|
+
* them (`data: a` + `data: b` → `"a\nb"`), with no trailing newline; `event` sets the
|
|
72
|
+
* event type (last wins); `id` sets the last-event-id (last wins; an `id` containing a
|
|
73
|
+
* NUL is ignored per spec); `retry` sets the reconnection time in ms (integer only -
|
|
74
|
+
* a non-integer is ignored). Unknown fields are ignored.
|
|
75
|
+
* - **Blank-line dispatch.** A blank line flushes the accumulated event: an
|
|
76
|
+
* {@link SSEEvent} is emitted ONLY when the data buffer is non-empty (a dispatch with
|
|
77
|
+
* an empty data buffer emits nothing - a comment-only or field-only "event" produces
|
|
78
|
+
* no event), and the data buffer + event type reset for the next event afterwards.
|
|
79
|
+
* `id` / `retry` ride on the emitted event for the consumer to track per-event - the
|
|
80
|
+
* emitted event's shape is unchanged - AND are separately persisted as connection
|
|
81
|
+
* state, exposed via the sticky `id` / `retry` getters (see below).
|
|
82
|
+
* - **Sticky connection state.** Each valid `id:` field updates a persisted
|
|
83
|
+
* last-event-id, exposed via the `id` getter; each valid `retry:` field updates a
|
|
84
|
+
* persisted reconnection time, exposed via the `retry` getter. Neither is cleared
|
|
85
|
+
* when an event dispatches - only `reset()` clears them - matching WHATWG
|
|
86
|
+
* last-event-id semantics. A NUL-voided `id` field does not alter the persisted
|
|
87
|
+
* value.
|
|
88
|
+
* - **Cross-chunk reassembly.** `parse(chunk)` appends `chunk` to an internal buffer,
|
|
89
|
+
* splits on the line terminator, processes every COMPLETE line, and retains the
|
|
90
|
+
* trailing partial line (and any in-progress event) for the next call - so an event
|
|
91
|
+
* split across chunk boundaries is reassembled once its blank line arrives. An
|
|
92
|
+
* un-terminated final event stays buffered and is never emitted until its blank
|
|
93
|
+
* line arrives - call `flush()` to force it out at end-of-stream instead.
|
|
94
|
+
* - **Line endings + BOM.** `\r\n`, `\r`, and `\n` are all valid terminators and are
|
|
95
|
+
* normalized; a CRLF split across two chunks is held safely (a trailing `\r` is
|
|
96
|
+
* retained, not flushed as a line, until its `\n` is known). A leading byte-order
|
|
97
|
+
* mark on the first NON-EMPTY chunk is stripped (an empty `parse('')` call before
|
|
98
|
+
* any content does not arm the BOM check).
|
|
99
|
+
* - **Bounded buffering.** Pass {@link SSEParserOptions.limit} to cap the total
|
|
100
|
+
* buffered characters (un-consumed line buffer + in-progress event field lengths);
|
|
101
|
+
* a `parse(chunk)` call that would exceed it throws an {@link SSEError} with code
|
|
102
|
+
* `'OVERFLOW'` and leaves parser state unchanged - the chunk is not appended.
|
|
103
|
+
* - **Total + event-free.** A pure functional primitive - no Emitter, no events, no
|
|
104
|
+
* server / HTTP / agent coupling. It never throws on malformed input (a bad `retry`
|
|
105
|
+
* is ignored, not thrown) - it throws only an {@link SSEError} when a configured
|
|
106
|
+
* `limit` is exceeded. Testable with plain strings. Pair it with a
|
|
107
|
+
* `TextDecoder({ stream: true })` when reading a byte stream so multi-byte UTF-8
|
|
108
|
+
* characters split across reads are handled (the decoder handles partial CHARS, this
|
|
109
|
+
* parser handles partial LINES + events).
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* const parser = new SSEParser({ limit: 1_000_000 })
|
|
114
|
+
* parser.parse('data: a\ndata: b\n\n') // [{ data: 'a\nb' }] - the two data lines joined
|
|
115
|
+
* parser.parse('event: ping\ndata: 1') // [] - the event is buffered until its blank line
|
|
116
|
+
* parser.parse('\n\n') // [{ data: '1', event: 'ping' }]
|
|
117
|
+
* parser.id // undefined - no `id:` field has been seen
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
var SSEParser = class {
|
|
121
|
+
#limit;
|
|
122
|
+
#buffer = "";
|
|
123
|
+
#offset = 0;
|
|
124
|
+
#started = false;
|
|
125
|
+
#carriage = false;
|
|
126
|
+
#data = [];
|
|
127
|
+
#event = void 0;
|
|
128
|
+
#id = void 0;
|
|
129
|
+
#retry = void 0;
|
|
130
|
+
#lastId = void 0;
|
|
131
|
+
#lastRetry = void 0;
|
|
132
|
+
constructor(options) {
|
|
133
|
+
this.#limit = options?.limit;
|
|
134
|
+
}
|
|
135
|
+
get id() {
|
|
136
|
+
return this.#lastId;
|
|
137
|
+
}
|
|
138
|
+
get retry() {
|
|
139
|
+
return this.#lastRetry;
|
|
140
|
+
}
|
|
141
|
+
parse(chunk) {
|
|
142
|
+
if (this.#limit !== void 0) {
|
|
143
|
+
const size = this.#size() + chunk.length;
|
|
144
|
+
if (size > this.#limit) throw new SSEError("OVERFLOW", `SSE parser buffer would exceed the configured limit of ${this.#limit} characters`, {
|
|
145
|
+
limit: this.#limit,
|
|
146
|
+
size
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (!this.#started && chunk.length > 0) {
|
|
150
|
+
this.#started = true;
|
|
151
|
+
if (chunk.startsWith("")) chunk = chunk.slice(1);
|
|
152
|
+
}
|
|
153
|
+
this.#buffer += chunk;
|
|
154
|
+
const events = [];
|
|
155
|
+
for (;;) {
|
|
156
|
+
const line = this.#take();
|
|
157
|
+
if (line === void 0) break;
|
|
158
|
+
this.#process(line, events);
|
|
159
|
+
}
|
|
160
|
+
if (this.#offset > 0) {
|
|
161
|
+
this.#buffer = this.#buffer.slice(this.#offset);
|
|
162
|
+
this.#offset = 0;
|
|
163
|
+
}
|
|
164
|
+
return events;
|
|
165
|
+
}
|
|
166
|
+
flush() {
|
|
167
|
+
const events = [];
|
|
168
|
+
if (this.#buffer.length > 0) {
|
|
169
|
+
const line = this.#buffer;
|
|
170
|
+
this.#buffer = "";
|
|
171
|
+
this.#offset = 0;
|
|
172
|
+
this.#process(line, events);
|
|
173
|
+
}
|
|
174
|
+
this.#dispatch(events);
|
|
175
|
+
return events;
|
|
176
|
+
}
|
|
177
|
+
reset() {
|
|
178
|
+
this.#buffer = "";
|
|
179
|
+
this.#offset = 0;
|
|
180
|
+
this.#started = false;
|
|
181
|
+
this.#carriage = false;
|
|
182
|
+
this.#clear();
|
|
183
|
+
this.#lastId = void 0;
|
|
184
|
+
this.#lastRetry = void 0;
|
|
185
|
+
}
|
|
186
|
+
#size() {
|
|
187
|
+
let dataTotal = 0;
|
|
188
|
+
for (const segment of this.#data) dataTotal += segment.length;
|
|
189
|
+
return this.#buffer.length + dataTotal + (this.#event?.length ?? 0) + (this.#id?.length ?? 0);
|
|
190
|
+
}
|
|
191
|
+
#take() {
|
|
192
|
+
if (this.#carriage && this.#offset < this.#buffer.length) {
|
|
193
|
+
if (this.#buffer[this.#offset] === "\n") this.#offset += 1;
|
|
194
|
+
this.#carriage = false;
|
|
195
|
+
}
|
|
196
|
+
const newline = this.#buffer.indexOf("\n", this.#offset);
|
|
197
|
+
const carriage = this.#buffer.indexOf("\r", this.#offset);
|
|
198
|
+
if (newline === -1 && carriage === -1) return void 0;
|
|
199
|
+
if (carriage !== -1 && (newline === -1 || carriage < newline)) {
|
|
200
|
+
this.#carriage = true;
|
|
201
|
+
const line = this.#buffer.slice(this.#offset, carriage);
|
|
202
|
+
this.#offset = carriage + 1;
|
|
203
|
+
return line;
|
|
204
|
+
}
|
|
205
|
+
const line = this.#buffer.slice(this.#offset, newline);
|
|
206
|
+
this.#offset = newline + 1;
|
|
207
|
+
return line;
|
|
208
|
+
}
|
|
209
|
+
#process(line, events) {
|
|
210
|
+
if (line.length === 0) {
|
|
211
|
+
this.#dispatch(events);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (line.startsWith(":")) return;
|
|
215
|
+
const colon = line.indexOf(":");
|
|
216
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
217
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
218
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
219
|
+
this.#field(field, value);
|
|
220
|
+
}
|
|
221
|
+
#field(field, value) {
|
|
222
|
+
if (field === "data") this.#data.push(value);
|
|
223
|
+
else if (field === "event") this.#event = value;
|
|
224
|
+
else if (field === "id") {
|
|
225
|
+
if (!value.includes("\0")) {
|
|
226
|
+
this.#id = value;
|
|
227
|
+
this.#lastId = value;
|
|
228
|
+
}
|
|
229
|
+
} else if (field === "retry") {
|
|
230
|
+
if (/^\d+$/.test(value)) {
|
|
231
|
+
this.#retry = Number(value);
|
|
232
|
+
this.#lastRetry = this.#retry;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
#dispatch(events) {
|
|
237
|
+
if (this.#data.length > 0) {
|
|
238
|
+
const event = {
|
|
239
|
+
data: this.#data.join("\n"),
|
|
240
|
+
...this.#event !== void 0 ? { event: this.#event } : {},
|
|
241
|
+
...this.#id !== void 0 ? { id: this.#id } : {},
|
|
242
|
+
...this.#retry !== void 0 ? { retry: this.#retry } : {}
|
|
243
|
+
};
|
|
244
|
+
events.push(event);
|
|
245
|
+
}
|
|
246
|
+
this.#clear();
|
|
247
|
+
}
|
|
248
|
+
#clear() {
|
|
249
|
+
this.#data = [];
|
|
250
|
+
this.#event = void 0;
|
|
251
|
+
this.#id = void 0;
|
|
252
|
+
this.#retry = void 0;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/core/factories.ts
|
|
257
|
+
/**
|
|
258
|
+
* Create a Server-Sent-Events (SSE) stream parser - a stateful handle that turns
|
|
259
|
+
* string chunks into the complete events dispatched so far.
|
|
260
|
+
*
|
|
261
|
+
* @param options - See {@link SSEParserOptions}.
|
|
262
|
+
* @remarks
|
|
263
|
+
* `options.limit` caps the total buffered characters (un-consumed line buffer plus
|
|
264
|
+
* the in-progress event's field lengths); unset → unbounded, the default. Feed
|
|
265
|
+
* chunks to `parse(chunk)` as they arrive; each blank line DISPATCHES an event whose
|
|
266
|
+
* `data` is its `data:` fields joined by `\n` (plus the last `event:` / `id:` /
|
|
267
|
+
* `retry:`), and an in-progress event split across chunk boundaries is buffered until
|
|
268
|
+
* its blank line arrives (or `flush()` forces it out at end-of-stream). The `id` /
|
|
269
|
+
* `retry` getters expose the persisted last-event-id / reconnection time, which
|
|
270
|
+
* survive dispatch and only clear on `reset()`. Generic and event-free - no
|
|
271
|
+
* server / agent coupling; never throws on malformed input, only on a configured
|
|
272
|
+
* `limit` being exceeded (an {@link import('./errors.js').SSEError} with code
|
|
273
|
+
* `'OVERFLOW'`). Pair it with a `TextDecoder({ stream: true })` to also handle
|
|
274
|
+
* multi-byte UTF-8 characters split across byte reads.
|
|
275
|
+
*
|
|
276
|
+
* @returns A working {@link SSEParserInterface}
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```ts
|
|
280
|
+
* import { createSSEParser, isSSEError } from '@src/core'
|
|
281
|
+
*
|
|
282
|
+
* const parser = createSSEParser({ limit: 1_000_000 })
|
|
283
|
+
* parser.parse('data: a\ndata: b\n\n') // [{ data: 'a\nb' }] - the two data lines joined
|
|
284
|
+
* parser.parse('event: ping\ndata: 1') // [] - buffered until its blank line
|
|
285
|
+
* parser.parse('\n\n') // [{ data: '1', event: 'ping' }]
|
|
286
|
+
* try {
|
|
287
|
+
* parser.parse('x'.repeat(2_000_000))
|
|
288
|
+
* } catch (error) {
|
|
289
|
+
* if (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()
|
|
290
|
+
* }
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
function createSSEParser(options) {
|
|
294
|
+
return new SSEParser(options);
|
|
295
|
+
}
|
|
296
|
+
//#endregion
|
|
297
|
+
exports.BOM = BOM;
|
|
298
|
+
exports.NUL = NUL;
|
|
299
|
+
exports.SSEError = SSEError;
|
|
300
|
+
exports.SSEParser = SSEParser;
|
|
301
|
+
exports.createSSEParser = createSSEParser;
|
|
302
|
+
exports.isSSEError = isSSEError;
|
|
303
|
+
|
|
304
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["#limit","#lastId","#lastRetry","#size","#started","#buffer","#take","#process","#offset","#dispatch","#carriage","#clear","#data","#event","#id","#field","#retry"],"sources":["../../../src/core/constants.ts","../../../src/core/errors.ts","../../../src/core/SSEParser.ts","../../../src/core/factories.ts"],"sourcesContent":["/**\n * The NUL byte (`U+0000`). The SSE spec voids an `id:` field whose value contains\n * it, so an `id` carrying a NUL is never surfaced. Spelled as a codepoint so the\n * wire content is unambiguous in source.\n */\nexport const NUL = String.fromCharCode(0)\n\n/**\n * The byte-order mark (`U+FEFF`), stripped from the very first chunk of an SSE\n * stream (a leading BOM on later chunks is ordinary content). Spelled as a\n * codepoint so the wire content is unambiguous in source.\n */\nexport const BOM = String.fromCharCode(0xfeff)\n","import type { SSEErrorCode } from './types.js'\n\n// AGENTS §12: a configured `limit` exceeded by a `parse(chunk)` call `throw`s an\n// `SSEError` carrying a machine-readable `code`, so a `catch` branches on `error.code`\n// instead of parsing the message. Every other malformed-input case (a bad `retry`, a\n// NUL-voided `id`, an unknown field) is ignored per the WHATWG SSE algorithm and never\n// throws.\n\n/**\n * An error thrown by the SSE parser.\n *\n * @remarks\n * Thrown for: a `parse(chunk)` call whose resulting buffered total (un-consumed\n * line buffer + accumulated per-event field lengths + the incoming chunk) would\n * exceed a configured {@link import('./types.js').SSEParserOptions.limit}\n * (`OVERFLOW`). The parser's state is left UNCHANGED by the throwing call - the\n * chunk is not appended - so a consumer may `reset()` and continue. `context`\n * carries at least `{ limit, size }`: the configured limit and the size the\n * buffer would have reached.\n */\nexport class SSEError extends Error {\n\treadonly code: SSEErrorCode\n\treadonly context?: Readonly<Record<string, unknown>>\n\n\tconstructor(code: SSEErrorCode, message: string, context?: Readonly<Record<string, unknown>>) {\n\t\tsuper(message)\n\t\tthis.name = 'SSEError'\n\t\tthis.code = code\n\t\tthis.context = context\n\t}\n}\n\n/**\n * Narrow an unknown caught value to an {@link SSEError}.\n *\n * @param value - The value to test (typically a `catch` binding)\n * @returns `true` when `value` is an {@link SSEError}\n *\n * @example\n * ```ts\n * import { isSSEError } from '@src/core'\n *\n * try {\n * \tparser.parse(chunk)\n * } catch (error) {\n * \tif (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()\n * }\n * ```\n */\nexport function isSSEError(value: unknown): value is SSEError {\n\treturn value instanceof SSEError\n}\n","import type { SSEEvent, SSEParserInterface, SSEParserOptions } from './types.js'\nimport { BOM, NUL } from './constants.js'\nimport { SSEError } from './errors.js'\n\n/**\n * A stateful Server-Sent-Events (SSE) stream parser - feed it string chunks, get\n * back the complete events dispatched so far.\n *\n * @remarks\n * - **The wire format.** SSE is a UTF-8 text stream of events separated by a blank\n * line. Within an event each line is a `field: value` (one optional space after the\n * colon is stripped; a line with no colon is a field with an empty value; a line\n * STARTING with a colon is a comment and is ignored). The fields: `data` is appended\n * to the event's data buffer - MULTIPLE `data:` lines concatenate with `\\n` between\n * them (`data: a` + `data: b` → `\"a\\nb\"`), with no trailing newline; `event` sets the\n * event type (last wins); `id` sets the last-event-id (last wins; an `id` containing a\n * NUL is ignored per spec); `retry` sets the reconnection time in ms (integer only -\n * a non-integer is ignored). Unknown fields are ignored.\n * - **Blank-line dispatch.** A blank line flushes the accumulated event: an\n * {@link SSEEvent} is emitted ONLY when the data buffer is non-empty (a dispatch with\n * an empty data buffer emits nothing - a comment-only or field-only \"event\" produces\n * no event), and the data buffer + event type reset for the next event afterwards.\n * `id` / `retry` ride on the emitted event for the consumer to track per-event - the\n * emitted event's shape is unchanged - AND are separately persisted as connection\n * state, exposed via the sticky `id` / `retry` getters (see below).\n * - **Sticky connection state.** Each valid `id:` field updates a persisted\n * last-event-id, exposed via the `id` getter; each valid `retry:` field updates a\n * persisted reconnection time, exposed via the `retry` getter. Neither is cleared\n * when an event dispatches - only `reset()` clears them - matching WHATWG\n * last-event-id semantics. A NUL-voided `id` field does not alter the persisted\n * value.\n * - **Cross-chunk reassembly.** `parse(chunk)` appends `chunk` to an internal buffer,\n * splits on the line terminator, processes every COMPLETE line, and retains the\n * trailing partial line (and any in-progress event) for the next call - so an event\n * split across chunk boundaries is reassembled once its blank line arrives. An\n * un-terminated final event stays buffered and is never emitted until its blank\n * line arrives - call `flush()` to force it out at end-of-stream instead.\n * - **Line endings + BOM.** `\\r\\n`, `\\r`, and `\\n` are all valid terminators and are\n * normalized; a CRLF split across two chunks is held safely (a trailing `\\r` is\n * retained, not flushed as a line, until its `\\n` is known). A leading byte-order\n * mark on the first NON-EMPTY chunk is stripped (an empty `parse('')` call before\n * any content does not arm the BOM check).\n * - **Bounded buffering.** Pass {@link SSEParserOptions.limit} to cap the total\n * buffered characters (un-consumed line buffer + in-progress event field lengths);\n * a `parse(chunk)` call that would exceed it throws an {@link SSEError} with code\n * `'OVERFLOW'` and leaves parser state unchanged - the chunk is not appended.\n * - **Total + event-free.** A pure functional primitive - no Emitter, no events, no\n * server / HTTP / agent coupling. It never throws on malformed input (a bad `retry`\n * is ignored, not thrown) - it throws only an {@link SSEError} when a configured\n * `limit` is exceeded. Testable with plain strings. Pair it with a\n * `TextDecoder({ stream: true })` when reading a byte stream so multi-byte UTF-8\n * characters split across reads are handled (the decoder handles partial CHARS, this\n * parser handles partial LINES + events).\n *\n * @example\n * ```ts\n * const parser = new SSEParser({ limit: 1_000_000 })\n * parser.parse('data: a\\ndata: b\\n\\n') // [{ data: 'a\\nb' }] - the two data lines joined\n * parser.parse('event: ping\\ndata: 1') // [] - the event is buffered until its blank line\n * parser.parse('\\n\\n') // [{ data: '1', event: 'ping' }]\n * parser.id // undefined - no `id:` field has been seen\n * ```\n */\nexport class SSEParser implements SSEParserInterface {\n\treadonly #limit: number | undefined\n\t#buffer = ''\n\t// Read cursor into `#buffer` for the current `parse()` call - lines are consumed\n\t// by advancing this offset, never by front-slicing `#buffer` per line. The\n\t// invariant `#offset === 0` holds BETWEEN calls: `parse()` compacts `#buffer` to\n\t// its unconsumed tail (and resets `#offset` to 0) once, at the end of the call.\n\t#offset = 0\n\t#started = false\n\t// True when the last consumed line was terminated by a `\\r`: a `\\n` arriving next\n\t// (the second half of a CRLF split across chunks) is then swallowed, not read as a\n\t// fresh blank line. This lets a bare-`\\r` terminator flush immediately AND keeps a\n\t// CRLF straddling a chunk boundary as one terminator.\n\t#carriage = false\n\t// The in-progress event accumulator, reset after each blank-line dispatch.\n\t#data: string[] = []\n\t#event: string | undefined = undefined\n\t#id: string | undefined = undefined\n\t#retry: number | undefined = undefined\n\t// Persisted connection state (WHATWG last-event-id / reconnection time) - updated\n\t// by every valid `id:` / `retry:` field, never cleared on dispatch; only `reset()`\n\t// clears them.\n\t#lastId: string | undefined = undefined\n\t#lastRetry: number | undefined = undefined\n\n\tconstructor(options?: SSEParserOptions) {\n\t\tthis.#limit = options?.limit\n\t}\n\n\tget id(): string | undefined {\n\t\treturn this.#lastId\n\t}\n\n\tget retry(): number | undefined {\n\t\treturn this.#lastRetry\n\t}\n\n\tparse(chunk: string): readonly SSEEvent[] {\n\t\tif (this.#limit !== undefined) {\n\t\t\tconst size = this.#size() + chunk.length\n\t\t\tif (size > this.#limit) {\n\t\t\t\tthrow new SSEError(\n\t\t\t\t\t'OVERFLOW',\n\t\t\t\t\t`SSE parser buffer would exceed the configured limit of ${this.#limit} characters`,\n\t\t\t\t\t{ limit: this.#limit, size },\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\t// Strip a leading byte-order mark on the first NON-EMPTY chunk only - an empty\n\t\t// `parse('')` before any content must not disarm the check.\n\t\tif (!this.#started && chunk.length > 0) {\n\t\t\tthis.#started = true\n\t\t\tif (chunk.startsWith(BOM)) chunk = chunk.slice(BOM.length)\n\t\t}\n\t\tthis.#buffer += chunk\n\t\tconst events: SSEEvent[] = []\n\t\t// Pull every COMPLETE line off the buffer by advancing the read cursor, leaving\n\t\t// the trailing partial line buffered (unconsumed) for the next call.\n\t\tfor (;;) {\n\t\t\tconst line = this.#take()\n\t\t\tif (line === undefined) break\n\t\t\tthis.#process(line, events)\n\t\t}\n\t\t// Compact once per call: drop the consumed prefix and reset the cursor.\n\t\tif (this.#offset > 0) {\n\t\t\tthis.#buffer = this.#buffer.slice(this.#offset)\n\t\t\tthis.#offset = 0\n\t\t}\n\t\treturn events\n\t}\n\n\tflush(): SSEEvent[] {\n\t\tconst events: SSEEvent[] = []\n\t\tif (this.#buffer.length > 0) {\n\t\t\tconst line = this.#buffer\n\t\t\tthis.#buffer = ''\n\t\t\tthis.#offset = 0\n\t\t\tthis.#process(line, events)\n\t\t}\n\t\tthis.#dispatch(events)\n\t\treturn events\n\t}\n\n\treset(): void {\n\t\tthis.#buffer = ''\n\t\tthis.#offset = 0\n\t\tthis.#started = false\n\t\tthis.#carriage = false\n\t\tthis.#clear()\n\t\tthis.#lastId = undefined\n\t\tthis.#lastRetry = undefined\n\t}\n\n\t// Total characters currently buffered: the un-consumed line buffer (always\n\t// compacted to offset 0 between calls) plus the in-progress event's accumulated\n\t// field lengths (every `data:` segment + the pending `event` + the pending `id`).\n\t#size(): number {\n\t\tlet dataTotal = 0\n\t\tfor (const segment of this.#data) dataTotal += segment.length\n\t\treturn this.#buffer.length + dataTotal + (this.#event?.length ?? 0) + (this.#id?.length ?? 0)\n\t}\n\n\t// Take the next complete line off the buffer, or `undefined` when only a partial\n\t// line remains. A line ends at `\\n`, a `\\r`, or a `\\r\\n`. A `\\r` flushes its line\n\t// immediately (bare-`\\r` is a valid terminator) and arms {@link #carriage}; a `\\n`\n\t// directly following a consumed `\\r` is the CRLF's second half and is swallowed.\n\t#take(): string | undefined {\n\t\t// Swallow a `\\n` left over from a `\\r\\n` whose `\\r` already terminated a line -\n\t\t// only when a byte is present to inspect, so the flag survives an empty buffer\n\t\t// between calls (a `\\r` ends one chunk, its `\\n` opens the next).\n\t\tif (this.#carriage && this.#offset < this.#buffer.length) {\n\t\t\tif (this.#buffer[this.#offset] === '\\n') this.#offset += 1\n\t\t\tthis.#carriage = false\n\t\t}\n\t\tconst newline = this.#buffer.indexOf('\\n', this.#offset)\n\t\tconst carriage = this.#buffer.indexOf('\\r', this.#offset)\n\t\t// No terminator at all → the rest of the buffer is a partial line.\n\t\tif (newline === -1 && carriage === -1) return undefined\n\t\t// Pick the earliest terminator; a leading `\\r` flushes its line and arms the\n\t\t// carriage flag so the matching `\\n` (this chunk or the next) is swallowed.\n\t\tif (carriage !== -1 && (newline === -1 || carriage < newline)) {\n\t\t\tthis.#carriage = true\n\t\t\tconst line = this.#buffer.slice(this.#offset, carriage)\n\t\t\tthis.#offset = carriage + 1\n\t\t\treturn line\n\t\t}\n\t\tconst line = this.#buffer.slice(this.#offset, newline)\n\t\tthis.#offset = newline + 1\n\t\treturn line\n\t}\n\n\t// Process one complete line: a blank line dispatches the accumulated event; a\n\t// comment (a line starting with `:`) is ignored; otherwise it is a `field: value`.\n\t#process(line: string, events: SSEEvent[]): void {\n\t\tif (line.length === 0) {\n\t\t\tthis.#dispatch(events)\n\t\t\treturn\n\t\t}\n\t\tif (line.startsWith(':')) return // comment line - ignored\n\t\tconst colon = line.indexOf(':')\n\t\t// No colon → the whole line is the field name, value is empty (per spec).\n\t\tconst field = colon === -1 ? line : line.slice(0, colon)\n\t\t// One optional leading space after the colon is stripped; nothing else.\n\t\tlet value = colon === -1 ? '' : line.slice(colon + 1)\n\t\tif (value.startsWith(' ')) value = value.slice(1)\n\t\tthis.#field(field, value)\n\t}\n\n\t// Apply one parsed field to the in-progress event (and the persisted connection\n\t// state for `id` / `retry`). Unknown fields are ignored.\n\t#field(field: string, value: string): void {\n\t\tif (field === 'data') this.#data.push(value)\n\t\telse if (field === 'event') this.#event = value\n\t\telse if (field === 'id') {\n\t\t\t// An `id` containing a NUL is voided per spec - never surfaced, and the\n\t\t\t// persisted last-event-id is left unchanged.\n\t\t\tif (!value.includes(NUL)) {\n\t\t\t\tthis.#id = value\n\t\t\t\tthis.#lastId = value\n\t\t\t}\n\t\t} else if (field === 'retry') {\n\t\t\t// Integer-only; a non-integer reconnection time is ignored (never throws).\n\t\t\tif (/^\\d+$/.test(value)) {\n\t\t\t\tthis.#retry = Number(value)\n\t\t\t\tthis.#lastRetry = this.#retry\n\t\t\t}\n\t\t}\n\t}\n\n\t// Dispatch the accumulated event on a blank line (or on `flush()`): emit ONLY when\n\t// data was buffered (the spec's empty-data rule), then reset the accumulator for\n\t// the next event. Persisted `id` / `retry` state is untouched.\n\t#dispatch(events: SSEEvent[]): void {\n\t\tif (this.#data.length > 0) {\n\t\t\tconst event: SSEEvent = {\n\t\t\t\tdata: this.#data.join('\\n'),\n\t\t\t\t...(this.#event !== undefined ? { event: this.#event } : {}),\n\t\t\t\t...(this.#id !== undefined ? { id: this.#id } : {}),\n\t\t\t\t...(this.#retry !== undefined ? { retry: this.#retry } : {}),\n\t\t\t}\n\t\t\tevents.push(event)\n\t\t}\n\t\tthis.#clear()\n\t}\n\n\t// Reset the in-progress event accumulator (data buffer + last-seen fields).\n\t#clear(): void {\n\t\tthis.#data = []\n\t\tthis.#event = undefined\n\t\tthis.#id = undefined\n\t\tthis.#retry = undefined\n\t}\n}\n","import type { SSEParserInterface, SSEParserOptions } from './types.js'\nimport { SSEParser } from './SSEParser.js'\n\n/**\n * Create a Server-Sent-Events (SSE) stream parser - a stateful handle that turns\n * string chunks into the complete events dispatched so far.\n *\n * @param options - See {@link SSEParserOptions}.\n * @remarks\n * `options.limit` caps the total buffered characters (un-consumed line buffer plus\n * the in-progress event's field lengths); unset → unbounded, the default. Feed\n * chunks to `parse(chunk)` as they arrive; each blank line DISPATCHES an event whose\n * `data` is its `data:` fields joined by `\\n` (plus the last `event:` / `id:` /\n * `retry:`), and an in-progress event split across chunk boundaries is buffered until\n * its blank line arrives (or `flush()` forces it out at end-of-stream). The `id` /\n * `retry` getters expose the persisted last-event-id / reconnection time, which\n * survive dispatch and only clear on `reset()`. Generic and event-free - no\n * server / agent coupling; never throws on malformed input, only on a configured\n * `limit` being exceeded (an {@link import('./errors.js').SSEError} with code\n * `'OVERFLOW'`). Pair it with a `TextDecoder({ stream: true })` to also handle\n * multi-byte UTF-8 characters split across byte reads.\n *\n * @returns A working {@link SSEParserInterface}\n *\n * @example\n * ```ts\n * import { createSSEParser, isSSEError } from '@src/core'\n *\n * const parser = createSSEParser({ limit: 1_000_000 })\n * parser.parse('data: a\\ndata: b\\n\\n') // [{ data: 'a\\nb' }] - the two data lines joined\n * parser.parse('event: ping\\ndata: 1') // [] - buffered until its blank line\n * parser.parse('\\n\\n') // [{ data: '1', event: 'ping' }]\n * try {\n * \tparser.parse('x'.repeat(2_000_000))\n * } catch (error) {\n * \tif (isSSEError(error) && error.code === 'OVERFLOW') parser.reset()\n * }\n * ```\n */\nexport function createSSEParser(options?: SSEParserOptions): SSEParserInterface {\n\treturn new SSEParser(options)\n}\n"],"mappings":";;;;;;;AAKA,IAAa,MAAM,OAAO,aAAa,CAAC;;;;;;AAOxC,IAAa,MAAM,OAAO,aAAa,KAAM;;;;;;;;;;;;;;;ACQ7C,IAAa,WAAb,cAA8B,MAAM;CACnC;CACA;CAEA,YAAY,MAAoB,SAAiB,SAA6C;EAC7F,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,UAAU;CAChB;AACD;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,WAAW,OAAmC;CAC7D,OAAO,iBAAiB;AACzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACYA,IAAa,YAAb,MAAqD;CACpD;CACA,UAAU;CAKV,UAAU;CACV,WAAW;CAKX,YAAY;CAEZ,QAAkB,CAAC;CACnB,SAA6B,KAAA;CAC7B,MAA0B,KAAA;CAC1B,SAA6B,KAAA;CAI7B,UAA8B,KAAA;CAC9B,aAAiC,KAAA;CAEjC,YAAY,SAA4B;EACvC,KAAKA,SAAS,SAAS;CACxB;CAEA,IAAI,KAAyB;EAC5B,OAAO,KAAKC;CACb;CAEA,IAAI,QAA4B;EAC/B,OAAO,KAAKC;CACb;CAEA,MAAM,OAAoC;EACzC,IAAI,KAAKF,WAAW,KAAA,GAAW;GAC9B,MAAM,OAAO,KAAKG,MAAM,IAAI,MAAM;GAClC,IAAI,OAAO,KAAKH,QACf,MAAM,IAAI,SACT,YACA,0DAA0D,KAAKA,OAAO,cACtE;IAAE,OAAO,KAAKA;IAAQ;GAAK,CAC5B;EAEF;EAGA,IAAI,CAAC,KAAKI,YAAY,MAAM,SAAS,GAAG;GACvC,KAAKA,WAAW;GAChB,IAAI,MAAM,WAAA,GAAc,GAAG,QAAQ,MAAM,MAAA,CAAgB;EAC1D;EACA,KAAKC,WAAW;EAChB,MAAM,SAAqB,CAAC;EAG5B,SAAS;GACR,MAAM,OAAO,KAAKC,MAAM;GACxB,IAAI,SAAS,KAAA,GAAW;GACxB,KAAKC,SAAS,MAAM,MAAM;EAC3B;EAEA,IAAI,KAAKC,UAAU,GAAG;GACrB,KAAKH,UAAU,KAAKA,QAAQ,MAAM,KAAKG,OAAO;GAC9C,KAAKA,UAAU;EAChB;EACA,OAAO;CACR;CAEA,QAAoB;EACnB,MAAM,SAAqB,CAAC;EAC5B,IAAI,KAAKH,QAAQ,SAAS,GAAG;GAC5B,MAAM,OAAO,KAAKA;GAClB,KAAKA,UAAU;GACf,KAAKG,UAAU;GACf,KAAKD,SAAS,MAAM,MAAM;EAC3B;EACA,KAAKE,UAAU,MAAM;EACrB,OAAO;CACR;CAEA,QAAc;EACb,KAAKJ,UAAU;EACf,KAAKG,UAAU;EACf,KAAKJ,WAAW;EAChB,KAAKM,YAAY;EACjB,KAAKC,OAAO;EACZ,KAAKV,UAAU,KAAA;EACf,KAAKC,aAAa,KAAA;CACnB;CAKA,QAAgB;EACf,IAAI,YAAY;EAChB,KAAK,MAAM,WAAW,KAAKU,OAAO,aAAa,QAAQ;EACvD,OAAO,KAAKP,QAAQ,SAAS,aAAa,KAAKQ,QAAQ,UAAU,MAAM,KAAKC,KAAK,UAAU;CAC5F;CAMA,QAA4B;EAI3B,IAAI,KAAKJ,aAAa,KAAKF,UAAU,KAAKH,QAAQ,QAAQ;GACzD,IAAI,KAAKA,QAAQ,KAAKG,aAAa,MAAM,KAAKA,WAAW;GACzD,KAAKE,YAAY;EAClB;EACA,MAAM,UAAU,KAAKL,QAAQ,QAAQ,MAAM,KAAKG,OAAO;EACvD,MAAM,WAAW,KAAKH,QAAQ,QAAQ,MAAM,KAAKG,OAAO;EAExD,IAAI,YAAY,MAAM,aAAa,IAAI,OAAO,KAAA;EAG9C,IAAI,aAAa,OAAO,YAAY,MAAM,WAAW,UAAU;GAC9D,KAAKE,YAAY;GACjB,MAAM,OAAO,KAAKL,QAAQ,MAAM,KAAKG,SAAS,QAAQ;GACtD,KAAKA,UAAU,WAAW;GAC1B,OAAO;EACR;EACA,MAAM,OAAO,KAAKH,QAAQ,MAAM,KAAKG,SAAS,OAAO;EACrD,KAAKA,UAAU,UAAU;EACzB,OAAO;CACR;CAIA,SAAS,MAAc,QAA0B;EAChD,IAAI,KAAK,WAAW,GAAG;GACtB,KAAKC,UAAU,MAAM;GACrB;EACD;EACA,IAAI,KAAK,WAAW,GAAG,GAAG;EAC1B,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAE9B,MAAM,QAAQ,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;EAEvD,IAAI,QAAQ,UAAU,KAAK,KAAK,KAAK,MAAM,QAAQ,CAAC;EACpD,IAAI,MAAM,WAAW,GAAG,GAAG,QAAQ,MAAM,MAAM,CAAC;EAChD,KAAKM,OAAO,OAAO,KAAK;CACzB;CAIA,OAAO,OAAe,OAAqB;EAC1C,IAAI,UAAU,QAAQ,KAAKH,MAAM,KAAK,KAAK;OACtC,IAAI,UAAU,SAAS,KAAKC,SAAS;OACrC,IAAI,UAAU;OAGd,CAAC,MAAM,SAAA,IAAY,GAAG;IACzB,KAAKC,MAAM;IACX,KAAKb,UAAU;GAChB;SACM,IAAI,UAAU;OAEhB,QAAQ,KAAK,KAAK,GAAG;IACxB,KAAKe,SAAS,OAAO,KAAK;IAC1B,KAAKd,aAAa,KAAKc;GACxB;;CAEF;CAKA,UAAU,QAA0B;EACnC,IAAI,KAAKJ,MAAM,SAAS,GAAG;GAC1B,MAAM,QAAkB;IACvB,MAAM,KAAKA,MAAM,KAAK,IAAI;IAC1B,GAAI,KAAKC,WAAW,KAAA,IAAY,EAAE,OAAO,KAAKA,OAAO,IAAI,CAAC;IAC1D,GAAI,KAAKC,QAAQ,KAAA,IAAY,EAAE,IAAI,KAAKA,IAAI,IAAI,CAAC;IACjD,GAAI,KAAKE,WAAW,KAAA,IAAY,EAAE,OAAO,KAAKA,OAAO,IAAI,CAAC;GAC3D;GACA,OAAO,KAAK,KAAK;EAClB;EACA,KAAKL,OAAO;CACb;CAGA,SAAe;EACd,KAAKC,QAAQ,CAAC;EACd,KAAKC,SAAS,KAAA;EACd,KAAKC,MAAM,KAAA;EACX,KAAKE,SAAS,KAAA;CACf;AACD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACxNA,SAAgB,gBAAgB,SAAgD;CAC/E,OAAO,IAAI,UAAU,OAAO;AAC7B"}
|