eventsource-parser
Advanced tools
+95
-43
@@ -8,2 +8,3 @@ "use strict"; | ||
| } | ||
| const LF = 10, CR = 13, SPACE = 32; | ||
| function noop(_arg) { | ||
@@ -17,25 +18,91 @@ } | ||
| const { onEvent = noop, onError = noop, onRetry = noop, onComment } = callbacks; | ||
| let incompleteLine = "", isFirstChunk = !0, id, data = "", eventType = ""; | ||
| function feed(newChunk) { | ||
| const chunk = isFirstChunk ? newChunk.replace(/^\xEF\xBB\xBF/, "") : newChunk, [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`); | ||
| for (const line of complete) | ||
| parseLine(line); | ||
| incompleteLine = incomplete, isFirstChunk = !1; | ||
| let incompleteLine = "", isFirstChunk = !0, id, data = "", dataLines = 0, eventType; | ||
| function feed(chunk) { | ||
| if (isFirstChunk) { | ||
| isFirstChunk = !1, feedFirst(chunk); | ||
| return; | ||
| } | ||
| const input = incompleteLine === "" ? chunk : incompleteLine + chunk; | ||
| incompleteLine = processLines(input); | ||
| } | ||
| function parseLine(line) { | ||
| if (line === "") { | ||
| function feedFirst(chunk) { | ||
| chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191 && (chunk = chunk.slice(3)); | ||
| const input = incompleteLine === "" ? chunk : incompleteLine + chunk; | ||
| incompleteLine = processLines(input); | ||
| } | ||
| function processLines(chunk) { | ||
| let searchIndex = 0; | ||
| if (chunk.indexOf("\r") === -1) { | ||
| let lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| for (; lfIndex !== -1; ) { | ||
| if (searchIndex === lfIndex) { | ||
| dataLines > 0 && onEvent({ id, event: eventType, data }), id = void 0, data = "", dataLines = 0, eventType = void 0, searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| continue; | ||
| } | ||
| const firstCharCode = chunk.charCodeAt(searchIndex); | ||
| if (isDataPrefix(chunk, searchIndex, firstCharCode)) { | ||
| const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5, value = chunk.slice(valueStart, lfIndex); | ||
| if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) { | ||
| onEvent({ id, event: eventType, data: value }), id = void 0, data = "", eventType = void 0, searchIndex = lfIndex + 2, lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| continue; | ||
| } | ||
| data = dataLines === 0 ? value : `${data} | ||
| ${value}`, dataLines++; | ||
| } else isEventPrefix(chunk, searchIndex, firstCharCode) ? eventType = chunk.slice( | ||
| chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, | ||
| lfIndex | ||
| ) || void 0 : parseLine(chunk, searchIndex, lfIndex); | ||
| searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| } | ||
| return chunk.slice(searchIndex); | ||
| } | ||
| for (; searchIndex < chunk.length; ) { | ||
| const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| let lineEnd = -1; | ||
| if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = crIndex < lfIndex ? crIndex : lfIndex : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) | ||
| break; | ||
| parseLine(chunk, searchIndex, lineEnd), searchIndex = lineEnd + 1, chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF && searchIndex++; | ||
| } | ||
| return chunk.slice(searchIndex); | ||
| } | ||
| function parseLine(chunk, start, end) { | ||
| if (start === end) { | ||
| dispatchEvent(); | ||
| return; | ||
| } | ||
| if (line.startsWith(":")) { | ||
| onComment && onComment(line.slice(line.startsWith(": ") ? 2 : 1)); | ||
| const firstCharCode = chunk.charCodeAt(start); | ||
| if (isDataPrefix(chunk, start, firstCharCode)) { | ||
| const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5, value2 = chunk.slice(valueStart, end); | ||
| data = dataLines === 0 ? value2 : `${data} | ||
| ${value2}`, dataLines++; | ||
| return; | ||
| } | ||
| const fieldSeparatorIndex = line.indexOf(":"); | ||
| if (fieldSeparatorIndex !== -1) { | ||
| const field = line.slice(0, fieldSeparatorIndex), offset = line[fieldSeparatorIndex + 1] === " " ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset); | ||
| processField(field, value, line); | ||
| if (isEventPrefix(chunk, start, firstCharCode)) { | ||
| eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0; | ||
| return; | ||
| } | ||
| processField(line, "", line); | ||
| if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) { | ||
| const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end); | ||
| id = value2.includes("\0") ? void 0 : value2; | ||
| return; | ||
| } | ||
| if (firstCharCode === 58) { | ||
| if (onComment) { | ||
| const line2 = chunk.slice(start, end); | ||
| onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1)); | ||
| } | ||
| return; | ||
| } | ||
| const line = chunk.slice(start, end), fieldSeparatorIndex = line.indexOf(":"); | ||
| if (fieldSeparatorIndex === -1) { | ||
| processField(line, "", line); | ||
| return; | ||
| } | ||
| const field = line.slice(0, fieldSeparatorIndex), offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset); | ||
| processField(field, value, line); | ||
| } | ||
@@ -45,7 +112,7 @@ function processField(field, value, line) { | ||
| case "event": | ||
| eventType = value; | ||
| eventType = value || void 0; | ||
| break; | ||
| case "data": | ||
| data = `${data}${value} | ||
| `; | ||
| data = dataLines === 0 ? value : `${data} | ||
| ${value}`, dataLines++; | ||
| break; | ||
@@ -75,36 +142,21 @@ case "id": | ||
| function dispatchEvent() { | ||
| data.length > 0 && onEvent({ | ||
| dataLines > 0 && onEvent({ | ||
| id, | ||
| event: eventType || void 0, | ||
| // If the data buffer's last character is a U+000A LINE FEED (LF) character, | ||
| // then remove the last character from the data buffer. | ||
| data: data.endsWith(` | ||
| `) ? data.slice(0, -1) : data | ||
| }), id = void 0, data = "", eventType = ""; | ||
| event: eventType, | ||
| data | ||
| }), id = void 0, data = "", dataLines = 0, eventType = void 0; | ||
| } | ||
| function reset(options = {}) { | ||
| incompleteLine && options.consume && parseLine(incompleteLine), isFirstChunk = !0, id = void 0, data = "", eventType = "", incompleteLine = ""; | ||
| incompleteLine && options.consume && parseLine(incompleteLine, 0, incompleteLine.length), isFirstChunk = !0, id = void 0, data = "", dataLines = 0, eventType = void 0, incompleteLine = ""; | ||
| } | ||
| return { feed, reset }; | ||
| } | ||
| function splitLines(chunk) { | ||
| const lines = []; | ||
| let incompleteLine = "", searchIndex = 0; | ||
| for (; searchIndex < chunk.length; ) { | ||
| const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| let lineEnd = -1; | ||
| if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = Math.min(crIndex, lfIndex) : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) { | ||
| incompleteLine = chunk.slice(searchIndex); | ||
| break; | ||
| } else { | ||
| const line = chunk.slice(searchIndex, lineEnd); | ||
| lines.push(line), searchIndex = lineEnd + 1, chunk[searchIndex - 1] === "\r" && chunk[searchIndex] === ` | ||
| ` && searchIndex++; | ||
| } | ||
| } | ||
| return [lines, incompleteLine]; | ||
| function isDataPrefix(chunk, i, firstCharCode) { | ||
| return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58; | ||
| } | ||
| function isEventPrefix(chunk, i, firstCharCode) { | ||
| return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58; | ||
| } | ||
| exports.ParseError = ParseError; | ||
| exports.createParser = createParser; | ||
| //# sourceMappingURL=index.cjs.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.cjs","sources":["../src/errors.ts","../src/parse.ts"],"sourcesContent":["/**\n * The type of error that occurred.\n * @public\n */\nexport type ErrorType = 'invalid-retry' | 'unknown-field'\n\n/**\n * Error thrown when encountering an issue during parsing.\n *\n * @public\n */\nexport class ParseError extends Error {\n /**\n * The type of error that occurred.\n */\n type: ErrorType\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the field name.\n */\n field?: string | undefined\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the value of the field.\n */\n value?: string | undefined\n\n /**\n * The line that caused the error, if available.\n */\n line?: string | undefined\n\n constructor(\n message: string,\n options: {type: ErrorType; field?: string; value?: string; line?: string},\n ) {\n super(message)\n this.name = 'ParseError'\n this.type = options.type\n this.field = options.field\n this.value = options.value\n this.line = options.line\n }\n}\n","/**\n * EventSource/Server-Sent Events parser\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html\n */\nimport {ParseError} from './errors.ts'\nimport type {EventSourceParser, ParserCallbacks} from './types.ts'\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nfunction noop(_arg: unknown) {\n // intentional noop\n}\n\n/**\n * Creates a new EventSource parser.\n *\n * @param callbacks - Callbacks to invoke on different parsing events:\n * - `onEvent` when a new event is parsed\n * - `onError` when an error occurs\n * - `onRetry` when a new reconnection interval has been sent from the server\n * - `onComment` when a comment is encountered in the stream\n *\n * @returns A new EventSource parser, with `parse` and `reset` methods.\n * @public\n */\nexport function createParser(callbacks: ParserCallbacks): EventSourceParser {\n if (typeof callbacks === 'function') {\n throw new TypeError(\n '`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?',\n )\n }\n\n const {onEvent = noop, onError = noop, onRetry = noop, onComment} = callbacks\n\n let incompleteLine = ''\n\n let isFirstChunk = true\n let id: string | undefined\n let data = ''\n let eventType = ''\n\n function feed(newChunk: string) {\n // Strip any UTF8 byte order mark (BOM) at the start of the stream\n const chunk = isFirstChunk ? newChunk.replace(/^\\xEF\\xBB\\xBF/, '') : newChunk\n\n // If there was a previous incomplete line, append it to the new chunk,\n // so we may process it together as a new (hopefully complete) chunk.\n const [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`)\n\n for (const line of complete) {\n parseLine(line)\n }\n\n incompleteLine = incomplete\n isFirstChunk = false\n }\n\n function parseLine(line: string) {\n // If the line is empty (a blank line), dispatch the event\n if (line === '') {\n dispatchEvent()\n return\n }\n\n // If the line starts with a U+003A COLON character (:), ignore the line.\n if (line.startsWith(':')) {\n if (onComment) {\n onComment(line.slice(line.startsWith(': ') ? 2 : 1))\n }\n return\n }\n\n // If the line contains a U+003A COLON character (:)\n const fieldSeparatorIndex = line.indexOf(':')\n if (fieldSeparatorIndex !== -1) {\n // Collect the characters on the line before the first U+003A COLON character (:),\n // and let `field` be that string.\n const field = line.slice(0, fieldSeparatorIndex)\n\n // Collect the characters on the line after the first U+003A COLON character (:),\n // and let `value` be that string. If value starts with a U+0020 SPACE character,\n // remove it from value.\n const offset = line[fieldSeparatorIndex + 1] === ' ' ? 2 : 1\n const value = line.slice(fieldSeparatorIndex + offset)\n\n processField(field, value, line)\n return\n }\n\n // Otherwise, the string is not empty but does not contain a U+003A COLON character (:)\n // Process the field using the whole line as the field name, and an empty string as the field value.\n // 👆 This is according to spec. That means that a line that has the value `data` will result in\n // a newline being added to the current `data` buffer, for instance.\n processField(line, '', line)\n }\n\n function processField(field: string, value: string, line: string) {\n // Field names must be compared literally, with no case folding performed.\n switch (field) {\n case 'event':\n // Set the `event type` buffer to field value\n eventType = value\n break\n case 'data':\n // Append the field value to the `data` buffer, then append a single U+000A LINE FEED(LF)\n // character to the `data` buffer.\n data = `${data}${value}\\n`\n break\n case 'id':\n // If the field value does not contain U+0000 NULL, then set the `ID` buffer to\n // the field value. Otherwise, ignore the field.\n id = value.includes('\\0') ? undefined : value\n break\n case 'retry':\n // If the field value consists of only ASCII digits, then interpret the field value as an\n // integer in base ten, and set the event stream's reconnection time to that integer.\n // Otherwise, ignore the field.\n if (/^\\d+$/.test(value)) {\n onRetry(parseInt(value, 10))\n } else {\n onError(\n new ParseError(`Invalid \\`retry\\` value: \"${value}\"`, {\n type: 'invalid-retry',\n value,\n line,\n }),\n )\n }\n break\n default:\n // Otherwise, the field is ignored.\n onError(\n new ParseError(\n `Unknown field \"${field.length > 20 ? `${field.slice(0, 20)}…` : field}\"`,\n {type: 'unknown-field', field, value, line},\n ),\n )\n break\n }\n }\n\n function dispatchEvent() {\n const shouldDispatch = data.length > 0\n if (shouldDispatch) {\n onEvent({\n id,\n event: eventType || undefined,\n // If the data buffer's last character is a U+000A LINE FEED (LF) character,\n // then remove the last character from the data buffer.\n data: data.endsWith('\\n') ? data.slice(0, -1) : data,\n })\n }\n\n // Reset for the next event\n id = undefined\n data = ''\n eventType = ''\n }\n\n function reset(options: {consume?: boolean} = {}) {\n if (incompleteLine && options.consume) {\n parseLine(incompleteLine)\n }\n\n isFirstChunk = true\n id = undefined\n data = ''\n eventType = ''\n incompleteLine = ''\n }\n\n return {feed, reset}\n}\n\n/**\n * For the given `chunk`, split it into lines according to spec, and return any remaining incomplete line.\n *\n * @param chunk - The chunk to split into lines\n * @returns A tuple containing an array of complete lines, and any remaining incomplete line\n * @internal\n */\nfunction splitLines(chunk: string): [complete: Array<string>, incomplete: string] {\n /**\n * According to the spec, a line is terminated by either:\n * - U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair\n * - a single U+000A LINE FEED(LF) character not preceded by a U+000D CARRIAGE RETURN(CR) character\n * - a single U+000D CARRIAGE RETURN(CR) character not followed by a U+000A LINE FEED(LF) character\n */\n const lines: Array<string> = []\n let incompleteLine = ''\n let searchIndex = 0\n\n while (searchIndex < chunk.length) {\n // Find next line terminator\n const crIndex = chunk.indexOf('\\r', searchIndex)\n const lfIndex = chunk.indexOf('\\n', searchIndex)\n\n // Determine line end\n let lineEnd = -1\n if (crIndex !== -1 && lfIndex !== -1) {\n // CRLF case\n lineEnd = Math.min(crIndex, lfIndex)\n } else if (crIndex !== -1) {\n // CR at the end of a chunk might be part of a CRLF sequence that spans chunks,\n // so we shouldn't treat it as a line terminator (yet)\n if (crIndex === chunk.length - 1) {\n lineEnd = -1\n } else {\n lineEnd = crIndex\n }\n } else if (lfIndex !== -1) {\n lineEnd = lfIndex\n }\n\n // Extract line if terminator found\n if (lineEnd === -1) {\n // No terminator found, rest is incomplete\n incompleteLine = chunk.slice(searchIndex)\n break\n } else {\n const line = chunk.slice(searchIndex, lineEnd)\n lines.push(line)\n\n // Move past line terminator\n searchIndex = lineEnd + 1\n if (chunk[searchIndex - 1] === '\\r' && chunk[searchIndex] === '\\n') {\n searchIndex++\n }\n }\n }\n\n return [lines, incompleteLine]\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"index.cjs","sources":["../src/errors.ts","../src/parse.ts"],"sourcesContent":["/**\n * The type of error that occurred.\n * @public\n */\nexport type ErrorType = 'invalid-retry' | 'unknown-field'\n\n/**\n * Error thrown when encountering an issue during parsing.\n *\n * @public\n */\nexport class ParseError extends Error {\n /**\n * The type of error that occurred.\n */\n type: ErrorType\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the field name.\n */\n field?: string | undefined\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the value of the field.\n */\n value?: string | undefined\n\n /**\n * The line that caused the error, if available.\n */\n line?: string | undefined\n\n constructor(\n message: string,\n options: {type: ErrorType; field?: string; value?: string; line?: string},\n ) {\n super(message)\n this.name = 'ParseError'\n this.type = options.type\n this.field = options.field\n this.value = options.value\n this.line = options.line\n }\n}\n","/**\n * EventSource/Server-Sent Events parser\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html\n */\nimport {ParseError} from './errors.ts'\nimport type {EventSourceParser, ParserCallbacks} from './types.ts'\n\n// ASCII codes used in the hot parsing paths.\nconst LF = 10\nconst CR = 13\nconst SPACE = 32\n\n// oxlint-disable-next-line no-unused-vars\nfunction noop(_arg: unknown) {\n // intentional noop\n}\n\n/**\n * Creates a new EventSource parser.\n *\n * @param callbacks - Callbacks to invoke on different parsing events:\n * - `onEvent` when a new event is parsed\n * - `onError` when an error occurs\n * - `onRetry` when a new reconnection interval has been sent from the server\n * - `onComment` when a comment is encountered in the stream\n *\n * @returns A new EventSource parser, with `parse` and `reset` methods.\n * @public\n */\nexport function createParser(callbacks: ParserCallbacks): EventSourceParser {\n if (typeof callbacks === 'function') {\n throw new TypeError(\n '`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?',\n )\n }\n\n const {onEvent = noop, onError = noop, onRetry = noop, onComment} = callbacks\n\n let incompleteLine = ''\n\n let isFirstChunk = true\n let id: string | undefined\n let data = ''\n let dataLines = 0\n let eventType: string | undefined\n\n /**\n * Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do\n * not yet form a complete line are held back and prepended to the next chunk,\n * so callers can pass arbitrary slices of the stream without worrying about\n * line boundaries.\n *\n * The first chunk is dispatched to {@link feedFirst} so the leading UTF-8\n * BOM can be stripped before parsing begins.\n */\n function feed(chunk: string) {\n if (isFirstChunk) {\n isFirstChunk = false\n feedFirst(chunk)\n return\n }\n const input = incompleteLine === '' ? chunk : incompleteLine + chunk\n incompleteLine = processLines(input)\n }\n\n /**\n * Handles the very first chunk of the stream. Per the SSE spec, a UTF-8 BOM\n * (0xEF 0xBB 0xBF) at the start of the stream must be stripped before\n * parsing. After BOM handling, behaves identically to {@link feed}.\n *\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream\n */\n function feedFirst(chunk: string) {\n if (\n chunk.charCodeAt(0) === 0xef &&\n chunk.charCodeAt(1) === 0xbb &&\n chunk.charCodeAt(2) === 0xbf\n ) {\n chunk = chunk.slice(3)\n }\n const input = incompleteLine === '' ? chunk : incompleteLine + chunk\n incompleteLine = processLines(input)\n }\n\n /**\n * Splits `chunk` into SSE lines and dispatches each to the appropriate handler.\n * Returns any trailing bytes that did not terminate with a line break, so the\n * caller can prepend them to the next chunk.\n *\n * The SSE spec permits three line terminators: `\\n`, `\\r`, and `\\r\\n`. Real-world\n * streams almost always use plain `\\n`, so we take a fast path when no `\\r` is\n * present in the chunk. The slow path is spec-correct but does more work per line.\n */\n function processLines(chunk: string): string {\n let searchIndex = 0\n\n // Fast path: LF-only chunk (the common case for typical SSE servers).\n // We can scan forward with a single `indexOf('\\n')` per line and inline\n // the hot-path branches for `data:` and `event:` without the CR bookkeeping\n // the slow path needs.\n if (chunk.indexOf('\\r') === -1) {\n let lfIndex = chunk.indexOf('\\n', searchIndex)\n while (lfIndex !== -1) {\n // Blank line: end-of-event marker. Dispatch the accumulated event (if any)\n // and reset the buffered fields. This is hoisted out of `parseLine` because\n // it's the single most common line shape after `data:` lines.\n if (searchIndex === lfIndex) {\n if (dataLines > 0) {\n onEvent({id, event: eventType, data})\n }\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n const firstCharCode = chunk.charCodeAt(searchIndex)\n if (isDataPrefix(chunk, searchIndex, firstCharCode)) {\n // `data:` line — append the value to the event's data buffer.\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart =\n chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5\n const value = chunk.slice(valueStart, lfIndex)\n // Fast path within a fast path: if this is the first data line AND the\n // next char is another LF (i.e. `data:foo\\n\\n`), dispatch immediately\n // without ever writing to the `data` buffer. This is the shape of a\n // typical single-line SSE event (ChatGPT-style streams, etc.) and is\n // hot enough to be worth the duplication.\n if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {\n onEvent({id, event: eventType, data: value})\n id = undefined\n data = ''\n eventType = undefined\n searchIndex = lfIndex + 2\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n // Multi-line data: concatenate with newline separator per spec.\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {\n // `event:` line — set the event type for the next dispatch. Per spec,\n // an empty value resets `event type` to its default (undefined here).\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(\n chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,\n lfIndex,\n ) || undefined\n } else {\n // Everything else: `id:`, `retry:`, comment lines (`:` prefix), unknown\n // fields, or malformed lines. These are rarer and go through the full\n // per-line parser, which handles the SSE field grammar in detail.\n parseLine(chunk, searchIndex, lfIndex)\n }\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n }\n return chunk.slice(searchIndex)\n }\n\n // Slow path: the chunk contains at least one `\\r`, so lines may be terminated\n // by `\\r`, `\\n`, or `\\r\\n`. We locate the next terminator by looking at both\n // the nearest `\\r` and `\\n` and picking whichever comes first.\n while (searchIndex < chunk.length) {\n const crIndex = chunk.indexOf('\\r', searchIndex)\n const lfIndex = chunk.indexOf('\\n', searchIndex)\n\n let lineEnd = -1\n if (crIndex !== -1 && lfIndex !== -1) {\n lineEnd = crIndex < lfIndex ? crIndex : lfIndex\n } else if (crIndex !== -1) {\n // A trailing `\\r` at the very end of the chunk is ambiguous: it could be\n // a bare-CR terminator, or the first half of a `\\r\\n` whose `\\n` arrives\n // in the next chunk. Defer until we see more input.\n if (crIndex === chunk.length - 1) {\n lineEnd = -1\n } else {\n lineEnd = crIndex\n }\n } else if (lfIndex !== -1) {\n lineEnd = lfIndex\n }\n\n if (lineEnd === -1) {\n break\n }\n\n parseLine(chunk, searchIndex, lineEnd)\n searchIndex = lineEnd + 1\n // If we just consumed a `\\r` and the next char is `\\n`, skip it so the\n // pair is treated as a single terminator rather than an empty line.\n if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {\n searchIndex++\n }\n }\n\n return chunk.slice(searchIndex)\n }\n\n function parseLine(chunk: string, start: number, end: number) {\n if (start === end) {\n dispatchEvent()\n return\n }\n\n const firstCharCode = chunk.charCodeAt(start)\n\n if (isDataPrefix(chunk, start, firstCharCode)) {\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5\n const value = chunk.slice(valueStart, end)\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n return\n }\n\n if (isEventPrefix(chunk, start, firstCharCode)) {\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || undefined\n return\n }\n\n // Fast path for \"id:\" — 'i' = 105, 'd' = 100, ':' = 58\n if (\n firstCharCode === 105 &&\n chunk.charCodeAt(start + 1) === 100 &&\n chunk.charCodeAt(start + 2) === 58\n ) {\n // 'id:'.length === 3, 'id: '.length === 4\n const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end)\n id = value.includes('\\0') ? undefined : value\n return\n }\n\n // Comment line — ':' = 58\n if (firstCharCode === 58) {\n if (onComment) {\n const line = chunk.slice(start, end)\n // skip ':' (+1), or ': ' (+2) when a space follows\n onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1))\n }\n return\n }\n\n const line = chunk.slice(start, end)\n const fieldSeparatorIndex = line.indexOf(':')\n if (fieldSeparatorIndex === -1) {\n processField(line, '', line)\n return\n }\n\n const field = line.slice(0, fieldSeparatorIndex)\n // skip ':' (+1), or ': ' (+2) when a space follows\n const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1\n const value = line.slice(fieldSeparatorIndex + offset)\n processField(field, value, line)\n }\n\n function processField(field: string, value: string, line: string) {\n // Field names must be compared literally, with no case folding performed.\n switch (field) {\n case 'event':\n // Set the `event type` buffer to field value\n eventType = value || undefined\n break\n case 'data':\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n break\n case 'id':\n // If the field value does not contain U+0000 NULL, then set the `ID` buffer to\n // the field value. Otherwise, ignore the field.\n id = value.includes('\\0') ? undefined : value\n break\n case 'retry':\n // If the field value consists of only ASCII digits, then interpret the field value as an\n // integer in base ten, and set the event stream's reconnection time to that integer.\n // Otherwise, ignore the field.\n if (/^\\d+$/.test(value)) {\n onRetry(parseInt(value, 10))\n } else {\n onError(\n new ParseError(`Invalid \\`retry\\` value: \"${value}\"`, {\n type: 'invalid-retry',\n value,\n line,\n }),\n )\n }\n break\n default:\n // Otherwise, the field is ignored.\n onError(\n new ParseError(\n `Unknown field \"${field.length > 20 ? `${field.slice(0, 20)}…` : field}\"`,\n {type: 'unknown-field', field, value, line},\n ),\n )\n break\n }\n }\n\n function dispatchEvent() {\n if (dataLines > 0) {\n onEvent({\n id,\n event: eventType,\n data,\n })\n }\n\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n }\n\n function reset(options: {consume?: boolean} = {}) {\n if (incompleteLine && options.consume) {\n parseLine(incompleteLine, 0, incompleteLine.length)\n }\n\n isFirstChunk = true\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n incompleteLine = ''\n }\n\n return {feed, reset}\n}\n\n/**\n * Checks if `chunk` starts with the literal `data:` at index `i`.\n *\n * Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this\n * hand-unrolled char-code comparison is ~20% faster on common event types.\n * The caller passes `firstCharCode` (the code at `i`) so it can be reused\n * across prefix checks.\n *\n * ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58\n */\nfunction isDataPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 100 &&\n chunk.charCodeAt(i + 1) === 97 &&\n chunk.charCodeAt(i + 2) === 116 &&\n chunk.charCodeAt(i + 3) === 97 &&\n chunk.charCodeAt(i + 4) === 58\n )\n}\n\n/**\n * Checks if `chunk` starts with the literal `event:` at index `i`.\n *\n * See {@link isDataPrefix} for why this is hand-unrolled rather than using\n * `String.prototype.startsWith`.\n *\n * ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58\n */\nfunction isEventPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 101 &&\n chunk.charCodeAt(i + 1) === 118 &&\n chunk.charCodeAt(i + 2) === 101 &&\n chunk.charCodeAt(i + 3) === 110 &&\n chunk.charCodeAt(i + 4) === 116 &&\n chunk.charCodeAt(i + 5) === 58\n )\n}\n"],"names":["value","line"],"mappings":";;AAWO,MAAM,mBAAmB,MAAM;AAAA,EAqBpC,YACE,SACA,SACA;AACA,UAAM,OAAO,GACb,KAAK,OAAO,cACZ,KAAK,OAAO,QAAQ,MACpB,KAAK,QAAQ,QAAQ,OACrB,KAAK,QAAQ,QAAQ,OACrB,KAAK,OAAO,QAAQ;AAAA,EACtB;AACF;ACnCA,MAAM,KAAK,IACL,KAAK,IACL,QAAQ;AAGd,SAAS,KAAK,MAAe;AAE7B;AAcO,SAAS,aAAa,WAA+C;AAC1E,MAAI,OAAO,aAAc;AACvB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAM,EAAC,UAAU,MAAM,UAAU,MAAM,UAAU,MAAM,cAAa;AAEpE,MAAI,iBAAiB,IAEjB,eAAe,IACf,IACA,OAAO,IACP,YAAY,GACZ;AAWJ,WAAS,KAAK,OAAe;AAC3B,QAAI,cAAc;AAChB,qBAAe,IACf,UAAU,KAAK;AACf;AAAA,IACF;AACA,UAAM,QAAQ,mBAAmB,KAAK,QAAQ,iBAAiB;AAC/D,qBAAiB,aAAa,KAAK;AAAA,EACrC;AASA,WAAS,UAAU,OAAe;AAE9B,UAAM,WAAW,CAAC,MAAM,OACxB,MAAM,WAAW,CAAC,MAAM,OACxB,MAAM,WAAW,CAAC,MAAM,QAExB,QAAQ,MAAM,MAAM,CAAC;AAEvB,UAAM,QAAQ,mBAAmB,KAAK,QAAQ,iBAAiB;AAC/D,qBAAiB,aAAa,KAAK;AAAA,EACrC;AAWA,WAAS,aAAa,OAAuB;AAC3C,QAAI,cAAc;AAMlB,QAAI,MAAM,QAAQ,IAAI,MAAM,IAAI;AAC9B,UAAI,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AAC7C,aAAO,YAAY,MAAI;AAIrB,YAAI,gBAAgB,SAAS;AACvB,sBAAY,KACd,QAAQ,EAAC,IAAI,OAAO,WAAW,KAAA,CAAK,GAEtC,KAAK,QACL,OAAO,IACP,YAAY,GACZ,YAAY,QACZ,cAAc,UAAU,GACxB,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AACzC;AAAA,QACF;AACA,cAAM,gBAAgB,MAAM,WAAW,WAAW;AAClD,YAAI,aAAa,OAAO,aAAa,aAAa,GAAG;AAGnD,gBAAM,aACJ,MAAM,WAAW,cAAc,CAAC,MAAM,QAAQ,cAAc,IAAI,cAAc,GAC1E,QAAQ,MAAM,MAAM,YAAY,OAAO;AAM7C,cAAI,cAAc,KAAK,MAAM,WAAW,UAAU,CAAC,MAAM,IAAI;AAC3D,oBAAQ,EAAC,IAAI,OAAO,WAAW,MAAM,MAAA,CAAM,GAC3C,KAAK,QACL,OAAO,IACP,YAAY,QACZ,cAAc,UAAU,GACxB,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AACzC;AAAA,UACF;AAEA,iBAAO,cAAc,IAAI,QAAQ,GAAG,IAAI;AAAA,EAAK,KAAK,IAClD;AAAA,QACF,MAAW,eAAc,OAAO,aAAa,aAAa,IAIxD,YACE,MAAM;AAAA,UACJ,MAAM,WAAW,cAAc,CAAC,MAAM,QAAQ,cAAc,IAAI,cAAc;AAAA,UAC9E;AAAA,QAAA,KACG,SAKP,UAAU,OAAO,aAAa,OAAO;AAEvC,sBAAc,UAAU,GACxB,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AAAA,MAC3C;AACA,aAAO,MAAM,MAAM,WAAW;AAAA,IAChC;AAKA,WAAO,cAAc,MAAM,UAAQ;AACjC,YAAM,UAAU,MAAM,QAAQ,MAAM,WAAW,GACzC,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AAE/C,UAAI,UAAU;AAgBd,UAfI,YAAY,MAAM,YAAY,KAChC,UAAU,UAAU,UAAU,UAAU,UAC/B,YAAY,KAIjB,YAAY,MAAM,SAAS,IAC7B,UAAU,KAEV,UAAU,UAEH,YAAY,OACrB,UAAU,UAGR,YAAY;AACd;AAGF,gBAAU,OAAO,aAAa,OAAO,GACrC,cAAc,UAAU,GAGpB,MAAM,WAAW,cAAc,CAAC,MAAM,MAAM,MAAM,WAAW,WAAW,MAAM,MAChF;AAAA,IAEJ;AAEA,WAAO,MAAM,MAAM,WAAW;AAAA,EAChC;AAEA,WAAS,UAAU,OAAe,OAAe,KAAa;AAC5D,QAAI,UAAU,KAAK;AACjB,oBAAA;AACA;AAAA,IACF;AAEA,UAAM,gBAAgB,MAAM,WAAW,KAAK;AAE5C,QAAI,aAAa,OAAO,OAAO,aAAa,GAAG;AAE7C,YAAM,aAAa,MAAM,WAAW,QAAQ,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,GACzEA,SAAQ,MAAM,MAAM,YAAY,GAAG;AACzC,aAAO,cAAc,IAAIA,SAAQ,GAAG,IAAI;AAAA,EAAKA,MAAK,IAClD;AACA;AAAA,IACF;AAEA,QAAI,cAAc,OAAO,OAAO,aAAa,GAAG;AAE9C,kBACE,MAAM,MAAM,MAAM,WAAW,QAAQ,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,GAAG,GAAG,KAAK;AACrF;AAAA,IACF;AAGA,QACE,kBAAkB,OAClB,MAAM,WAAW,QAAQ,CAAC,MAAM,OAChC,MAAM,WAAW,QAAQ,CAAC,MAAM,IAChC;AAEA,YAAMA,SAAQ,MAAM,MAAM,MAAM,WAAW,QAAQ,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,GAAG,GAAG;AAC5F,WAAKA,OAAM,SAAS,IAAI,IAAI,SAAYA;AACxC;AAAA,IACF;AAGA,QAAI,kBAAkB,IAAI;AACxB,UAAI,WAAW;AACb,cAAMC,QAAO,MAAM,MAAM,OAAO,GAAG;AAEnC,kBAAUA,MAAK,MAAM,MAAM,WAAW,QAAQ,CAAC,MAAM,QAAQ,IAAI,CAAC,CAAC;AAAA,MACrE;AACA;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,MAAM,OAAO,GAAG,GAC7B,sBAAsB,KAAK,QAAQ,GAAG;AAC5C,QAAI,wBAAwB,IAAI;AAC9B,mBAAa,MAAM,IAAI,IAAI;AAC3B;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK,MAAM,GAAG,mBAAmB,GAEzC,SAAS,KAAK,WAAW,sBAAsB,CAAC,MAAM,QAAQ,IAAI,GAClE,QAAQ,KAAK,MAAM,sBAAsB,MAAM;AACrD,iBAAa,OAAO,OAAO,IAAI;AAAA,EACjC;AAEA,WAAS,aAAa,OAAe,OAAe,MAAc;AAEhE,YAAQ,OAAA;AAAA,MACN,KAAK;AAEH,oBAAY,SAAS;AACrB;AAAA,MACF,KAAK;AACH,eAAO,cAAc,IAAI,QAAQ,GAAG,IAAI;AAAA,EAAK,KAAK,IAClD;AACA;AAAA,MACF,KAAK;AAGH,aAAK,MAAM,SAAS,IAAI,IAAI,SAAY;AACxC;AAAA,MACF,KAAK;AAIC,gBAAQ,KAAK,KAAK,IACpB,QAAQ,SAAS,OAAO,EAAE,CAAC,IAE3B;AAAA,UACE,IAAI,WAAW,6BAA6B,KAAK,KAAK;AAAA,YACpD,MAAM;AAAA,YACN;AAAA,YACA;AAAA,UAAA,CACD;AAAA,QAAA;AAGL;AAAA,MACF;AAEE;AAAA,UACE,IAAI;AAAA,YACF,kBAAkB,MAAM,SAAS,KAAK,GAAG,MAAM,MAAM,GAAG,EAAE,CAAC,WAAM,KAAK;AAAA,YACtE,EAAC,MAAM,iBAAiB,OAAO,OAAO,KAAA;AAAA,UAAI;AAAA,QAC5C;AAEF;AAAA,IAAA;AAAA,EAEN;AAEA,WAAS,gBAAgB;AACnB,gBAAY,KACd,QAAQ;AAAA,MACN;AAAA,MACA,OAAO;AAAA,MACP;AAAA,IAAA,CACD,GAGH,KAAK,QACL,OAAO,IACP,YAAY,GACZ,YAAY;AAAA,EACd;AAEA,WAAS,MAAM,UAA+B,IAAI;AAC5C,sBAAkB,QAAQ,WAC5B,UAAU,gBAAgB,GAAG,eAAe,MAAM,GAGpD,eAAe,IACf,KAAK,QACL,OAAO,IACP,YAAY,GACZ,YAAY,QACZ,iBAAiB;AAAA,EACnB;AAEA,SAAO,EAAC,MAAM,MAAA;AAChB;AAYA,SAAS,aAAa,OAAe,GAAW,eAAgC;AAC9E,SACE,kBAAkB,OAClB,MAAM,WAAW,IAAI,CAAC,MAAM,MAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,MAC5B,MAAM,WAAW,IAAI,CAAC,MAAM;AAEhC;AAUA,SAAS,cAAc,OAAe,GAAW,eAAgC;AAC/E,SACE,kBAAkB,OAClB,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM;AAEhC;;;"} |
+23
-21
@@ -13,3 +13,5 @@ /** | ||
| */ | ||
| export declare function createParser(callbacks: ParserCallbacks): EventSourceParser | ||
| export declare function createParser( | ||
| callbacks: ParserCallbacks, | ||
| ): EventSourceParser; | ||
@@ -20,3 +22,3 @@ /** | ||
| */ | ||
| export declare type ErrorType = 'invalid-retry' | 'unknown-field' | ||
| export declare type ErrorType = "invalid-retry" | "unknown-field"; | ||
@@ -34,3 +36,3 @@ /** | ||
| */ | ||
| event?: string | undefined | ||
| event?: string | undefined; | ||
| /** | ||
@@ -40,7 +42,7 @@ * ID of the message, if any was provided by the server. Can be used by clients to keep the | ||
| */ | ||
| id?: string | undefined | ||
| id?: string | undefined; | ||
| /** | ||
| * The data received for this message | ||
| */ | ||
| data: string | ||
| data: string; | ||
| } | ||
@@ -64,3 +66,3 @@ | ||
| */ | ||
| feed(chunk: string): void | ||
| feed(chunk: string): void; | ||
| /** | ||
@@ -77,3 +79,3 @@ * Resets the parser state. This is required when you have a new stream of messages - | ||
| */ | ||
| reset(options?: {consume?: boolean}): void | ||
| reset(options?: { consume?: boolean }): void; | ||
| } | ||
@@ -90,24 +92,24 @@ | ||
| */ | ||
| type: ErrorType | ||
| type: ErrorType; | ||
| /** | ||
| * In the case of an unknown field encountered in the stream, this will be the field name. | ||
| */ | ||
| field?: string | undefined | ||
| field?: string | undefined; | ||
| /** | ||
| * In the case of an unknown field encountered in the stream, this will be the value of the field. | ||
| */ | ||
| value?: string | undefined | ||
| value?: string | undefined; | ||
| /** | ||
| * The line that caused the error, if available. | ||
| */ | ||
| line?: string | undefined | ||
| line?: string | undefined; | ||
| constructor( | ||
| message: string, | ||
| options: { | ||
| type: ErrorType | ||
| field?: string | ||
| value?: string | ||
| line?: string | ||
| type: ErrorType; | ||
| field?: string; | ||
| value?: string; | ||
| line?: string; | ||
| }, | ||
| ) | ||
| ); | ||
| } | ||
@@ -128,3 +130,3 @@ | ||
| */ | ||
| onEvent?: ((event: EventSourceMessage) => void) | undefined | ||
| onEvent?: ((event: EventSourceMessage) => void) | undefined; | ||
| /** | ||
@@ -135,3 +137,3 @@ * Callback for when the server sends a new reconnection interval through the `retry` field. | ||
| */ | ||
| onRetry?: ((retry: number) => void) | undefined | ||
| onRetry?: ((retry: number) => void) | undefined; | ||
| /** | ||
@@ -142,3 +144,3 @@ * Callback for when a comment is encountered in the stream. | ||
| */ | ||
| onComment?: ((comment: string) => void) | undefined | ||
| onComment?: ((comment: string) => void) | undefined; | ||
| /** | ||
@@ -151,5 +153,5 @@ * Callback for when an error occurs during parsing. This is a catch-all for any errors | ||
| */ | ||
| onError?: ((error: ParseError) => void) | undefined | ||
| onError?: ((error: ParseError) => void) | undefined; | ||
| } | ||
| export {} | ||
| export {}; |
+23
-21
@@ -13,3 +13,5 @@ /** | ||
| */ | ||
| export declare function createParser(callbacks: ParserCallbacks): EventSourceParser | ||
| export declare function createParser( | ||
| callbacks: ParserCallbacks, | ||
| ): EventSourceParser; | ||
@@ -20,3 +22,3 @@ /** | ||
| */ | ||
| export declare type ErrorType = 'invalid-retry' | 'unknown-field' | ||
| export declare type ErrorType = "invalid-retry" | "unknown-field"; | ||
@@ -34,3 +36,3 @@ /** | ||
| */ | ||
| event?: string | undefined | ||
| event?: string | undefined; | ||
| /** | ||
@@ -40,7 +42,7 @@ * ID of the message, if any was provided by the server. Can be used by clients to keep the | ||
| */ | ||
| id?: string | undefined | ||
| id?: string | undefined; | ||
| /** | ||
| * The data received for this message | ||
| */ | ||
| data: string | ||
| data: string; | ||
| } | ||
@@ -64,3 +66,3 @@ | ||
| */ | ||
| feed(chunk: string): void | ||
| feed(chunk: string): void; | ||
| /** | ||
@@ -77,3 +79,3 @@ * Resets the parser state. This is required when you have a new stream of messages - | ||
| */ | ||
| reset(options?: {consume?: boolean}): void | ||
| reset(options?: { consume?: boolean }): void; | ||
| } | ||
@@ -90,24 +92,24 @@ | ||
| */ | ||
| type: ErrorType | ||
| type: ErrorType; | ||
| /** | ||
| * In the case of an unknown field encountered in the stream, this will be the field name. | ||
| */ | ||
| field?: string | undefined | ||
| field?: string | undefined; | ||
| /** | ||
| * In the case of an unknown field encountered in the stream, this will be the value of the field. | ||
| */ | ||
| value?: string | undefined | ||
| value?: string | undefined; | ||
| /** | ||
| * The line that caused the error, if available. | ||
| */ | ||
| line?: string | undefined | ||
| line?: string | undefined; | ||
| constructor( | ||
| message: string, | ||
| options: { | ||
| type: ErrorType | ||
| field?: string | ||
| value?: string | ||
| line?: string | ||
| type: ErrorType; | ||
| field?: string; | ||
| value?: string; | ||
| line?: string; | ||
| }, | ||
| ) | ||
| ); | ||
| } | ||
@@ -128,3 +130,3 @@ | ||
| */ | ||
| onEvent?: ((event: EventSourceMessage) => void) | undefined | ||
| onEvent?: ((event: EventSourceMessage) => void) | undefined; | ||
| /** | ||
@@ -135,3 +137,3 @@ * Callback for when the server sends a new reconnection interval through the `retry` field. | ||
| */ | ||
| onRetry?: ((retry: number) => void) | undefined | ||
| onRetry?: ((retry: number) => void) | undefined; | ||
| /** | ||
@@ -142,3 +144,3 @@ * Callback for when a comment is encountered in the stream. | ||
| */ | ||
| onComment?: ((comment: string) => void) | undefined | ||
| onComment?: ((comment: string) => void) | undefined; | ||
| /** | ||
@@ -151,5 +153,5 @@ * Callback for when an error occurs during parsing. This is a catch-all for any errors | ||
| */ | ||
| onError?: ((error: ParseError) => void) | undefined | ||
| onError?: ((error: ParseError) => void) | undefined; | ||
| } | ||
| export {} | ||
| export {}; |
+95
-43
@@ -6,2 +6,3 @@ class ParseError extends Error { | ||
| } | ||
| const LF = 10, CR = 13, SPACE = 32; | ||
| function noop(_arg) { | ||
@@ -15,25 +16,91 @@ } | ||
| const { onEvent = noop, onError = noop, onRetry = noop, onComment } = callbacks; | ||
| let incompleteLine = "", isFirstChunk = !0, id, data = "", eventType = ""; | ||
| function feed(newChunk) { | ||
| const chunk = isFirstChunk ? newChunk.replace(/^\xEF\xBB\xBF/, "") : newChunk, [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`); | ||
| for (const line of complete) | ||
| parseLine(line); | ||
| incompleteLine = incomplete, isFirstChunk = !1; | ||
| let incompleteLine = "", isFirstChunk = !0, id, data = "", dataLines = 0, eventType; | ||
| function feed(chunk) { | ||
| if (isFirstChunk) { | ||
| isFirstChunk = !1, feedFirst(chunk); | ||
| return; | ||
| } | ||
| const input = incompleteLine === "" ? chunk : incompleteLine + chunk; | ||
| incompleteLine = processLines(input); | ||
| } | ||
| function parseLine(line) { | ||
| if (line === "") { | ||
| function feedFirst(chunk) { | ||
| chunk.charCodeAt(0) === 239 && chunk.charCodeAt(1) === 187 && chunk.charCodeAt(2) === 191 && (chunk = chunk.slice(3)); | ||
| const input = incompleteLine === "" ? chunk : incompleteLine + chunk; | ||
| incompleteLine = processLines(input); | ||
| } | ||
| function processLines(chunk) { | ||
| let searchIndex = 0; | ||
| if (chunk.indexOf("\r") === -1) { | ||
| let lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| for (; lfIndex !== -1; ) { | ||
| if (searchIndex === lfIndex) { | ||
| dataLines > 0 && onEvent({ id, event: eventType, data }), id = void 0, data = "", dataLines = 0, eventType = void 0, searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| continue; | ||
| } | ||
| const firstCharCode = chunk.charCodeAt(searchIndex); | ||
| if (isDataPrefix(chunk, searchIndex, firstCharCode)) { | ||
| const valueStart = chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5, value = chunk.slice(valueStart, lfIndex); | ||
| if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) { | ||
| onEvent({ id, event: eventType, data: value }), id = void 0, data = "", eventType = void 0, searchIndex = lfIndex + 2, lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| continue; | ||
| } | ||
| data = dataLines === 0 ? value : `${data} | ||
| ${value}`, dataLines++; | ||
| } else isEventPrefix(chunk, searchIndex, firstCharCode) ? eventType = chunk.slice( | ||
| chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, | ||
| lfIndex | ||
| ) || void 0 : parseLine(chunk, searchIndex, lfIndex); | ||
| searchIndex = lfIndex + 1, lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| } | ||
| return chunk.slice(searchIndex); | ||
| } | ||
| for (; searchIndex < chunk.length; ) { | ||
| const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| let lineEnd = -1; | ||
| if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = crIndex < lfIndex ? crIndex : lfIndex : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) | ||
| break; | ||
| parseLine(chunk, searchIndex, lineEnd), searchIndex = lineEnd + 1, chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF && searchIndex++; | ||
| } | ||
| return chunk.slice(searchIndex); | ||
| } | ||
| function parseLine(chunk, start, end) { | ||
| if (start === end) { | ||
| dispatchEvent(); | ||
| return; | ||
| } | ||
| if (line.startsWith(":")) { | ||
| onComment && onComment(line.slice(line.startsWith(": ") ? 2 : 1)); | ||
| const firstCharCode = chunk.charCodeAt(start); | ||
| if (isDataPrefix(chunk, start, firstCharCode)) { | ||
| const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5, value2 = chunk.slice(valueStart, end); | ||
| data = dataLines === 0 ? value2 : `${data} | ||
| ${value2}`, dataLines++; | ||
| return; | ||
| } | ||
| const fieldSeparatorIndex = line.indexOf(":"); | ||
| if (fieldSeparatorIndex !== -1) { | ||
| const field = line.slice(0, fieldSeparatorIndex), offset = line[fieldSeparatorIndex + 1] === " " ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset); | ||
| processField(field, value, line); | ||
| if (isEventPrefix(chunk, start, firstCharCode)) { | ||
| eventType = chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || void 0; | ||
| return; | ||
| } | ||
| processField(line, "", line); | ||
| if (firstCharCode === 105 && chunk.charCodeAt(start + 1) === 100 && chunk.charCodeAt(start + 2) === 58) { | ||
| const value2 = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end); | ||
| id = value2.includes("\0") ? void 0 : value2; | ||
| return; | ||
| } | ||
| if (firstCharCode === 58) { | ||
| if (onComment) { | ||
| const line2 = chunk.slice(start, end); | ||
| onComment(line2.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1)); | ||
| } | ||
| return; | ||
| } | ||
| const line = chunk.slice(start, end), fieldSeparatorIndex = line.indexOf(":"); | ||
| if (fieldSeparatorIndex === -1) { | ||
| processField(line, "", line); | ||
| return; | ||
| } | ||
| const field = line.slice(0, fieldSeparatorIndex), offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1, value = line.slice(fieldSeparatorIndex + offset); | ||
| processField(field, value, line); | ||
| } | ||
@@ -43,7 +110,7 @@ function processField(field, value, line) { | ||
| case "event": | ||
| eventType = value; | ||
| eventType = value || void 0; | ||
| break; | ||
| case "data": | ||
| data = `${data}${value} | ||
| `; | ||
| data = dataLines === 0 ? value : `${data} | ||
| ${value}`, dataLines++; | ||
| break; | ||
@@ -73,34 +140,19 @@ case "id": | ||
| function dispatchEvent() { | ||
| data.length > 0 && onEvent({ | ||
| dataLines > 0 && onEvent({ | ||
| id, | ||
| event: eventType || void 0, | ||
| // If the data buffer's last character is a U+000A LINE FEED (LF) character, | ||
| // then remove the last character from the data buffer. | ||
| data: data.endsWith(` | ||
| `) ? data.slice(0, -1) : data | ||
| }), id = void 0, data = "", eventType = ""; | ||
| event: eventType, | ||
| data | ||
| }), id = void 0, data = "", dataLines = 0, eventType = void 0; | ||
| } | ||
| function reset(options = {}) { | ||
| incompleteLine && options.consume && parseLine(incompleteLine), isFirstChunk = !0, id = void 0, data = "", eventType = "", incompleteLine = ""; | ||
| incompleteLine && options.consume && parseLine(incompleteLine, 0, incompleteLine.length), isFirstChunk = !0, id = void 0, data = "", dataLines = 0, eventType = void 0, incompleteLine = ""; | ||
| } | ||
| return { feed, reset }; | ||
| } | ||
| function splitLines(chunk) { | ||
| const lines = []; | ||
| let incompleteLine = "", searchIndex = 0; | ||
| for (; searchIndex < chunk.length; ) { | ||
| const crIndex = chunk.indexOf("\r", searchIndex), lfIndex = chunk.indexOf(` | ||
| `, searchIndex); | ||
| let lineEnd = -1; | ||
| if (crIndex !== -1 && lfIndex !== -1 ? lineEnd = Math.min(crIndex, lfIndex) : crIndex !== -1 ? crIndex === chunk.length - 1 ? lineEnd = -1 : lineEnd = crIndex : lfIndex !== -1 && (lineEnd = lfIndex), lineEnd === -1) { | ||
| incompleteLine = chunk.slice(searchIndex); | ||
| break; | ||
| } else { | ||
| const line = chunk.slice(searchIndex, lineEnd); | ||
| lines.push(line), searchIndex = lineEnd + 1, chunk[searchIndex - 1] === "\r" && chunk[searchIndex] === ` | ||
| ` && searchIndex++; | ||
| } | ||
| } | ||
| return [lines, incompleteLine]; | ||
| function isDataPrefix(chunk, i, firstCharCode) { | ||
| return firstCharCode === 100 && chunk.charCodeAt(i + 1) === 97 && chunk.charCodeAt(i + 2) === 116 && chunk.charCodeAt(i + 3) === 97 && chunk.charCodeAt(i + 4) === 58; | ||
| } | ||
| function isEventPrefix(chunk, i, firstCharCode) { | ||
| return firstCharCode === 101 && chunk.charCodeAt(i + 1) === 118 && chunk.charCodeAt(i + 2) === 101 && chunk.charCodeAt(i + 3) === 110 && chunk.charCodeAt(i + 4) === 116 && chunk.charCodeAt(i + 5) === 58; | ||
| } | ||
| export { | ||
@@ -107,0 +159,0 @@ ParseError, |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","sources":["../src/errors.ts","../src/parse.ts"],"sourcesContent":["/**\n * The type of error that occurred.\n * @public\n */\nexport type ErrorType = 'invalid-retry' | 'unknown-field'\n\n/**\n * Error thrown when encountering an issue during parsing.\n *\n * @public\n */\nexport class ParseError extends Error {\n /**\n * The type of error that occurred.\n */\n type: ErrorType\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the field name.\n */\n field?: string | undefined\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the value of the field.\n */\n value?: string | undefined\n\n /**\n * The line that caused the error, if available.\n */\n line?: string | undefined\n\n constructor(\n message: string,\n options: {type: ErrorType; field?: string; value?: string; line?: string},\n ) {\n super(message)\n this.name = 'ParseError'\n this.type = options.type\n this.field = options.field\n this.value = options.value\n this.line = options.line\n }\n}\n","/**\n * EventSource/Server-Sent Events parser\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html\n */\nimport {ParseError} from './errors.ts'\nimport type {EventSourceParser, ParserCallbacks} from './types.ts'\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nfunction noop(_arg: unknown) {\n // intentional noop\n}\n\n/**\n * Creates a new EventSource parser.\n *\n * @param callbacks - Callbacks to invoke on different parsing events:\n * - `onEvent` when a new event is parsed\n * - `onError` when an error occurs\n * - `onRetry` when a new reconnection interval has been sent from the server\n * - `onComment` when a comment is encountered in the stream\n *\n * @returns A new EventSource parser, with `parse` and `reset` methods.\n * @public\n */\nexport function createParser(callbacks: ParserCallbacks): EventSourceParser {\n if (typeof callbacks === 'function') {\n throw new TypeError(\n '`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?',\n )\n }\n\n const {onEvent = noop, onError = noop, onRetry = noop, onComment} = callbacks\n\n let incompleteLine = ''\n\n let isFirstChunk = true\n let id: string | undefined\n let data = ''\n let eventType = ''\n\n function feed(newChunk: string) {\n // Strip any UTF8 byte order mark (BOM) at the start of the stream\n const chunk = isFirstChunk ? newChunk.replace(/^\\xEF\\xBB\\xBF/, '') : newChunk\n\n // If there was a previous incomplete line, append it to the new chunk,\n // so we may process it together as a new (hopefully complete) chunk.\n const [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`)\n\n for (const line of complete) {\n parseLine(line)\n }\n\n incompleteLine = incomplete\n isFirstChunk = false\n }\n\n function parseLine(line: string) {\n // If the line is empty (a blank line), dispatch the event\n if (line === '') {\n dispatchEvent()\n return\n }\n\n // If the line starts with a U+003A COLON character (:), ignore the line.\n if (line.startsWith(':')) {\n if (onComment) {\n onComment(line.slice(line.startsWith(': ') ? 2 : 1))\n }\n return\n }\n\n // If the line contains a U+003A COLON character (:)\n const fieldSeparatorIndex = line.indexOf(':')\n if (fieldSeparatorIndex !== -1) {\n // Collect the characters on the line before the first U+003A COLON character (:),\n // and let `field` be that string.\n const field = line.slice(0, fieldSeparatorIndex)\n\n // Collect the characters on the line after the first U+003A COLON character (:),\n // and let `value` be that string. If value starts with a U+0020 SPACE character,\n // remove it from value.\n const offset = line[fieldSeparatorIndex + 1] === ' ' ? 2 : 1\n const value = line.slice(fieldSeparatorIndex + offset)\n\n processField(field, value, line)\n return\n }\n\n // Otherwise, the string is not empty but does not contain a U+003A COLON character (:)\n // Process the field using the whole line as the field name, and an empty string as the field value.\n // 👆 This is according to spec. That means that a line that has the value `data` will result in\n // a newline being added to the current `data` buffer, for instance.\n processField(line, '', line)\n }\n\n function processField(field: string, value: string, line: string) {\n // Field names must be compared literally, with no case folding performed.\n switch (field) {\n case 'event':\n // Set the `event type` buffer to field value\n eventType = value\n break\n case 'data':\n // Append the field value to the `data` buffer, then append a single U+000A LINE FEED(LF)\n // character to the `data` buffer.\n data = `${data}${value}\\n`\n break\n case 'id':\n // If the field value does not contain U+0000 NULL, then set the `ID` buffer to\n // the field value. Otherwise, ignore the field.\n id = value.includes('\\0') ? undefined : value\n break\n case 'retry':\n // If the field value consists of only ASCII digits, then interpret the field value as an\n // integer in base ten, and set the event stream's reconnection time to that integer.\n // Otherwise, ignore the field.\n if (/^\\d+$/.test(value)) {\n onRetry(parseInt(value, 10))\n } else {\n onError(\n new ParseError(`Invalid \\`retry\\` value: \"${value}\"`, {\n type: 'invalid-retry',\n value,\n line,\n }),\n )\n }\n break\n default:\n // Otherwise, the field is ignored.\n onError(\n new ParseError(\n `Unknown field \"${field.length > 20 ? `${field.slice(0, 20)}…` : field}\"`,\n {type: 'unknown-field', field, value, line},\n ),\n )\n break\n }\n }\n\n function dispatchEvent() {\n const shouldDispatch = data.length > 0\n if (shouldDispatch) {\n onEvent({\n id,\n event: eventType || undefined,\n // If the data buffer's last character is a U+000A LINE FEED (LF) character,\n // then remove the last character from the data buffer.\n data: data.endsWith('\\n') ? data.slice(0, -1) : data,\n })\n }\n\n // Reset for the next event\n id = undefined\n data = ''\n eventType = ''\n }\n\n function reset(options: {consume?: boolean} = {}) {\n if (incompleteLine && options.consume) {\n parseLine(incompleteLine)\n }\n\n isFirstChunk = true\n id = undefined\n data = ''\n eventType = ''\n incompleteLine = ''\n }\n\n return {feed, reset}\n}\n\n/**\n * For the given `chunk`, split it into lines according to spec, and return any remaining incomplete line.\n *\n * @param chunk - The chunk to split into lines\n * @returns A tuple containing an array of complete lines, and any remaining incomplete line\n * @internal\n */\nfunction splitLines(chunk: string): [complete: Array<string>, incomplete: string] {\n /**\n * According to the spec, a line is terminated by either:\n * - U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair\n * - a single U+000A LINE FEED(LF) character not preceded by a U+000D CARRIAGE RETURN(CR) character\n * - a single U+000D CARRIAGE RETURN(CR) character not followed by a U+000A LINE FEED(LF) character\n */\n const lines: Array<string> = []\n let incompleteLine = ''\n let searchIndex = 0\n\n while (searchIndex < chunk.length) {\n // Find next line terminator\n const crIndex = chunk.indexOf('\\r', searchIndex)\n const lfIndex = chunk.indexOf('\\n', searchIndex)\n\n // Determine line end\n let lineEnd = -1\n if (crIndex !== -1 && lfIndex !== -1) {\n // CRLF case\n lineEnd = Math.min(crIndex, lfIndex)\n } else if (crIndex !== -1) {\n // CR at the end of a chunk might be part of a CRLF sequence that spans chunks,\n // so we shouldn't treat it as a line terminator (yet)\n if (crIndex === chunk.length - 1) {\n lineEnd = -1\n } else {\n lineEnd = crIndex\n }\n } else if (lfIndex !== -1) {\n lineEnd = lfIndex\n }\n\n // Extract line if terminator found\n if (lineEnd === -1) {\n // No terminator found, rest is incomplete\n incompleteLine = chunk.slice(searchIndex)\n break\n } else {\n const line = chunk.slice(searchIndex, lineEnd)\n lines.push(line)\n\n // Move past line terminator\n searchIndex = lineEnd + 1\n if (chunk[searchIndex - 1] === '\\r' && chunk[searchIndex] === '\\n') {\n searchIndex++\n }\n }\n }\n\n return [lines, incompleteLine]\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"index.js","sources":["../src/errors.ts","../src/parse.ts"],"sourcesContent":["/**\n * The type of error that occurred.\n * @public\n */\nexport type ErrorType = 'invalid-retry' | 'unknown-field'\n\n/**\n * Error thrown when encountering an issue during parsing.\n *\n * @public\n */\nexport class ParseError extends Error {\n /**\n * The type of error that occurred.\n */\n type: ErrorType\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the field name.\n */\n field?: string | undefined\n\n /**\n * In the case of an unknown field encountered in the stream, this will be the value of the field.\n */\n value?: string | undefined\n\n /**\n * The line that caused the error, if available.\n */\n line?: string | undefined\n\n constructor(\n message: string,\n options: {type: ErrorType; field?: string; value?: string; line?: string},\n ) {\n super(message)\n this.name = 'ParseError'\n this.type = options.type\n this.field = options.field\n this.value = options.value\n this.line = options.line\n }\n}\n","/**\n * EventSource/Server-Sent Events parser\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html\n */\nimport {ParseError} from './errors.ts'\nimport type {EventSourceParser, ParserCallbacks} from './types.ts'\n\n// ASCII codes used in the hot parsing paths.\nconst LF = 10\nconst CR = 13\nconst SPACE = 32\n\n// oxlint-disable-next-line no-unused-vars\nfunction noop(_arg: unknown) {\n // intentional noop\n}\n\n/**\n * Creates a new EventSource parser.\n *\n * @param callbacks - Callbacks to invoke on different parsing events:\n * - `onEvent` when a new event is parsed\n * - `onError` when an error occurs\n * - `onRetry` when a new reconnection interval has been sent from the server\n * - `onComment` when a comment is encountered in the stream\n *\n * @returns A new EventSource parser, with `parse` and `reset` methods.\n * @public\n */\nexport function createParser(callbacks: ParserCallbacks): EventSourceParser {\n if (typeof callbacks === 'function') {\n throw new TypeError(\n '`callbacks` must be an object, got a function instead. Did you mean `{onEvent: fn}`?',\n )\n }\n\n const {onEvent = noop, onError = noop, onRetry = noop, onComment} = callbacks\n\n let incompleteLine = ''\n\n let isFirstChunk = true\n let id: string | undefined\n let data = ''\n let dataLines = 0\n let eventType: string | undefined\n\n /**\n * Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do\n * not yet form a complete line are held back and prepended to the next chunk,\n * so callers can pass arbitrary slices of the stream without worrying about\n * line boundaries.\n *\n * The first chunk is dispatched to {@link feedFirst} so the leading UTF-8\n * BOM can be stripped before parsing begins.\n */\n function feed(chunk: string) {\n if (isFirstChunk) {\n isFirstChunk = false\n feedFirst(chunk)\n return\n }\n const input = incompleteLine === '' ? chunk : incompleteLine + chunk\n incompleteLine = processLines(input)\n }\n\n /**\n * Handles the very first chunk of the stream. Per the SSE spec, a UTF-8 BOM\n * (0xEF 0xBB 0xBF) at the start of the stream must be stripped before\n * parsing. After BOM handling, behaves identically to {@link feed}.\n *\n * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream\n */\n function feedFirst(chunk: string) {\n if (\n chunk.charCodeAt(0) === 0xef &&\n chunk.charCodeAt(1) === 0xbb &&\n chunk.charCodeAt(2) === 0xbf\n ) {\n chunk = chunk.slice(3)\n }\n const input = incompleteLine === '' ? chunk : incompleteLine + chunk\n incompleteLine = processLines(input)\n }\n\n /**\n * Splits `chunk` into SSE lines and dispatches each to the appropriate handler.\n * Returns any trailing bytes that did not terminate with a line break, so the\n * caller can prepend them to the next chunk.\n *\n * The SSE spec permits three line terminators: `\\n`, `\\r`, and `\\r\\n`. Real-world\n * streams almost always use plain `\\n`, so we take a fast path when no `\\r` is\n * present in the chunk. The slow path is spec-correct but does more work per line.\n */\n function processLines(chunk: string): string {\n let searchIndex = 0\n\n // Fast path: LF-only chunk (the common case for typical SSE servers).\n // We can scan forward with a single `indexOf('\\n')` per line and inline\n // the hot-path branches for `data:` and `event:` without the CR bookkeeping\n // the slow path needs.\n if (chunk.indexOf('\\r') === -1) {\n let lfIndex = chunk.indexOf('\\n', searchIndex)\n while (lfIndex !== -1) {\n // Blank line: end-of-event marker. Dispatch the accumulated event (if any)\n // and reset the buffered fields. This is hoisted out of `parseLine` because\n // it's the single most common line shape after `data:` lines.\n if (searchIndex === lfIndex) {\n if (dataLines > 0) {\n onEvent({id, event: eventType, data})\n }\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n const firstCharCode = chunk.charCodeAt(searchIndex)\n if (isDataPrefix(chunk, searchIndex, firstCharCode)) {\n // `data:` line — append the value to the event's data buffer.\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart =\n chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5\n const value = chunk.slice(valueStart, lfIndex)\n // Fast path within a fast path: if this is the first data line AND the\n // next char is another LF (i.e. `data:foo\\n\\n`), dispatch immediately\n // without ever writing to the `data` buffer. This is the shape of a\n // typical single-line SSE event (ChatGPT-style streams, etc.) and is\n // hot enough to be worth the duplication.\n if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) {\n onEvent({id, event: eventType, data: value})\n id = undefined\n data = ''\n eventType = undefined\n searchIndex = lfIndex + 2\n lfIndex = chunk.indexOf('\\n', searchIndex)\n continue\n }\n // Multi-line data: concatenate with newline separator per spec.\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) {\n // `event:` line — set the event type for the next dispatch. Per spec,\n // an empty value resets `event type` to its default (undefined here).\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(\n chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6,\n lfIndex,\n ) || undefined\n } else {\n // Everything else: `id:`, `retry:`, comment lines (`:` prefix), unknown\n // fields, or malformed lines. These are rarer and go through the full\n // per-line parser, which handles the SSE field grammar in detail.\n parseLine(chunk, searchIndex, lfIndex)\n }\n searchIndex = lfIndex + 1\n lfIndex = chunk.indexOf('\\n', searchIndex)\n }\n return chunk.slice(searchIndex)\n }\n\n // Slow path: the chunk contains at least one `\\r`, so lines may be terminated\n // by `\\r`, `\\n`, or `\\r\\n`. We locate the next terminator by looking at both\n // the nearest `\\r` and `\\n` and picking whichever comes first.\n while (searchIndex < chunk.length) {\n const crIndex = chunk.indexOf('\\r', searchIndex)\n const lfIndex = chunk.indexOf('\\n', searchIndex)\n\n let lineEnd = -1\n if (crIndex !== -1 && lfIndex !== -1) {\n lineEnd = crIndex < lfIndex ? crIndex : lfIndex\n } else if (crIndex !== -1) {\n // A trailing `\\r` at the very end of the chunk is ambiguous: it could be\n // a bare-CR terminator, or the first half of a `\\r\\n` whose `\\n` arrives\n // in the next chunk. Defer until we see more input.\n if (crIndex === chunk.length - 1) {\n lineEnd = -1\n } else {\n lineEnd = crIndex\n }\n } else if (lfIndex !== -1) {\n lineEnd = lfIndex\n }\n\n if (lineEnd === -1) {\n break\n }\n\n parseLine(chunk, searchIndex, lineEnd)\n searchIndex = lineEnd + 1\n // If we just consumed a `\\r` and the next char is `\\n`, skip it so the\n // pair is treated as a single terminator rather than an empty line.\n if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) {\n searchIndex++\n }\n }\n\n return chunk.slice(searchIndex)\n }\n\n function parseLine(chunk: string, start: number, end: number) {\n if (start === end) {\n dispatchEvent()\n return\n }\n\n const firstCharCode = chunk.charCodeAt(start)\n\n if (isDataPrefix(chunk, start, firstCharCode)) {\n // 'data:'.length === 5, 'data: '.length === 6\n const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5\n const value = chunk.slice(valueStart, end)\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n return\n }\n\n if (isEventPrefix(chunk, start, firstCharCode)) {\n // 'event:'.length === 6, 'event: '.length === 7\n eventType =\n chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || undefined\n return\n }\n\n // Fast path for \"id:\" — 'i' = 105, 'd' = 100, ':' = 58\n if (\n firstCharCode === 105 &&\n chunk.charCodeAt(start + 1) === 100 &&\n chunk.charCodeAt(start + 2) === 58\n ) {\n // 'id:'.length === 3, 'id: '.length === 4\n const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end)\n id = value.includes('\\0') ? undefined : value\n return\n }\n\n // Comment line — ':' = 58\n if (firstCharCode === 58) {\n if (onComment) {\n const line = chunk.slice(start, end)\n // skip ':' (+1), or ': ' (+2) when a space follows\n onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1))\n }\n return\n }\n\n const line = chunk.slice(start, end)\n const fieldSeparatorIndex = line.indexOf(':')\n if (fieldSeparatorIndex === -1) {\n processField(line, '', line)\n return\n }\n\n const field = line.slice(0, fieldSeparatorIndex)\n // skip ':' (+1), or ': ' (+2) when a space follows\n const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1\n const value = line.slice(fieldSeparatorIndex + offset)\n processField(field, value, line)\n }\n\n function processField(field: string, value: string, line: string) {\n // Field names must be compared literally, with no case folding performed.\n switch (field) {\n case 'event':\n // Set the `event type` buffer to field value\n eventType = value || undefined\n break\n case 'data':\n data = dataLines === 0 ? value : `${data}\\n${value}`\n dataLines++\n break\n case 'id':\n // If the field value does not contain U+0000 NULL, then set the `ID` buffer to\n // the field value. Otherwise, ignore the field.\n id = value.includes('\\0') ? undefined : value\n break\n case 'retry':\n // If the field value consists of only ASCII digits, then interpret the field value as an\n // integer in base ten, and set the event stream's reconnection time to that integer.\n // Otherwise, ignore the field.\n if (/^\\d+$/.test(value)) {\n onRetry(parseInt(value, 10))\n } else {\n onError(\n new ParseError(`Invalid \\`retry\\` value: \"${value}\"`, {\n type: 'invalid-retry',\n value,\n line,\n }),\n )\n }\n break\n default:\n // Otherwise, the field is ignored.\n onError(\n new ParseError(\n `Unknown field \"${field.length > 20 ? `${field.slice(0, 20)}…` : field}\"`,\n {type: 'unknown-field', field, value, line},\n ),\n )\n break\n }\n }\n\n function dispatchEvent() {\n if (dataLines > 0) {\n onEvent({\n id,\n event: eventType,\n data,\n })\n }\n\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n }\n\n function reset(options: {consume?: boolean} = {}) {\n if (incompleteLine && options.consume) {\n parseLine(incompleteLine, 0, incompleteLine.length)\n }\n\n isFirstChunk = true\n id = undefined\n data = ''\n dataLines = 0\n eventType = undefined\n incompleteLine = ''\n }\n\n return {feed, reset}\n}\n\n/**\n * Checks if `chunk` starts with the literal `data:` at index `i`.\n *\n * Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this\n * hand-unrolled char-code comparison is ~20% faster on common event types.\n * The caller passes `firstCharCode` (the code at `i`) so it can be reused\n * across prefix checks.\n *\n * ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58\n */\nfunction isDataPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 100 &&\n chunk.charCodeAt(i + 1) === 97 &&\n chunk.charCodeAt(i + 2) === 116 &&\n chunk.charCodeAt(i + 3) === 97 &&\n chunk.charCodeAt(i + 4) === 58\n )\n}\n\n/**\n * Checks if `chunk` starts with the literal `event:` at index `i`.\n *\n * See {@link isDataPrefix} for why this is hand-unrolled rather than using\n * `String.prototype.startsWith`.\n *\n * ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58\n */\nfunction isEventPrefix(chunk: string, i: number, firstCharCode: number): boolean {\n return (\n firstCharCode === 101 &&\n chunk.charCodeAt(i + 1) === 118 &&\n chunk.charCodeAt(i + 2) === 101 &&\n chunk.charCodeAt(i + 3) === 110 &&\n chunk.charCodeAt(i + 4) === 116 &&\n chunk.charCodeAt(i + 5) === 58\n )\n}\n"],"names":["value","line"],"mappings":"AAWO,MAAM,mBAAmB,MAAM;AAAA,EAqBpC,YACE,SACA,SACA;AACA,UAAM,OAAO,GACb,KAAK,OAAO,cACZ,KAAK,OAAO,QAAQ,MACpB,KAAK,QAAQ,QAAQ,OACrB,KAAK,QAAQ,QAAQ,OACrB,KAAK,OAAO,QAAQ;AAAA,EACtB;AACF;ACnCA,MAAM,KAAK,IACL,KAAK,IACL,QAAQ;AAGd,SAAS,KAAK,MAAe;AAE7B;AAcO,SAAS,aAAa,WAA+C;AAC1E,MAAI,OAAO,aAAc;AACvB,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAIJ,QAAM,EAAC,UAAU,MAAM,UAAU,MAAM,UAAU,MAAM,cAAa;AAEpE,MAAI,iBAAiB,IAEjB,eAAe,IACf,IACA,OAAO,IACP,YAAY,GACZ;AAWJ,WAAS,KAAK,OAAe;AAC3B,QAAI,cAAc;AAChB,qBAAe,IACf,UAAU,KAAK;AACf;AAAA,IACF;AACA,UAAM,QAAQ,mBAAmB,KAAK,QAAQ,iBAAiB;AAC/D,qBAAiB,aAAa,KAAK;AAAA,EACrC;AASA,WAAS,UAAU,OAAe;AAE9B,UAAM,WAAW,CAAC,MAAM,OACxB,MAAM,WAAW,CAAC,MAAM,OACxB,MAAM,WAAW,CAAC,MAAM,QAExB,QAAQ,MAAM,MAAM,CAAC;AAEvB,UAAM,QAAQ,mBAAmB,KAAK,QAAQ,iBAAiB;AAC/D,qBAAiB,aAAa,KAAK;AAAA,EACrC;AAWA,WAAS,aAAa,OAAuB;AAC3C,QAAI,cAAc;AAMlB,QAAI,MAAM,QAAQ,IAAI,MAAM,IAAI;AAC9B,UAAI,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AAC7C,aAAO,YAAY,MAAI;AAIrB,YAAI,gBAAgB,SAAS;AACvB,sBAAY,KACd,QAAQ,EAAC,IAAI,OAAO,WAAW,KAAA,CAAK,GAEtC,KAAK,QACL,OAAO,IACP,YAAY,GACZ,YAAY,QACZ,cAAc,UAAU,GACxB,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AACzC;AAAA,QACF;AACA,cAAM,gBAAgB,MAAM,WAAW,WAAW;AAClD,YAAI,aAAa,OAAO,aAAa,aAAa,GAAG;AAGnD,gBAAM,aACJ,MAAM,WAAW,cAAc,CAAC,MAAM,QAAQ,cAAc,IAAI,cAAc,GAC1E,QAAQ,MAAM,MAAM,YAAY,OAAO;AAM7C,cAAI,cAAc,KAAK,MAAM,WAAW,UAAU,CAAC,MAAM,IAAI;AAC3D,oBAAQ,EAAC,IAAI,OAAO,WAAW,MAAM,MAAA,CAAM,GAC3C,KAAK,QACL,OAAO,IACP,YAAY,QACZ,cAAc,UAAU,GACxB,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AACzC;AAAA,UACF;AAEA,iBAAO,cAAc,IAAI,QAAQ,GAAG,IAAI;AAAA,EAAK,KAAK,IAClD;AAAA,QACF,MAAW,eAAc,OAAO,aAAa,aAAa,IAIxD,YACE,MAAM;AAAA,UACJ,MAAM,WAAW,cAAc,CAAC,MAAM,QAAQ,cAAc,IAAI,cAAc;AAAA,UAC9E;AAAA,QAAA,KACG,SAKP,UAAU,OAAO,aAAa,OAAO;AAEvC,sBAAc,UAAU,GACxB,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AAAA,MAC3C;AACA,aAAO,MAAM,MAAM,WAAW;AAAA,IAChC;AAKA,WAAO,cAAc,MAAM,UAAQ;AACjC,YAAM,UAAU,MAAM,QAAQ,MAAM,WAAW,GACzC,UAAU,MAAM,QAAQ;AAAA,GAAM,WAAW;AAE/C,UAAI,UAAU;AAgBd,UAfI,YAAY,MAAM,YAAY,KAChC,UAAU,UAAU,UAAU,UAAU,UAC/B,YAAY,KAIjB,YAAY,MAAM,SAAS,IAC7B,UAAU,KAEV,UAAU,UAEH,YAAY,OACrB,UAAU,UAGR,YAAY;AACd;AAGF,gBAAU,OAAO,aAAa,OAAO,GACrC,cAAc,UAAU,GAGpB,MAAM,WAAW,cAAc,CAAC,MAAM,MAAM,MAAM,WAAW,WAAW,MAAM,MAChF;AAAA,IAEJ;AAEA,WAAO,MAAM,MAAM,WAAW;AAAA,EAChC;AAEA,WAAS,UAAU,OAAe,OAAe,KAAa;AAC5D,QAAI,UAAU,KAAK;AACjB,oBAAA;AACA;AAAA,IACF;AAEA,UAAM,gBAAgB,MAAM,WAAW,KAAK;AAE5C,QAAI,aAAa,OAAO,OAAO,aAAa,GAAG;AAE7C,YAAM,aAAa,MAAM,WAAW,QAAQ,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,GACzEA,SAAQ,MAAM,MAAM,YAAY,GAAG;AACzC,aAAO,cAAc,IAAIA,SAAQ,GAAG,IAAI;AAAA,EAAKA,MAAK,IAClD;AACA;AAAA,IACF;AAEA,QAAI,cAAc,OAAO,OAAO,aAAa,GAAG;AAE9C,kBACE,MAAM,MAAM,MAAM,WAAW,QAAQ,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,GAAG,GAAG,KAAK;AACrF;AAAA,IACF;AAGA,QACE,kBAAkB,OAClB,MAAM,WAAW,QAAQ,CAAC,MAAM,OAChC,MAAM,WAAW,QAAQ,CAAC,MAAM,IAChC;AAEA,YAAMA,SAAQ,MAAM,MAAM,MAAM,WAAW,QAAQ,CAAC,MAAM,QAAQ,QAAQ,IAAI,QAAQ,GAAG,GAAG;AAC5F,WAAKA,OAAM,SAAS,IAAI,IAAI,SAAYA;AACxC;AAAA,IACF;AAGA,QAAI,kBAAkB,IAAI;AACxB,UAAI,WAAW;AACb,cAAMC,QAAO,MAAM,MAAM,OAAO,GAAG;AAEnC,kBAAUA,MAAK,MAAM,MAAM,WAAW,QAAQ,CAAC,MAAM,QAAQ,IAAI,CAAC,CAAC;AAAA,MACrE;AACA;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,MAAM,OAAO,GAAG,GAC7B,sBAAsB,KAAK,QAAQ,GAAG;AAC5C,QAAI,wBAAwB,IAAI;AAC9B,mBAAa,MAAM,IAAI,IAAI;AAC3B;AAAA,IACF;AAEA,UAAM,QAAQ,KAAK,MAAM,GAAG,mBAAmB,GAEzC,SAAS,KAAK,WAAW,sBAAsB,CAAC,MAAM,QAAQ,IAAI,GAClE,QAAQ,KAAK,MAAM,sBAAsB,MAAM;AACrD,iBAAa,OAAO,OAAO,IAAI;AAAA,EACjC;AAEA,WAAS,aAAa,OAAe,OAAe,MAAc;AAEhE,YAAQ,OAAA;AAAA,MACN,KAAK;AAEH,oBAAY,SAAS;AACrB;AAAA,MACF,KAAK;AACH,eAAO,cAAc,IAAI,QAAQ,GAAG,IAAI;AAAA,EAAK,KAAK,IAClD;AACA;AAAA,MACF,KAAK;AAGH,aAAK,MAAM,SAAS,IAAI,IAAI,SAAY;AACxC;AAAA,MACF,KAAK;AAIC,gBAAQ,KAAK,KAAK,IACpB,QAAQ,SAAS,OAAO,EAAE,CAAC,IAE3B;AAAA,UACE,IAAI,WAAW,6BAA6B,KAAK,KAAK;AAAA,YACpD,MAAM;AAAA,YACN;AAAA,YACA;AAAA,UAAA,CACD;AAAA,QAAA;AAGL;AAAA,MACF;AAEE;AAAA,UACE,IAAI;AAAA,YACF,kBAAkB,MAAM,SAAS,KAAK,GAAG,MAAM,MAAM,GAAG,EAAE,CAAC,WAAM,KAAK;AAAA,YACtE,EAAC,MAAM,iBAAiB,OAAO,OAAO,KAAA;AAAA,UAAI;AAAA,QAC5C;AAEF;AAAA,IAAA;AAAA,EAEN;AAEA,WAAS,gBAAgB;AACnB,gBAAY,KACd,QAAQ;AAAA,MACN;AAAA,MACA,OAAO;AAAA,MACP;AAAA,IAAA,CACD,GAGH,KAAK,QACL,OAAO,IACP,YAAY,GACZ,YAAY;AAAA,EACd;AAEA,WAAS,MAAM,UAA+B,IAAI;AAC5C,sBAAkB,QAAQ,WAC5B,UAAU,gBAAgB,GAAG,eAAe,MAAM,GAGpD,eAAe,IACf,KAAK,QACL,OAAO,IACP,YAAY,GACZ,YAAY,QACZ,iBAAiB;AAAA,EACnB;AAEA,SAAO,EAAC,MAAM,MAAA;AAChB;AAYA,SAAS,aAAa,OAAe,GAAW,eAAgC;AAC9E,SACE,kBAAkB,OAClB,MAAM,WAAW,IAAI,CAAC,MAAM,MAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,MAC5B,MAAM,WAAW,IAAI,CAAC,MAAM;AAEhC;AAUA,SAAS,cAAc,OAAe,GAAW,eAAgC;AAC/E,SACE,kBAAkB,OAClB,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM,OAC5B,MAAM,WAAW,IAAI,CAAC,MAAM;AAEhC;"} |
+22
-19
@@ -5,3 +5,3 @@ /** | ||
| */ | ||
| export declare type ErrorType = 'invalid-retry' | 'unknown-field' | ||
| export declare type ErrorType = "invalid-retry" | "unknown-field"; | ||
@@ -19,3 +19,3 @@ /** | ||
| */ | ||
| event?: string | undefined | ||
| event?: string | undefined; | ||
| /** | ||
@@ -25,7 +25,7 @@ * ID of the message, if any was provided by the server. Can be used by clients to keep the | ||
| */ | ||
| id?: string | undefined | ||
| id?: string | undefined; | ||
| /** | ||
| * The data received for this message | ||
| */ | ||
| data: string | ||
| data: string; | ||
| } | ||
@@ -54,4 +54,7 @@ | ||
| */ | ||
| export declare class EventSourceParserStream extends TransformStream<string, EventSourceMessage> { | ||
| constructor({onError, onRetry, onComment}?: StreamOptions) | ||
| export declare class EventSourceParserStream extends TransformStream< | ||
| string, | ||
| EventSourceMessage | ||
| > { | ||
| constructor({ onError, onRetry, onComment }?: StreamOptions); | ||
| } | ||
@@ -68,24 +71,24 @@ | ||
| */ | ||
| type: ErrorType | ||
| type: ErrorType; | ||
| /** | ||
| * In the case of an unknown field encountered in the stream, this will be the field name. | ||
| */ | ||
| field?: string | undefined | ||
| field?: string | undefined; | ||
| /** | ||
| * In the case of an unknown field encountered in the stream, this will be the value of the field. | ||
| */ | ||
| value?: string | undefined | ||
| value?: string | undefined; | ||
| /** | ||
| * The line that caused the error, if available. | ||
| */ | ||
| line?: string | undefined | ||
| line?: string | undefined; | ||
| constructor( | ||
| message: string, | ||
| options: { | ||
| type: ErrorType | ||
| field?: string | ||
| value?: string | ||
| line?: string | ||
| type: ErrorType; | ||
| field?: string; | ||
| value?: string; | ||
| line?: string; | ||
| }, | ||
| ) | ||
| ); | ||
| } | ||
@@ -108,3 +111,3 @@ | ||
| */ | ||
| onError?: ('terminate' | ((error: Error) => void)) | undefined | ||
| onError?: ("terminate" | ((error: Error) => void)) | undefined; | ||
| /** | ||
@@ -115,3 +118,3 @@ * Callback for when a reconnection interval is sent from the server. | ||
| */ | ||
| onRetry?: ((retry: number) => void) | undefined | ||
| onRetry?: ((retry: number) => void) | undefined; | ||
| /** | ||
@@ -122,5 +125,5 @@ * Callback for when a comment is encountered in the stream. | ||
| */ | ||
| onComment?: ((comment: string) => void) | undefined | ||
| onComment?: ((comment: string) => void) | undefined; | ||
| } | ||
| export {} | ||
| export {}; |
+22
-19
@@ -5,3 +5,3 @@ /** | ||
| */ | ||
| export declare type ErrorType = 'invalid-retry' | 'unknown-field' | ||
| export declare type ErrorType = "invalid-retry" | "unknown-field"; | ||
@@ -19,3 +19,3 @@ /** | ||
| */ | ||
| event?: string | undefined | ||
| event?: string | undefined; | ||
| /** | ||
@@ -25,7 +25,7 @@ * ID of the message, if any was provided by the server. Can be used by clients to keep the | ||
| */ | ||
| id?: string | undefined | ||
| id?: string | undefined; | ||
| /** | ||
| * The data received for this message | ||
| */ | ||
| data: string | ||
| data: string; | ||
| } | ||
@@ -54,4 +54,7 @@ | ||
| */ | ||
| export declare class EventSourceParserStream extends TransformStream<string, EventSourceMessage> { | ||
| constructor({onError, onRetry, onComment}?: StreamOptions) | ||
| export declare class EventSourceParserStream extends TransformStream< | ||
| string, | ||
| EventSourceMessage | ||
| > { | ||
| constructor({ onError, onRetry, onComment }?: StreamOptions); | ||
| } | ||
@@ -68,24 +71,24 @@ | ||
| */ | ||
| type: ErrorType | ||
| type: ErrorType; | ||
| /** | ||
| * In the case of an unknown field encountered in the stream, this will be the field name. | ||
| */ | ||
| field?: string | undefined | ||
| field?: string | undefined; | ||
| /** | ||
| * In the case of an unknown field encountered in the stream, this will be the value of the field. | ||
| */ | ||
| value?: string | undefined | ||
| value?: string | undefined; | ||
| /** | ||
| * The line that caused the error, if available. | ||
| */ | ||
| line?: string | undefined | ||
| line?: string | undefined; | ||
| constructor( | ||
| message: string, | ||
| options: { | ||
| type: ErrorType | ||
| field?: string | ||
| value?: string | ||
| line?: string | ||
| type: ErrorType; | ||
| field?: string; | ||
| value?: string; | ||
| line?: string; | ||
| }, | ||
| ) | ||
| ); | ||
| } | ||
@@ -108,3 +111,3 @@ | ||
| */ | ||
| onError?: ('terminate' | ((error: Error) => void)) | undefined | ||
| onError?: ("terminate" | ((error: Error) => void)) | undefined; | ||
| /** | ||
@@ -115,3 +118,3 @@ * Callback for when a reconnection interval is sent from the server. | ||
| */ | ||
| onRetry?: ((retry: number) => void) | undefined | ||
| onRetry?: ((retry: number) => void) | undefined; | ||
| /** | ||
@@ -122,5 +125,5 @@ * Callback for when a comment is encountered in the stream. | ||
| */ | ||
| onComment?: ((comment: string) => void) | undefined | ||
| onComment?: ((comment: string) => void) | undefined; | ||
| } | ||
| export {} | ||
| export {}; |
+1
-1
| MIT License | ||
| Copyright (c) 2025 Espen Hovlandsdal <espen@hovlandsdal.com> | ||
| Copyright (c) 2026 Espen Hovlandsdal <espen@hovlandsdal.com> | ||
@@ -5,0 +5,0 @@ Permission is hereby granted, free of charge, to any person obtaining a copy |
+54
-77
| { | ||
| "name": "eventsource-parser", | ||
| "version": "3.0.6", | ||
| "version": "3.0.7", | ||
| "description": "Streaming, source-agnostic EventSource/Server-Sent Events parser", | ||
| "keywords": [ | ||
| "eventsource", | ||
| "server-sent-events", | ||
| "sse" | ||
| ], | ||
| "homepage": "https://github.com/rexxars/eventsource-parser#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/rexxars/eventsource-parser/issues" | ||
| }, | ||
| "license": "MIT", | ||
| "author": "Espen Hovlandsdal <espen@hovlandsdal.com>", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+ssh://git@github.com/rexxars/eventsource-parser.git" | ||
| }, | ||
| "files": [ | ||
| "dist", | ||
| "!dist/stats.html", | ||
| "!dist/index.min.js", | ||
| "src", | ||
| "stream.js" | ||
| ], | ||
| "type": "module", | ||
| "sideEffects": false, | ||
| "type": "module", | ||
| "main": "./dist/index.cjs", | ||
| "module": "./dist/index.js", | ||
| "types": "./dist/index.d.ts", | ||
| "module": "./dist/index.js", | ||
| "main": "./dist/index.cjs", | ||
| "exports": { | ||
@@ -25,29 +47,12 @@ ".": { | ||
| }, | ||
| "typesVersions": { | ||
| "*": { | ||
| "stream": [ | ||
| "./dist/stream.d.ts" | ||
| ] | ||
| } | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| }, | ||
| "browserslist": [ | ||
| "node >= 20", | ||
| "chrome >= 71", | ||
| "safari >= 14.1", | ||
| "firefox >= 105", | ||
| "edge >= 79" | ||
| ], | ||
| "files": [ | ||
| "dist", | ||
| "!dist/stats.html", | ||
| "src", | ||
| "stream.js" | ||
| ], | ||
| "scripts": { | ||
| "build": "pkg-utils build && pkg-utils --strict", | ||
| "clean": "rimraf dist coverage", | ||
| "lint": "eslint . && tsc --noEmit", | ||
| "check": "npm run clean && npm run format && npm run lint && npm run build && vitest run", | ||
| "format": "oxfmt", | ||
| "format:check": "oxfmt --check", | ||
| "bench": "node --expose-gc --experimental-strip-types --no-warnings=ExperimentalWarning bench/parse.bench.ts", | ||
| "bundle-size": "node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/bundle-size.ts", | ||
| "knip": "knip", | ||
| "lint": "oxlint && tsc --noEmit", | ||
| "posttest": "npm run lint", | ||
@@ -61,57 +66,29 @@ "prebuild": "npm run clean", | ||
| }, | ||
| "author": "Espen Hovlandsdal <espen@hovlandsdal.com>", | ||
| "keywords": [ | ||
| "sse", | ||
| "eventsource", | ||
| "server-sent-events" | ||
| ], | ||
| "devDependencies": { | ||
| "@sanity/pkg-utils": "^8.0.0", | ||
| "@sanity/semantic-release-preset": "^5.0.0", | ||
| "@sanity/pkg-utils": "^10.4.15", | ||
| "@sanity/semantic-release-preset": "^6.0.0", | ||
| "@sanity/tsconfig": "^2.1.0", | ||
| "@types/node": "^20.19.0", | ||
| "@typescript-eslint/eslint-plugin": "^7.0.0", | ||
| "@typescript-eslint/parser": "^7.0.0", | ||
| "eslint": "^8.51.0", | ||
| "eslint-config-prettier": "^9.1.0", | ||
| "eslint-config-sanity": "^7.1.2", | ||
| "eventsource-encoder": "^1.0.1", | ||
| "prettier": "^3.5.3", | ||
| "rimraf": "^6.0.1", | ||
| "knip": "^6.4.1", | ||
| "mitata": "^1.0.34", | ||
| "oxfmt": "^0.45.0", | ||
| "oxlint": "^1.60.0", | ||
| "rimraf": "^6.1.3", | ||
| "rollup-plugin-visualizer": "^6.0.3", | ||
| "semantic-release": "^24.2.3", | ||
| "typescript": "^5.8.3", | ||
| "vitest": "^3.1.3" | ||
| "semantic-release": "^25.0.3", | ||
| "terser": "^5.46.1", | ||
| "typescript": "^5.9.3", | ||
| "vitest": "^4.1.4" | ||
| }, | ||
| "homepage": "https://github.com/rexxars/eventsource-parser#readme", | ||
| "bugs": { | ||
| "url": "https://github.com/rexxars/eventsource-parser/issues" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+ssh://git@github.com/rexxars/eventsource-parser.git" | ||
| }, | ||
| "license": "MIT", | ||
| "prettier": { | ||
| "bracketSpacing": false, | ||
| "printWidth": 100, | ||
| "semi": false, | ||
| "singleQuote": true | ||
| }, | ||
| "eslintConfig": { | ||
| "parserOptions": { | ||
| "ecmaFeatures": { | ||
| "modules": true | ||
| }, | ||
| "ecmaVersion": 9, | ||
| "sourceType": "module" | ||
| }, | ||
| "extends": [ | ||
| "sanity", | ||
| "sanity/typescript", | ||
| "prettier" | ||
| ], | ||
| "ignorePatterns": [ | ||
| "lib/**/" | ||
| ] | ||
| "browserslist": [ | ||
| "node >= 18", | ||
| "chrome >= 71", | ||
| "safari >= 14.1", | ||
| "firefox >= 105", | ||
| "edge >= 79" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18.0.0" | ||
| } | ||
| } |
+247
-104
@@ -8,3 +8,8 @@ /** | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
| // ASCII codes used in the hot parsing paths. | ||
| const LF = 10 | ||
| const CR = 13 | ||
| const SPACE = 32 | ||
| // oxlint-disable-next-line no-unused-vars | ||
| function noop(_arg: unknown) { | ||
@@ -40,23 +45,163 @@ // intentional noop | ||
| let data = '' | ||
| let eventType = '' | ||
| let dataLines = 0 | ||
| let eventType: string | undefined | ||
| function feed(newChunk: string) { | ||
| // Strip any UTF8 byte order mark (BOM) at the start of the stream | ||
| const chunk = isFirstChunk ? newChunk.replace(/^\xEF\xBB\xBF/, '') : newChunk | ||
| /** | ||
| * Feeds a chunk of the SSE stream to the parser. Any trailing bytes that do | ||
| * not yet form a complete line are held back and prepended to the next chunk, | ||
| * so callers can pass arbitrary slices of the stream without worrying about | ||
| * line boundaries. | ||
| * | ||
| * The first chunk is dispatched to {@link feedFirst} so the leading UTF-8 | ||
| * BOM can be stripped before parsing begins. | ||
| */ | ||
| function feed(chunk: string) { | ||
| if (isFirstChunk) { | ||
| isFirstChunk = false | ||
| feedFirst(chunk) | ||
| return | ||
| } | ||
| const input = incompleteLine === '' ? chunk : incompleteLine + chunk | ||
| incompleteLine = processLines(input) | ||
| } | ||
| // If there was a previous incomplete line, append it to the new chunk, | ||
| // so we may process it together as a new (hopefully complete) chunk. | ||
| const [complete, incomplete] = splitLines(`${incompleteLine}${chunk}`) | ||
| /** | ||
| * Handles the very first chunk of the stream. Per the SSE spec, a UTF-8 BOM | ||
| * (0xEF 0xBB 0xBF) at the start of the stream must be stripped before | ||
| * parsing. After BOM handling, behaves identically to {@link feed}. | ||
| * | ||
| * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream | ||
| */ | ||
| function feedFirst(chunk: string) { | ||
| if ( | ||
| chunk.charCodeAt(0) === 0xef && | ||
| chunk.charCodeAt(1) === 0xbb && | ||
| chunk.charCodeAt(2) === 0xbf | ||
| ) { | ||
| chunk = chunk.slice(3) | ||
| } | ||
| const input = incompleteLine === '' ? chunk : incompleteLine + chunk | ||
| incompleteLine = processLines(input) | ||
| } | ||
| for (const line of complete) { | ||
| parseLine(line) | ||
| /** | ||
| * Splits `chunk` into SSE lines and dispatches each to the appropriate handler. | ||
| * Returns any trailing bytes that did not terminate with a line break, so the | ||
| * caller can prepend them to the next chunk. | ||
| * | ||
| * The SSE spec permits three line terminators: `\n`, `\r`, and `\r\n`. Real-world | ||
| * streams almost always use plain `\n`, so we take a fast path when no `\r` is | ||
| * present in the chunk. The slow path is spec-correct but does more work per line. | ||
| */ | ||
| function processLines(chunk: string): string { | ||
| let searchIndex = 0 | ||
| // Fast path: LF-only chunk (the common case for typical SSE servers). | ||
| // We can scan forward with a single `indexOf('\n')` per line and inline | ||
| // the hot-path branches for `data:` and `event:` without the CR bookkeeping | ||
| // the slow path needs. | ||
| if (chunk.indexOf('\r') === -1) { | ||
| let lfIndex = chunk.indexOf('\n', searchIndex) | ||
| while (lfIndex !== -1) { | ||
| // Blank line: end-of-event marker. Dispatch the accumulated event (if any) | ||
| // and reset the buffered fields. This is hoisted out of `parseLine` because | ||
| // it's the single most common line shape after `data:` lines. | ||
| if (searchIndex === lfIndex) { | ||
| if (dataLines > 0) { | ||
| onEvent({id, event: eventType, data}) | ||
| } | ||
| id = undefined | ||
| data = '' | ||
| dataLines = 0 | ||
| eventType = undefined | ||
| searchIndex = lfIndex + 1 | ||
| lfIndex = chunk.indexOf('\n', searchIndex) | ||
| continue | ||
| } | ||
| const firstCharCode = chunk.charCodeAt(searchIndex) | ||
| if (isDataPrefix(chunk, searchIndex, firstCharCode)) { | ||
| // `data:` line — append the value to the event's data buffer. | ||
| // 'data:'.length === 5, 'data: '.length === 6 | ||
| const valueStart = | ||
| chunk.charCodeAt(searchIndex + 5) === SPACE ? searchIndex + 6 : searchIndex + 5 | ||
| const value = chunk.slice(valueStart, lfIndex) | ||
| // Fast path within a fast path: if this is the first data line AND the | ||
| // next char is another LF (i.e. `data:foo\n\n`), dispatch immediately | ||
| // without ever writing to the `data` buffer. This is the shape of a | ||
| // typical single-line SSE event (ChatGPT-style streams, etc.) and is | ||
| // hot enough to be worth the duplication. | ||
| if (dataLines === 0 && chunk.charCodeAt(lfIndex + 1) === LF) { | ||
| onEvent({id, event: eventType, data: value}) | ||
| id = undefined | ||
| data = '' | ||
| eventType = undefined | ||
| searchIndex = lfIndex + 2 | ||
| lfIndex = chunk.indexOf('\n', searchIndex) | ||
| continue | ||
| } | ||
| // Multi-line data: concatenate with newline separator per spec. | ||
| data = dataLines === 0 ? value : `${data}\n${value}` | ||
| dataLines++ | ||
| } else if (isEventPrefix(chunk, searchIndex, firstCharCode)) { | ||
| // `event:` line — set the event type for the next dispatch. Per spec, | ||
| // an empty value resets `event type` to its default (undefined here). | ||
| // 'event:'.length === 6, 'event: '.length === 7 | ||
| eventType = | ||
| chunk.slice( | ||
| chunk.charCodeAt(searchIndex + 6) === SPACE ? searchIndex + 7 : searchIndex + 6, | ||
| lfIndex, | ||
| ) || undefined | ||
| } else { | ||
| // Everything else: `id:`, `retry:`, comment lines (`:` prefix), unknown | ||
| // fields, or malformed lines. These are rarer and go through the full | ||
| // per-line parser, which handles the SSE field grammar in detail. | ||
| parseLine(chunk, searchIndex, lfIndex) | ||
| } | ||
| searchIndex = lfIndex + 1 | ||
| lfIndex = chunk.indexOf('\n', searchIndex) | ||
| } | ||
| return chunk.slice(searchIndex) | ||
| } | ||
| incompleteLine = incomplete | ||
| isFirstChunk = false | ||
| // Slow path: the chunk contains at least one `\r`, so lines may be terminated | ||
| // by `\r`, `\n`, or `\r\n`. We locate the next terminator by looking at both | ||
| // the nearest `\r` and `\n` and picking whichever comes first. | ||
| while (searchIndex < chunk.length) { | ||
| const crIndex = chunk.indexOf('\r', searchIndex) | ||
| const lfIndex = chunk.indexOf('\n', searchIndex) | ||
| let lineEnd = -1 | ||
| if (crIndex !== -1 && lfIndex !== -1) { | ||
| lineEnd = crIndex < lfIndex ? crIndex : lfIndex | ||
| } else if (crIndex !== -1) { | ||
| // A trailing `\r` at the very end of the chunk is ambiguous: it could be | ||
| // a bare-CR terminator, or the first half of a `\r\n` whose `\n` arrives | ||
| // in the next chunk. Defer until we see more input. | ||
| if (crIndex === chunk.length - 1) { | ||
| lineEnd = -1 | ||
| } else { | ||
| lineEnd = crIndex | ||
| } | ||
| } else if (lfIndex !== -1) { | ||
| lineEnd = lfIndex | ||
| } | ||
| if (lineEnd === -1) { | ||
| break | ||
| } | ||
| parseLine(chunk, searchIndex, lineEnd) | ||
| searchIndex = lineEnd + 1 | ||
| // If we just consumed a `\r` and the next char is `\n`, skip it so the | ||
| // pair is treated as a single terminator rather than an empty line. | ||
| if (chunk.charCodeAt(searchIndex - 1) === CR && chunk.charCodeAt(searchIndex) === LF) { | ||
| searchIndex++ | ||
| } | ||
| } | ||
| return chunk.slice(searchIndex) | ||
| } | ||
| function parseLine(line: string) { | ||
| // If the line is empty (a blank line), dispatch the event | ||
| if (line === '') { | ||
| function parseLine(chunk: string, start: number, end: number) { | ||
| if (start === end) { | ||
| dispatchEvent() | ||
@@ -66,6 +211,38 @@ return | ||
| // If the line starts with a U+003A COLON character (:), ignore the line. | ||
| if (line.startsWith(':')) { | ||
| const firstCharCode = chunk.charCodeAt(start) | ||
| if (isDataPrefix(chunk, start, firstCharCode)) { | ||
| // 'data:'.length === 5, 'data: '.length === 6 | ||
| const valueStart = chunk.charCodeAt(start + 5) === SPACE ? start + 6 : start + 5 | ||
| const value = chunk.slice(valueStart, end) | ||
| data = dataLines === 0 ? value : `${data}\n${value}` | ||
| dataLines++ | ||
| return | ||
| } | ||
| if (isEventPrefix(chunk, start, firstCharCode)) { | ||
| // 'event:'.length === 6, 'event: '.length === 7 | ||
| eventType = | ||
| chunk.slice(chunk.charCodeAt(start + 6) === SPACE ? start + 7 : start + 6, end) || undefined | ||
| return | ||
| } | ||
| // Fast path for "id:" — 'i' = 105, 'd' = 100, ':' = 58 | ||
| if ( | ||
| firstCharCode === 105 && | ||
| chunk.charCodeAt(start + 1) === 100 && | ||
| chunk.charCodeAt(start + 2) === 58 | ||
| ) { | ||
| // 'id:'.length === 3, 'id: '.length === 4 | ||
| const value = chunk.slice(chunk.charCodeAt(start + 3) === SPACE ? start + 4 : start + 3, end) | ||
| id = value.includes('\0') ? undefined : value | ||
| return | ||
| } | ||
| // Comment line — ':' = 58 | ||
| if (firstCharCode === 58) { | ||
| if (onComment) { | ||
| onComment(line.slice(line.startsWith(': ') ? 2 : 1)) | ||
| const line = chunk.slice(start, end) | ||
| // skip ':' (+1), or ': ' (+2) when a space follows | ||
| onComment(line.slice(chunk.charCodeAt(start + 1) === SPACE ? 2 : 1)) | ||
| } | ||
@@ -75,24 +252,14 @@ return | ||
| // If the line contains a U+003A COLON character (:) | ||
| const line = chunk.slice(start, end) | ||
| const fieldSeparatorIndex = line.indexOf(':') | ||
| if (fieldSeparatorIndex !== -1) { | ||
| // Collect the characters on the line before the first U+003A COLON character (:), | ||
| // and let `field` be that string. | ||
| const field = line.slice(0, fieldSeparatorIndex) | ||
| // Collect the characters on the line after the first U+003A COLON character (:), | ||
| // and let `value` be that string. If value starts with a U+0020 SPACE character, | ||
| // remove it from value. | ||
| const offset = line[fieldSeparatorIndex + 1] === ' ' ? 2 : 1 | ||
| const value = line.slice(fieldSeparatorIndex + offset) | ||
| processField(field, value, line) | ||
| if (fieldSeparatorIndex === -1) { | ||
| processField(line, '', line) | ||
| return | ||
| } | ||
| // Otherwise, the string is not empty but does not contain a U+003A COLON character (:) | ||
| // Process the field using the whole line as the field name, and an empty string as the field value. | ||
| // 👆 This is according to spec. That means that a line that has the value `data` will result in | ||
| // a newline being added to the current `data` buffer, for instance. | ||
| processField(line, '', line) | ||
| const field = line.slice(0, fieldSeparatorIndex) | ||
| // skip ':' (+1), or ': ' (+2) when a space follows | ||
| const offset = line.charCodeAt(fieldSeparatorIndex + 1) === SPACE ? 2 : 1 | ||
| const value = line.slice(fieldSeparatorIndex + offset) | ||
| processField(field, value, line) | ||
| } | ||
@@ -105,8 +272,7 @@ | ||
| // Set the `event type` buffer to field value | ||
| eventType = value | ||
| eventType = value || undefined | ||
| break | ||
| case 'data': | ||
| // Append the field value to the `data` buffer, then append a single U+000A LINE FEED(LF) | ||
| // character to the `data` buffer. | ||
| data = `${data}${value}\n` | ||
| data = dataLines === 0 ? value : `${data}\n${value}` | ||
| dataLines++ | ||
| break | ||
@@ -147,17 +313,14 @@ case 'id': | ||
| function dispatchEvent() { | ||
| const shouldDispatch = data.length > 0 | ||
| if (shouldDispatch) { | ||
| if (dataLines > 0) { | ||
| onEvent({ | ||
| id, | ||
| event: eventType || undefined, | ||
| // If the data buffer's last character is a U+000A LINE FEED (LF) character, | ||
| // then remove the last character from the data buffer. | ||
| data: data.endsWith('\n') ? data.slice(0, -1) : data, | ||
| event: eventType, | ||
| data, | ||
| }) | ||
| } | ||
| // Reset for the next event | ||
| id = undefined | ||
| data = '' | ||
| eventType = '' | ||
| dataLines = 0 | ||
| eventType = undefined | ||
| } | ||
@@ -167,3 +330,3 @@ | ||
| if (incompleteLine && options.consume) { | ||
| parseLine(incompleteLine) | ||
| parseLine(incompleteLine, 0, incompleteLine.length) | ||
| } | ||
@@ -174,3 +337,4 @@ | ||
| data = '' | ||
| eventType = '' | ||
| dataLines = 0 | ||
| eventType = undefined | ||
| incompleteLine = '' | ||
@@ -183,59 +347,38 @@ } | ||
| /** | ||
| * For the given `chunk`, split it into lines according to spec, and return any remaining incomplete line. | ||
| * Checks if `chunk` starts with the literal `data:` at index `i`. | ||
| * | ||
| * @param chunk - The chunk to split into lines | ||
| * @returns A tuple containing an array of complete lines, and any remaining incomplete line | ||
| * @internal | ||
| * Equivalent to `chunk.startsWith('data:', i)`, but benchmarks show this | ||
| * hand-unrolled char-code comparison is ~20% faster on common event types. | ||
| * The caller passes `firstCharCode` (the code at `i`) so it can be reused | ||
| * across prefix checks. | ||
| * | ||
| * ASCII: 'd' = 100, 'a' = 97, 't' = 116, 'a' = 97, ':' = 58 | ||
| */ | ||
| function splitLines(chunk: string): [complete: Array<string>, incomplete: string] { | ||
| /** | ||
| * According to the spec, a line is terminated by either: | ||
| * - U+000D CARRIAGE RETURN U+000A LINE FEED (CRLF) character pair | ||
| * - a single U+000A LINE FEED(LF) character not preceded by a U+000D CARRIAGE RETURN(CR) character | ||
| * - a single U+000D CARRIAGE RETURN(CR) character not followed by a U+000A LINE FEED(LF) character | ||
| */ | ||
| const lines: Array<string> = [] | ||
| let incompleteLine = '' | ||
| let searchIndex = 0 | ||
| function isDataPrefix(chunk: string, i: number, firstCharCode: number): boolean { | ||
| return ( | ||
| firstCharCode === 100 && | ||
| chunk.charCodeAt(i + 1) === 97 && | ||
| chunk.charCodeAt(i + 2) === 116 && | ||
| chunk.charCodeAt(i + 3) === 97 && | ||
| chunk.charCodeAt(i + 4) === 58 | ||
| ) | ||
| } | ||
| while (searchIndex < chunk.length) { | ||
| // Find next line terminator | ||
| const crIndex = chunk.indexOf('\r', searchIndex) | ||
| const lfIndex = chunk.indexOf('\n', searchIndex) | ||
| // Determine line end | ||
| let lineEnd = -1 | ||
| if (crIndex !== -1 && lfIndex !== -1) { | ||
| // CRLF case | ||
| lineEnd = Math.min(crIndex, lfIndex) | ||
| } else if (crIndex !== -1) { | ||
| // CR at the end of a chunk might be part of a CRLF sequence that spans chunks, | ||
| // so we shouldn't treat it as a line terminator (yet) | ||
| if (crIndex === chunk.length - 1) { | ||
| lineEnd = -1 | ||
| } else { | ||
| lineEnd = crIndex | ||
| } | ||
| } else if (lfIndex !== -1) { | ||
| lineEnd = lfIndex | ||
| } | ||
| // Extract line if terminator found | ||
| if (lineEnd === -1) { | ||
| // No terminator found, rest is incomplete | ||
| incompleteLine = chunk.slice(searchIndex) | ||
| break | ||
| } else { | ||
| const line = chunk.slice(searchIndex, lineEnd) | ||
| lines.push(line) | ||
| // Move past line terminator | ||
| searchIndex = lineEnd + 1 | ||
| if (chunk[searchIndex - 1] === '\r' && chunk[searchIndex] === '\n') { | ||
| searchIndex++ | ||
| } | ||
| } | ||
| } | ||
| return [lines, incompleteLine] | ||
| /** | ||
| * Checks if `chunk` starts with the literal `event:` at index `i`. | ||
| * | ||
| * See {@link isDataPrefix} for why this is hand-unrolled rather than using | ||
| * `String.prototype.startsWith`. | ||
| * | ||
| * ASCII: 'e' = 101, 'v' = 118, 'e' = 101, 'n' = 110, 't' = 116, ':' = 58 | ||
| */ | ||
| function isEventPrefix(chunk: string, i: number, firstCharCode: number): boolean { | ||
| return ( | ||
| firstCharCode === 101 && | ||
| chunk.charCodeAt(i + 1) === 118 && | ||
| chunk.charCodeAt(i + 2) === 101 && | ||
| chunk.charCodeAt(i + 3) === 110 && | ||
| chunk.charCodeAt(i + 4) === 116 && | ||
| chunk.charCodeAt(i + 5) === 58 | ||
| ) | ||
| } |
103275
35.15%1186
26.44%