@orpc/standard-server
Advanced tools
+3
-1
@@ -22,3 +22,5 @@ import { AsyncIteratorClass } from '@orpc/shared'; | ||
| private options; | ||
| private incomplete; | ||
| private pending; | ||
| private tail; | ||
| private discardLeadingLF; | ||
| constructor(options?: EventDecoderOptions); | ||
@@ -25,0 +27,0 @@ feed(chunk: string): void; |
+3
-1
@@ -22,3 +22,5 @@ import { AsyncIteratorClass } from '@orpc/shared'; | ||
| private options; | ||
| private incomplete; | ||
| private pending; | ||
| private tail; | ||
| private discardLeadingLF; | ||
| constructor(options?: EventDecoderOptions); | ||
@@ -25,0 +27,0 @@ feed(chunk: string): void; |
+88
-40
@@ -15,4 +15,9 @@ import { isTypescriptObject, AsyncIteratorClass, tryDecodeURIComponent, toArray, once, isAsyncIteratorObject, replicateAsyncIterator } from '@orpc/shared'; | ||
| const LINE_ENDING_REGEX$1 = /\r\n|\r(?!\n)|\n/; | ||
| const MESSAGE_DELIMITER_REGEX = /(?:\r\n|\r(?!\n)|\n){2}/; | ||
| const MESSAGE_DELIMITER_GLOBAL_REGEX = /(?:\r\n|\r(?!\n)|\n){2}/g; | ||
| const CR = 13; | ||
| const LF = 10; | ||
| const SPACE = 32; | ||
| function decodeEventMessage(encoded) { | ||
| const lines = encoded.replace(/\n+$/, "").split(/\n/); | ||
| const message = { | ||
@@ -25,24 +30,32 @@ data: void 0, | ||
| }; | ||
| for (const line of lines) { | ||
| for (const line of encoded.split(LINE_ENDING_REGEX$1)) { | ||
| if (line === "") { | ||
| continue; | ||
| } | ||
| const index = line.indexOf(":"); | ||
| const key = index === -1 ? line : line.slice(0, index); | ||
| const value = index === -1 ? "" : line.slice(index + 1).replace(/^\s/, ""); | ||
| const value = index === -1 ? "" : line.slice(line.charCodeAt(index + 1) === SPACE ? index + 2 : index + 1); | ||
| if (index === 0) { | ||
| message.comments.push(value); | ||
| } else if (key === "data") { | ||
| message.data ??= ""; | ||
| message.data += `${value} | ||
| `; | ||
| } else if (key === "event") { | ||
| message.event = value; | ||
| } else if (key === "id") { | ||
| message.id = value; | ||
| } else if (key === "retry") { | ||
| const maybeInteger = Number.parseInt(value); | ||
| if (Number.isInteger(maybeInteger) && maybeInteger >= 0 && maybeInteger.toString() === value) { | ||
| message.retry = maybeInteger; | ||
| continue; | ||
| } | ||
| switch (index === -1 ? line : line.slice(0, index)) { | ||
| case "data": | ||
| message.data = message.data === void 0 ? value : `${message.data} | ||
| ${value}`; | ||
| break; | ||
| case "event": | ||
| message.event = value; | ||
| break; | ||
| case "id": | ||
| message.id = value; | ||
| break; | ||
| case "retry": { | ||
| const maybeInteger = Number.parseInt(value, 10); | ||
| if (maybeInteger >= 0 && maybeInteger.toString() === value) { | ||
| message.retry = maybeInteger; | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| message.data = message.data?.replace(/\n$/, ""); | ||
| return message; | ||
@@ -54,15 +67,47 @@ } | ||
| } | ||
| incomplete = ""; | ||
| pending = []; | ||
| // Last up-to-3 characters of the pending buffer, prefixed to the next chunk | ||
| // so a delimiter straddling the boundary is still found. | ||
| tail = ""; | ||
| // Set when a chunk-ending '\r' was already consumed as a line ending, so a | ||
| // leading '\n' in the next chunk is the second half of that CRLF pair. | ||
| discardLeadingLF = false; | ||
| feed(chunk) { | ||
| this.incomplete += chunk; | ||
| const lastCompleteIndex = this.incomplete.lastIndexOf("\n\n"); | ||
| if (lastCompleteIndex === -1) { | ||
| if (chunk === "") { | ||
| return; | ||
| } | ||
| const completes = this.incomplete.slice(0, lastCompleteIndex).split(/\n\n/); | ||
| this.incomplete = this.incomplete.slice(lastCompleteIndex + 2); | ||
| for (const encoded of completes) { | ||
| const message = decodeEventMessage(`${encoded} | ||
| `); | ||
| if (this.discardLeadingLF) { | ||
| this.discardLeadingLF = false; | ||
| if (chunk.charCodeAt(0) === LF) { | ||
| chunk = chunk.slice(1); | ||
| if (chunk === "") { | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| const scan = this.tail + chunk; | ||
| if (!MESSAGE_DELIMITER_REGEX.test(scan)) { | ||
| this.pending.push(chunk); | ||
| this.tail = scan.slice(-3); | ||
| return; | ||
| } | ||
| this.pending.push(chunk); | ||
| const buffered = this.pending.length === 1 ? chunk : this.pending.join(""); | ||
| const offset = buffered.length - scan.length; | ||
| const parts = []; | ||
| let start = 0; | ||
| for (const match of scan.matchAll(MESSAGE_DELIMITER_GLOBAL_REGEX)) { | ||
| parts.push(buffered.slice(start, offset + match.index)); | ||
| start = offset + match.index + match[0].length; | ||
| } | ||
| const incomplete = buffered.slice(start); | ||
| this.pending.length = 0; | ||
| this.tail = incomplete.slice(-3); | ||
| if (incomplete === "") { | ||
| this.discardLeadingLF = chunk.charCodeAt(chunk.length - 1) === CR; | ||
| } else { | ||
| this.pending.push(incomplete); | ||
| } | ||
| for (const encoded of parts) { | ||
| const message = decodeEventMessage(encoded); | ||
| if (this.options.onEvent) { | ||
@@ -74,3 +119,3 @@ this.options.onEvent(message); | ||
| end() { | ||
| if (this.incomplete) { | ||
| if (this.pending.length !== 0) { | ||
| throw new EventDecoderError("Event Iterator ended before complete"); | ||
@@ -101,10 +146,15 @@ } | ||
| const LINE_ENDING_REGEX = /\r\n|[\n\r]/; | ||
| const LINE_ENDING_GLOBAL_REGEX = /\r\n|[\n\r]/g; | ||
| function containsLineBreak(value) { | ||
| return LINE_ENDING_REGEX.test(value); | ||
| } | ||
| function assertEventId(id) { | ||
| if (id.includes("\n")) { | ||
| throw new EventEncoderError("Event's id must not contain a newline character"); | ||
| if (containsLineBreak(id)) { | ||
| throw new EventEncoderError("Event's id must not contain a carriage return or newline character"); | ||
| } | ||
| } | ||
| function assertEventName(event) { | ||
| if (event.includes("\n")) { | ||
| throw new EventEncoderError("Event's event must not contain a newline character"); | ||
| if (containsLineBreak(event)) { | ||
| throw new EventEncoderError("Event's event must not contain a carriage return or newline character"); | ||
| } | ||
@@ -118,14 +168,12 @@ } | ||
| function assertEventComment(comment) { | ||
| if (comment.includes("\n")) { | ||
| throw new EventEncoderError("Event's comment must not contain a newline character"); | ||
| if (containsLineBreak(comment)) { | ||
| throw new EventEncoderError("Event's comment must not contain a carriage return or newline character"); | ||
| } | ||
| } | ||
| function encodeEventData(data) { | ||
| const lines = data?.split(/\n/) ?? []; | ||
| let output = ""; | ||
| for (const line of lines) { | ||
| output += `data: ${line} | ||
| if (data === void 0) { | ||
| return ""; | ||
| } | ||
| return `data: ${data.replace(LINE_ENDING_GLOBAL_REGEX, "\ndata: ")} | ||
| `; | ||
| } | ||
| return output; | ||
| } | ||
@@ -132,0 +180,0 @@ function encodeEventComments(comments) { |
+2
-2
| { | ||
| "name": "@orpc/standard-server", | ||
| "type": "module", | ||
| "version": "1.14.12", | ||
| "version": "1.14.13", | ||
| "license": "MIT", | ||
@@ -32,3 +32,3 @@ "homepage": "https://orpc.dev", | ||
| "dependencies": { | ||
| "@orpc/shared": "1.14.12" | ||
| "@orpc/shared": "1.14.13" | ||
| }, | ||
@@ -35,0 +35,0 @@ "scripts": { |
52357
3.31%589
9.48%+ Added
- Removed
Updated