@standardserver/core
Advanced tools
+3
-2
@@ -33,4 +33,5 @@ interface EventMeta { | ||
| private readonly onEvent; | ||
| private incomplete; | ||
| private trailingCR; | ||
| private pending; | ||
| private tail; | ||
| private discardLeadingLF; | ||
| constructor(onEvent: (event: EventStreamMessage) => void); | ||
@@ -37,0 +38,0 @@ feed(chunk: string): void; |
+3
-2
@@ -33,4 +33,5 @@ interface EventMeta { | ||
| private readonly onEvent; | ||
| private incomplete; | ||
| private trailingCR; | ||
| private pending; | ||
| private tail; | ||
| private discardLeadingLF; | ||
| constructor(onEvent: (event: EventStreamMessage) => void); | ||
@@ -37,0 +38,0 @@ feed(chunk: string): void; |
+85
-54
@@ -1,2 +0,2 @@ | ||
| import { getOrBind, isTypescriptObject, tryDecodeURIComponent, toArray } from '@standardserver/shared'; | ||
| import { isTypescriptObject, getOrBind, tryDecodeURIComponent, toArray } from '@standardserver/shared'; | ||
@@ -14,31 +14,38 @@ class EventStreamEncoderError extends TypeError { | ||
| const EVENT_STREAM_LINE_DELIMITER_REGEX = /\r\n|[\n\r]/; | ||
| const EVENT_STREAM_DELIMITER_REGEX = /(?:\r\n|\r(?!\n)|\n)(?:\r\n|\r(?!\n)|\n)/; | ||
| const LEADING_WHITESPACE_REGEX = /^\s/; | ||
| const LINE_ENDING_REGEX = /\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 decodeEventStreamMessage(encoded) { | ||
| const lines = encoded.split(EVENT_STREAM_LINE_DELIMITER_REGEX); | ||
| const message = {}; | ||
| for (const line of lines) { | ||
| for (const line of encoded.split(LINE_ENDING_REGEX)) { | ||
| 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(LEADING_WHITESPACE_REGEX, ""); | ||
| const value = index === -1 ? "" : line.slice(line.charCodeAt(index + 1) === SPACE ? index + 2 : index + 1); | ||
| if (index === 0) { | ||
| message.comments ??= []; | ||
| message.comments.push(value); | ||
| } else if (key === "data") { | ||
| if (message.data !== void 0) { | ||
| message.data += ` | ||
| (message.comments ??= []).push(value); | ||
| continue; | ||
| } | ||
| switch (index === -1 ? line : line.slice(0, index)) { | ||
| case "data": | ||
| message.data = message.data === void 0 ? value : `${message.data} | ||
| ${value}`; | ||
| } else { | ||
| 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; | ||
| } | ||
| } 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; | ||
| } | ||
| } | ||
@@ -52,19 +59,51 @@ } | ||
| } | ||
| incomplete = ""; | ||
| trailingCR = false; | ||
| 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) { | ||
| if (this.trailingCR && chunk.startsWith("\n")) { | ||
| chunk = chunk.slice(1); | ||
| if (chunk === "") { | ||
| return; | ||
| } | ||
| this.trailingCR = chunk.endsWith("\r"); | ||
| this.incomplete += chunk; | ||
| const parts = this.incomplete.split(EVENT_STREAM_DELIMITER_REGEX); | ||
| this.incomplete = parts.pop(); | ||
| 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 = decodeEventStreamMessage(encoded); | ||
| this.onEvent(message); | ||
| this.onEvent(decodeEventStreamMessage(encoded)); | ||
| } | ||
| } | ||
| end() { | ||
| if (this.incomplete) { | ||
| if (this.pending.length !== 0) { | ||
| throw new EventStreamDecoderError("Event Stream ended before complete"); | ||
@@ -94,2 +133,3 @@ } | ||
| const EVENT_STREAM_LINE_ENDING_REGEX = /\r\n|[\n\r]/; | ||
| const EVENT_STREAM_LINE_ENDING_GLOBAL_REGEX = /\r\n|[\n\r]/g; | ||
| function containsEventStreamLineBreak(value) { | ||
@@ -119,11 +159,7 @@ return EVENT_STREAM_LINE_ENDING_REGEX.test(value); | ||
| function encodeEventStreamMessageData(data) { | ||
| let output = ""; | ||
| if (data !== void 0) { | ||
| const lines = data.split(EVENT_STREAM_LINE_ENDING_REGEX); | ||
| for (const line of lines) { | ||
| output += `data: ${line} | ||
| if (data === void 0) { | ||
| return ""; | ||
| } | ||
| return `data: ${data.replace(EVENT_STREAM_LINE_ENDING_GLOBAL_REGEX, "\ndata: ")} | ||
| `; | ||
| } | ||
| } | ||
| return output; | ||
| } | ||
@@ -238,15 +274,10 @@ function encodeEventStreamMessageComments(comments) { | ||
| function mergeStandardHeaders(a, b) { | ||
| const merged = { ...a }; | ||
| const merged = { ...a, ...b }; | ||
| for (const key in b) { | ||
| if (Array.isArray(b[key])) { | ||
| merged[key] = [...toArray(merged[key]), ...b[key]]; | ||
| } else if (b[key] !== void 0) { | ||
| if (Array.isArray(merged[key])) { | ||
| merged[key] = [...merged[key], b[key]]; | ||
| } else if (merged[key] !== void 0) { | ||
| merged[key] = [merged[key], b[key]]; | ||
| } else { | ||
| merged[key] = b[key]; | ||
| } | ||
| if (!Object.hasOwn(a, key)) { | ||
| continue; | ||
| } | ||
| const aValue = a[key]; | ||
| const bValue = b[key]; | ||
| merged[key] = aValue === void 0 || bValue === void 0 ? aValue ?? bValue : [...toArray(aValue), ...toArray(bValue)]; | ||
| } | ||
@@ -253,0 +284,0 @@ return merged; |
+2
-2
| { | ||
| "name": "@standardserver/core", | ||
| "type": "module", | ||
| "version": "0.5.0", | ||
| "version": "0.6.0", | ||
| "license": "MIT", | ||
@@ -25,3 +25,3 @@ "homepage": "https://standardserver.dev", | ||
| "dependencies": { | ||
| "@standardserver/shared": "0.5.0" | ||
| "@standardserver/shared": "0.6.0" | ||
| }, | ||
@@ -28,0 +28,0 @@ "scripts": { |
+10
-7
@@ -10,2 +10,5 @@ # @standardserver/core | ||
| </a> | ||
| <a href="https://app.codspeed.io/middleapi/standardserver?utm_source=badge"> | ||
| <img src="https://img.shields.io/endpoint?url=https://codspeed.io/badge.json" alt="CodSpeed" /> | ||
| </a> | ||
| <a href="https://github.com/middleapi/standardserver/blob/main/LICENSE"> | ||
@@ -347,8 +350,6 @@ <img alt="MIT License" src="https://img.shields.io/github/license/middleapi/standardserver?logo=open-source-initiative" /> | ||
| <td align="center"><a href="https://github.com/K-Mistele?ref=orpc" target="_blank" rel="noopener" title="Kyle Mistele"><img src="https://avatars.githubusercontent.com/u/18430555?u=3afebeb81de666e35aaac3ed46f14159d7603ffb&v=4" width="139" alt="Kyle Mistele"/><br />Kyle Mistele</a></td> | ||
| <td align="center"><a href="https://github.com/andrewpeters9?ref=orpc" target="_blank" rel="noopener" title="Andrew Peters"><img src="https://avatars.githubusercontent.com/u/36251325?v=4" width="139" alt="Andrew Peters"/><br />Andrew Peters</a></td> | ||
| <td align="center"><a href="https://github.com/R44VC0RP?ref=orpc" target="_blank" rel="noopener" title="Ryan Vogel"><img src="https://avatars.githubusercontent.com/u/89211796?u=1857347b9787d8d8a7ea5bfc333f96be92d5a683&v=4" width="139" alt="Ryan Vogel"/><br />Ryan Vogel</a></td> | ||
| <td align="center"><a href="https://github.com/christ12938?ref=orpc" target="_blank" rel="noopener" title="christ12938"><img src="https://avatars.githubusercontent.com/u/25758598?v=4" width="139" alt="christ12938"/><br />christ12938</a></td> | ||
| <td align="center"><a href="https://github.com/Ryanjso?ref=orpc" target="_blank" rel="noopener" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center"><a href="https://github.com/christ12938?ref=orpc" target="_blank" rel="noopener" title="christ12938"><img src="https://avatars.githubusercontent.com/u/25758598?v=4" width="139" alt="christ12938"/><br />christ12938</a></td> | ||
| <td align="center"><a href="https://github.com/Ryanjso?ref=orpc" target="_blank" rel="noopener" title="Ryan Soderberg"><img src="https://avatars.githubusercontent.com/u/39172778?u=5ed913c31d57e7221b75784abcad48c7ebddde27&v=4" width="139" alt="Ryan Soderberg"/><br />Ryan Soderberg</a></td> | ||
| <td align="center"><a href="https://github.com/itigoore01?ref=orpc" target="_blank" rel="noopener" title="shota"><img src="https://avatars.githubusercontent.com/u/11831107?u=c976a6dc7e055eb026304c46c99100ed22b0c8e0&v=4" width="139" alt="shota"/><br />shota</a></td> | ||
@@ -363,3 +364,2 @@ </tr> | ||
| <td align="center"><a href="https://github.com/rhinodavid?ref=orpc" target="_blank" rel="noopener" title="David Walsh"><img src="https://avatars.githubusercontent.com/u/5778036?u=b5521f07d2f88c3db2a0dae62b5f2f8357214af0&v=4" width="119" alt="David Walsh"/><br />David Walsh</a></td> | ||
| <td align="center"><a href="https://github.com/Nic13Gamer?ref=orpc" target="_blank" rel="noopener" title="Nicholas"><img src="https://avatars.githubusercontent.com/u/54724556?u=56a7ab430ce7a80d648ab6eba051d454a818ed0b&v=4" width="119" alt="Nicholas"/><br />Nicholas</a></td> | ||
| <td align="center"><a href="https://github.com/Robbe95?ref=orpc" target="_blank" rel="noopener" title="Robbe Vaes"><img src="https://avatars.githubusercontent.com/u/44748019?u=e0232402c045ad4eac7cbd217f1f47e083103b89&v=4" width="119" alt="Robbe Vaes"/><br />Robbe Vaes</a></td> | ||
@@ -370,5 +370,5 @@ <td align="center"><a href="https://github.com/aidansunbury?ref=orpc" target="_blank" rel="noopener" title="Aidan Sunbury"><img src="https://avatars.githubusercontent.com/u/64103161?v=4" width="119" alt="Aidan Sunbury"/><br />Aidan Sunbury</a></td> | ||
| <td align="center"><a href="https://github.com/pumpkinlink?ref=orpc" target="_blank" rel="noopener" title="Denis"><img src="https://avatars.githubusercontent.com/u/11864620?u=5f47bbe6c65d0f6f5cf011021490238e4b0593d0&v=4" width="119" alt="Denis"/><br />Denis</a></td> | ||
| <td align="center"><a href="https://github.com/christopher-kapic?ref=orpc" target="_blank" rel="noopener" title="Christopher Kapic"><img src="https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&v=4" width="119" alt="Christopher Kapic"/><br />Christopher Kapic</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center"><a href="https://github.com/christopher-kapic?ref=orpc" target="_blank" rel="noopener" title="Christopher Kapic"><img src="https://avatars.githubusercontent.com/u/59740769?u=e7ad4b72b5bf6c9eb1644c26dbf3332a8f987377&v=4" width="119" alt="Christopher Kapic"/><br />Christopher Kapic</a></td> | ||
| <td align="center"><a href="https://github.com/thomasballinger?ref=orpc" target="_blank" rel="noopener" title="Tom Ballinger"><img src="https://avatars.githubusercontent.com/u/458879?u=4b045ac75d721b6ac2b42a74d7d37f61f0414031&v=4" width="119" alt="Tom Ballinger"/><br />Tom Ballinger</a></td> | ||
@@ -380,5 +380,5 @@ <td align="center"><a href="https://github.com/SSam0419?ref=orpc" target="_blank" rel="noopener" title="Sam"><img src="https://avatars.githubusercontent.com/u/102863520?u=3c89611f549d5070be232eb4532f690c8f2e7a65&v=4" width="119" alt="Sam"/><br />Sam</a></td> | ||
| <td align="center"><a href="https://github.com/ldub?ref=orpc" target="_blank" rel="noopener" title="Lev Dubinets"><img src="https://avatars.githubusercontent.com/u/3114081?u=f547f5d5012cab54851f1b1ad72d10e537f78fc2&v=4" width="119" alt="Lev Dubinets"/><br />Lev Dubinets</a></td> | ||
| <td align="center"><a href="https://github.com/mr-kelly?ref=orpc" target="_blank" rel="noopener" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td> | ||
| </tr> | ||
| <tr> | ||
| <td align="center"><a href="https://github.com/mr-kelly?ref=orpc" target="_blank" rel="noopener" title="Kelly Peilin Chan"><img src="https://avatars.githubusercontent.com/u/520852?u=6b0f7105f694e7b5cacf410a3f04c7044b469dc8&v=4" width="119" alt="Kelly Peilin Chan"/><br />Kelly Peilin Chan</a></td> | ||
| <td align="center"><a href="https://github.com/piscis?ref=orpc" target="_blank" rel="noopener" title="Alex"><img src="https://avatars.githubusercontent.com/u/326163?u=b245f368bd940cf51d08c0b6bf55f8257f359437&v=4" width="119" alt="Alex"/><br />Alex</a></td> | ||
@@ -415,2 +415,4 @@ <td align="center"><a href="https://github.com/finom?ref=orpc" target="_blank" rel="noopener" title="Andrey Gubanov"><img src="https://avatars.githubusercontent.com/u/1082083?u=29e91400dbd4a9c217048a8f59562c4f740498e6&v=4" width="119" alt="Andrey Gubanov"/><br />Andrey Gubanov</a></td> | ||
| <a href="https://github.com/wobsoriano?ref=orpc" target="_blank" rel="noopener" title="Robert Soriano"><img src="https://avatars.githubusercontent.com/u/13049130?u=6d72104182e7c9ed25934815313fb69107332111&v=4" width="32" height="32" alt="Robert Soriano" /></a> | ||
| <a href="https://github.com/andrewpeters9?ref=orpc" target="_blank" rel="noopener" title="Andrew Peters"><img src="https://avatars.githubusercontent.com/u/36251325?v=4" width="32" height="32" alt="Andrew Peters" /></a> | ||
| <a href="https://github.com/R44VC0RP?ref=orpc" target="_blank" rel="noopener" title="Ryan Vogel"><img src="https://avatars.githubusercontent.com/u/89211796?u=1857347b9787d8d8a7ea5bfc333f96be92d5a683&v=4" width="32" height="32" alt="Ryan Vogel" /></a> | ||
| <a href="https://github.com/SKostyukovich?ref=orpc" target="_blank" rel="noopener" title="SKostyukovich"><img src="https://avatars.githubusercontent.com/u/10700067?v=4" width="32" height="32" alt="SKostyukovich" /></a> | ||
@@ -428,2 +430,3 @@ <a href="https://github.com/peter-adam-dy?ref=orpc" target="_blank" rel="noopener" title="Peter Adam"><img src="https://avatars.githubusercontent.com/u/132129459?u=4f3dbbb3b443990b56acb7d6a5d11ed2c555f6db&v=4" width="32" height="32" alt="Peter Adam" /></a> | ||
| <a href="https://github.com/plancraft?ref=orpc" target="_blank" rel="noopener" title="plancraft"><img src="https://avatars.githubusercontent.com/u/46482287?v=4" width="32" height="32" alt="plancraft" /></a> | ||
| <a href="https://github.com/Nic13Gamer?ref=orpc" target="_blank" rel="noopener" title="Nicholas"><img src="https://avatars.githubusercontent.com/u/54724556?u=56a7ab430ce7a80d648ab6eba051d454a818ed0b&v=4" width="32" height="32" alt="Nicholas" /></a> | ||
| </p> |
57050
2.02%479
7.16%426
0.71%+ Added
- Removed
Updated