@orkestrel/websocket 0.0.11 → 0.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,68 +2,183 @@ import { createHash, randomBytes } from "node:crypto";
2
2
  import { Emitter } from "@orkestrel/emitter";
3
3
  //#region src/server/constants.ts
4
4
  /**
5
- * Names the RFC 6455 GUID concatenated to a client's `Sec-WebSocket-Key` before the SHA-1
6
- * hash that yields the `Sec-WebSocket-Accept` response value.
5
+ * Names the accept GUID concatenated to a client's `Sec-WebSocket-Key` before the accept
6
+ * hash, '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'.
7
7
  *
8
8
  * @remarks
9
- * A fixed, spec-mandated constant (RFC 6455 §4.2.2) read only by
9
+ * The base64-encoded SHA-1 of that concatenation is the `Sec-WebSocket-Accept` response
10
+ * value. A fixed, spec-mandated constant (RFC 6455 §4.2.2) — read only by
10
11
  * {@link computeWebSocketAccept}.
11
12
  */
12
13
  var WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
13
- /** Names the WebSocket protocol version this wrapper speaks (`Sec-WebSocket-Version: 13`). */
14
+ /**
15
+ * Names the supported protocol version, '13'.
16
+ *
17
+ * @remarks
18
+ * The value this wrapper speaks, carried by the `Sec-WebSocket-Version` handshake header.
19
+ */
14
20
  var WEBSOCKET_VERSION = "13";
15
- /** Names the text frame opcode — a UTF-8 payload (RFC 6455 §5.6). */
21
+ /**
22
+ * Names the text frame opcode, 0x01.
23
+ *
24
+ * @remarks
25
+ * A UTF-8 payload (RFC 6455 §5.6).
26
+ */
16
27
  var WEBSOCKET_OPCODE_TEXT = 1;
17
- /** Names the binary frame opcode — a raw byte payload (RFC 6455 §5.6). */
28
+ /**
29
+ * Names the binary frame opcode, 0x02.
30
+ *
31
+ * @remarks
32
+ * A raw byte payload (RFC 6455 §5.6).
33
+ */
18
34
  var WEBSOCKET_OPCODE_BINARY = 2;
19
- /** Names the continuation frame opcode — the next fragment of an open data message (RFC 6455 §5.4). */
35
+ /**
36
+ * Names the continuation frame opcode, 0x00.
37
+ *
38
+ * @remarks
39
+ * The next fragment of an open data message (RFC 6455 §5.4).
40
+ */
20
41
  var WEBSOCKET_OPCODE_CONTINUATION = 0;
21
- /** Names the close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */
42
+ /**
43
+ * Names the close frame opcode, 0x08.
44
+ *
45
+ * @remarks
46
+ * A control frame ending the connection (RFC 6455 §5.5.1).
47
+ */
22
48
  var WEBSOCKET_OPCODE_CLOSE = 8;
23
- /** Names the ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */
49
+ /**
50
+ * Names the ping frame opcode, 0x09.
51
+ *
52
+ * @remarks
53
+ * A control frame the peer must answer with a pong (RFC 6455 §5.5.2).
54
+ */
24
55
  var WEBSOCKET_OPCODE_PING = 9;
25
- /** Names the pong frame opcode — a control frame answering a ping (RFC 6455 §5.5.3). */
56
+ /**
57
+ * Names the pong frame opcode, 0x0a.
58
+ *
59
+ * @remarks
60
+ * A control frame answering a ping (RFC 6455 §5.5.3).
61
+ */
26
62
  var WEBSOCKET_OPCODE_PONG = 10;
27
- /** Names the ready state for a connecting WebSocket (before the handshake completes). */
63
+ /**
64
+ * Names the connecting ready state, 0.
65
+ *
66
+ * @remarks
67
+ * The state a WebSocket holds before its handshake completes.
68
+ */
28
69
  var WEBSOCKET_READY_CONNECTING = 0;
29
- /** Names the ready state for an open WebSocket (the handshake completed; frames flow). */
70
+ /**
71
+ * Names the open ready state, 1.
72
+ *
73
+ * @remarks
74
+ * The state a WebSocket holds after the handshake completes and while frames flow.
75
+ */
30
76
  var WEBSOCKET_READY_OPEN = 1;
31
- /** Names the ready state for a closing WebSocket (a close frame was sent or received). */
77
+ /**
78
+ * Names the closing ready state, 2.
79
+ *
80
+ * @remarks
81
+ * The state a WebSocket holds after a close frame is sent or received.
82
+ */
32
83
  var WEBSOCKET_READY_CLOSING = 2;
33
- /** Names the ready state for a closed WebSocket (the socket ended). */
84
+ /**
85
+ * Names the closed ready state, 3.
86
+ *
87
+ * @remarks
88
+ * The state a WebSocket holds after the socket ends.
89
+ */
34
90
  var WEBSOCKET_READY_CLOSED = 3;
35
- /** Names the normal-closure status code (RFC 6455 §7.4.1) — the default `close` code. */
91
+ /**
92
+ * Names the normal-closure status code, 1000.
93
+ *
94
+ * @remarks
95
+ * The default `close` code (RFC 6455 §7.4.1).
96
+ */
36
97
  var WEBSOCKET_CLOSE_NORMAL = 1e3;
37
- /** Names the protocol-error status code (RFC 6455 §7.4.1) — a framing/state rule was violated. */
98
+ /**
99
+ * Names the protocol-error status code, 1002.
100
+ *
101
+ * @remarks
102
+ * Sent when a framing or state rule was violated (RFC 6455 §7.4.1).
103
+ */
38
104
  var WEBSOCKET_CLOSE_PROTOCOL = 1002;
39
- /** Names the unsupported-data status code (RFC 6455 §7.4.1) — the endpoint received a data type it cannot accept, for example binary on a text-only endpoint. */
105
+ /**
106
+ * Names the unsupported-data status code, 1003.
107
+ *
108
+ * @remarks
109
+ * Sent when the endpoint received a data type it cannot accept (RFC 6455 §7.4.1), for
110
+ * example binary on a text-only endpoint.
111
+ */
40
112
  var WEBSOCKET_CLOSE_UNSUPPORTED = 1003;
41
- /** Names the invalid-frame-payload-data status code (RFC 6455 §7.4.1) — for example non-UTF-8 text or an unparseable close reason. */
113
+ /**
114
+ * Names the invalid-frame-payload-data status code, 1007.
115
+ *
116
+ * @remarks
117
+ * Sent for non-UTF-8 text or an unparseable close reason (RFC 6455 §7.4.1).
118
+ */
42
119
  var WEBSOCKET_CLOSE_INVALID = 1007;
43
- /** Names the message-too-big status code (RFC 6455 §7.4.1) — a reassembled message exceeded the payload cap. */
120
+ /**
121
+ * Names the message-too-big status code, 1009.
122
+ *
123
+ * @remarks
124
+ * Sent when a reassembled message exceeded the payload cap (RFC 6455 §7.4.1).
125
+ */
44
126
  var WEBSOCKET_CLOSE_TOO_BIG = 1009;
45
- /** Names the default maximum inbound single-frame length AND reassembled-message total byte count (100 MiB — the `ws` package default). */
127
+ /**
128
+ * Names the default cap on both an inbound frame's declared length and a reassembled
129
+ * message's total byte count, 104,857,600 bytes (100 MiB).
130
+ *
131
+ * @remarks
132
+ * The same value the `ws` package defaults to. Either breach closes
133
+ * {@link WEBSOCKET_CLOSE_TOO_BIG}.
134
+ */
46
135
  var WEBSOCKET_MAX_PAYLOAD = 104857600;
47
- /** Names the default close-handshake timeout in milliseconds — how long `close()` waits for the peer's echo before tearing the socket down. */
136
+ /**
137
+ * Names the default close-handshake timeout, 30,000 milliseconds — how long `close` waits
138
+ * for the peer's echo.
139
+ *
140
+ * @remarks
141
+ * After it expires the wrapper tears the socket down, so a silent peer cannot leak the
142
+ * handle open.
143
+ */
48
144
  var WEBSOCKET_CLOSE_TIMEOUT_MS = 3e4;
49
- /** Names the post-`#fail` flush grace in milliseconds — how long a validation-breach close frame is given to flush through the socket's write buffer before the hard `destroy()` fallback fires (the normal path destroys sooner, on the `end()` flush callback). */
145
+ /**
146
+ * Names the flush grace, 1,000 milliseconds, a validation-breach close frame is given
147
+ * before the hard teardown fallback destroys the socket.
148
+ *
149
+ * @remarks
150
+ * Armed after `#fail` writes the close frame, so the frame drains through the socket's
151
+ * write buffer rather than being discarded. The normal path destroys sooner, on the
152
+ * `end()` flush callback.
153
+ */
50
154
  var WEBSOCKET_FAIL_TIMEOUT_MS = 1e3;
51
- /** Names the maximum control-frame payload length in bytes (RFC 6455 §5.5). */
155
+ /**
156
+ * Names the maximum control-frame payload length, 125 bytes.
157
+ *
158
+ * @remarks
159
+ * The cap RFC 6455 §5.5 sets on every control frame's payload.
160
+ */
52
161
  var WEBSOCKET_CONTROL_MAX_LENGTH = 125;
53
- /** Names the maximum UTF-8 close-reason length after the two-byte status code. */
162
+ /**
163
+ * Names the maximum UTF-8 close-reason length after the two-byte status code, 123.
164
+ *
165
+ * @remarks
166
+ * What is left of {@link WEBSOCKET_CONTROL_MAX_LENGTH} after the close frame's status
167
+ * code.
168
+ */
54
169
  var WEBSOCKET_CLOSE_REASON_MAX_LENGTH = 123;
55
170
  //#endregion
56
171
  //#region src/server/errors.ts
57
172
  /**
58
- * Represents an error thrown by the WebSocket wrapper for a refused caller-supplied value.
173
+ * Represents an error the WebSocket wrapper throws for a refused caller-supplied value,
174
+ * carrying a machine-readable `code` and an optional `context`.
59
175
  *
60
176
  * @remarks
61
- * Carries a {@link WebSocketErrorCode} and an optional `context` record holding the
62
- * refused value under a key naming it: an `'OPTION'` carries the offending option
63
- * (`payload`, `timeout`, `key`, or `protocol`), a `'LIMIT'` carries `size` and the
64
- * `limit` it exceeded, a `'CLOSE'` carries the refused close `code`, and a `'FRAME'`
65
- * carries `opcode` or the mask's `size`. Narrow a caught value with
66
- * {@link isWebSocketError}.
177
+ * The `code` is a {@link WebSocketErrorCode}; the `context` record holds the refused
178
+ * value under a key naming it: an `'OPTION'` carries the offending option (`payload`,
179
+ * `timeout`, `key`, or `protocol`), a `'LIMIT'` carries `size` and the `limit` it
180
+ * exceeded, a `'CLOSE'` carries the refused close `code`, and a `'FRAME'` carries
181
+ * `opcode` or the mask's `size`. Narrow a caught value with {@link isWebSocketError}.
67
182
  *
68
183
  * @example
69
184
  * ```ts
@@ -94,7 +209,8 @@ var WebSocketError = class extends Error {
94
209
  }
95
210
  };
96
211
  /**
97
- * Checks whether a value is a {@link WebSocketError}.
212
+ * Checks whether a caught value is a {@link WebSocketError}, narrowing it so a `catch` can
213
+ * branch on `error.code`.
98
214
  *
99
215
  * @param value - The value to test (typically a `catch` binding)
100
216
  * @returns True if `value` is a `WebSocketError`; false otherwise
@@ -130,15 +246,15 @@ function computeWebSocketAccept(key) {
130
246
  return createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
131
247
  }
132
248
  /**
133
- * Reads the declared payload length off the front of a buffer, without buffering or
134
- * reading the payload itself.
249
+ * Reads the declared payload length off the front of a buffer without buffering or
250
+ * reading the payload itself, answering `undefined` until the length field is complete.
135
251
  *
136
252
  * @remarks
137
253
  * Decodes only byte 1's 7-bit length field, extended by the 16-bit (`126`) or 64-bit
138
254
  * (`127`) form exactly like `parseWebSocketFrame` — but stops there, so a caller
139
255
  * can reject an over-cap frame the moment its length is known, before the payload
140
- * bytes have even arrived. Returns `undefined` until the length field itself is fully
141
- * buffered (mirrors the parser's incomplete-buffer contract). Pure; never throws.
256
+ * bytes have even arrived. The incomplete-buffer contract mirrors the parser's. Pure;
257
+ * never throws.
142
258
  *
143
259
  * @param buffer - The accumulation buffer to read the next frame's length from
144
260
  * @returns The declared payload length, or `undefined` when the buffer is too short to know it yet
@@ -165,14 +281,14 @@ function measureWebSocketFrame(buffer) {
165
281
  return length;
166
282
  }
167
283
  /**
168
- * Checks whether the next frame uses the shortest valid RFC 6455 payload-length encoding.
284
+ * Checks whether the next frame uses the shortest valid RFC 6455 payload-length
285
+ * encoding, answering `undefined` until its length prefix is complete.
169
286
  *
170
287
  * @remarks
171
- * Returns `undefined` until the complete length prefix is buffered. The 16-bit form
172
- * is canonical only for lengths at least 126; the 64-bit form only for lengths at
173
- * least 65,536 and with its most-significant bit clear (RFC 6455 §5.2). Reads the same
174
- * length prefix as {@link measureWebSocketFrame}, under the same incomplete-buffer
175
- * contract. Pure; never throws.
288
+ * The 16-bit form is canonical only for lengths at least 126; the 64-bit form only for
289
+ * lengths at least 65,536 and with its most-significant bit clear (RFC 6455 §5.2). Reads
290
+ * the same length prefix as {@link measureWebSocketFrame}, under the same
291
+ * incomplete-buffer contract. Pure; never throws.
176
292
  *
177
293
  * @param buffer - The accumulation buffer containing the next frame header
178
294
  * @returns Its canonicality, or `undefined` while the length prefix is incomplete
@@ -205,7 +321,7 @@ function matchesWebSocketCanonical(buffer) {
205
321
  * the 7-bit form below 126, the `126` + 16-bit form below 65 536, or the `127` +
206
322
  * 64-bit form beyond; when `masked` is set the mask bit is set, a 4-byte key (supplied
207
323
  * through `options.mask`, else random) is written, and the payload is XOR-masked. Server→
208
- * client frames are unmasked (the default); pass `masked: true` to encode a CLIENT
324
+ * client frames are unmasked (the default); pass `masked: true` to encode a client
209
325
  * frame (for example to feed the parser in a test). A `string` payload is encoded as
210
326
  * UTF-8. Returns one contiguous `Buffer` (header + payload), so the wrapper writes it
211
327
  * with a single `socket.write`. Pure.
@@ -284,7 +400,8 @@ function isWebSocketProtocol(protocol) {
284
400
  return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(protocol);
285
401
  }
286
402
  /**
287
- * Checks whether a numeric value is a valid RFC 6455 close status code to RECEIVE (§7.4.1).
403
+ * Checks whether a numeric value is a close status code an RFC 6455 endpoint may
404
+ * receive (§7.4.1).
288
405
  *
289
406
  * @remarks
290
407
  * True for `1000`–`1003`, `1007`–`1014`, and the application range `3000`–`4999`; false
@@ -314,18 +431,19 @@ function isCloseCode(code) {
314
431
  //#endregion
315
432
  //#region src/server/parsers.ts
316
433
  /**
317
- * Decodes a single RFC 6455 frame from the front of a buffer.
434
+ * Decodes a single RFC 6455 frame from the front of a buffer, answering `undefined`
435
+ * while the buffer is incomplete so the caller accumulates and retries.
318
436
  *
319
437
  * @remarks
320
438
  * Reads the FIN bit and opcode (byte 0), the mask bit and 7-bit payload length (byte
321
439
  * 1) — extended to a 16-bit length when the 7-bit field is `126`, or a 64-bit length
322
440
  * when it is `127` — the optional 4-byte mask key, then the payload, XOR-unmasking it
323
- * against the key when the mask bit is set (client→server frames MUST be masked, RFC
441
+ * against the key when the mask bit is set (client→server frames must be masked, RFC
324
442
  * 6455 §5.3; an unmasked frame still decodes, leaving the payload as-is, so the caller
325
- * can enforce policy). Returns `undefined` the moment the buffer is too short for the
326
- * part it is up to (the length prefix, the mask, or the full payload) — the signal to
327
- * the caller to read more bytes and retry. `consumed` is the total bytes the frame
328
- * occupied, so the caller slices the remainder. Pure; never throws on a short buffer.
443
+ * can enforce policy). The incomplete answer comes the moment the buffer is too short
444
+ * for the part it is up to: the length prefix, the mask, or the full payload.
445
+ * `consumed` is the total bytes the frame occupied, so the caller slices the remainder.
446
+ * Pure; never throws on a short buffer.
329
447
  *
330
448
  * @param buffer - The accumulation buffer to decode the next frame from
331
449
  * @returns The parsed {@link WebSocketFrame}, or `undefined` when the buffer is incomplete
@@ -377,12 +495,13 @@ function parseWebSocketFrame(buffer) {
377
495
  };
378
496
  }
379
497
  /**
380
- * Decodes a byte sequence as strict UTF-8, or signals it is malformed.
498
+ * Decodes a byte sequence as strict UTF-8, answering `undefined` when the sequence is
499
+ * malformed.
381
500
  *
382
501
  * @remarks
383
- * Wraps `TextDecoder('utf-8', { fatal: true })` in a try/catch so a malformed sequence
384
- * returns `undefined` instead of throwing — a guard-adjacent coercer never throws on bad
385
- * input. Pure.
502
+ * Wraps `TextDecoder('utf-8', { fatal: true })` in a try/catch, so a malformed sequence
503
+ * returns rather than throwing — a guard-adjacent coercer never throws on bad input.
504
+ * Pure.
386
505
  *
387
506
  * @param bytes - The raw bytes to decode
388
507
  * @returns The decoded string, or `undefined` when `bytes` is not valid UTF-8
@@ -403,18 +522,19 @@ function parseUTF8(bytes) {
403
522
  //#endregion
404
523
  //#region src/server/NodeWebSocket.ts
405
524
  /**
406
- * Represents a server-native WebSocket over a raw upgraded `node:stream` Duplex — the lean
407
- * wrapper around the RFC 6455 wire protocol.
525
+ * Implements the wrapper contract over a raw upgraded `node:stream` Duplex socket,
526
+ * driving the RFC 6455 handshake, the frame codec, auto-pong, and the close handshake,
527
+ * and surfacing every event on an owned `emitter`.
408
528
  *
409
529
  * @remarks
410
- * Created by `createNodeWebSocket`. When given a client `key` it runs in SERVER mode —
530
+ * Created by `createNodeWebSocket`. When given a client `key` it runs in server mode —
411
531
  * it writes the `101 Switching Protocols` handshake (`computeWebSocketAccept(key)`) and
412
- * emits `open`; given no key it runs in CLIENT mode (no handshake, frames masked). It
532
+ * emits `open`; given no key it runs in client mode (no handshake, frames masked). It
413
533
  * then listens on the socket's `data`, accumulating bytes in `#buffer` and decoding
414
534
  * every complete frame with {@link parseWebSocketFrame} (slicing `consumed` and
415
- * re-parsing the remainder): a TEXT frame — reassembling continuation fragments across
416
- * `fin: false` frames — decodes to UTF-8 and emits `message`; a PING is auto-answered
417
- * with a PONG and emits `ping`; a PONG emits `pong`; a CLOSE is echoed and ends the
535
+ * re-parsing the remainder): a text frame — reassembling continuation fragments across
536
+ * `fin: false` frames — decodes to UTF-8 and emits `message`; a ping is auto-answered
537
+ * with a pong and emits `ping`; a pong emits `pong`; a close frame is echoed and ends the
418
538
  * socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
419
539
  * frame; `destroy` tears down immediately. It owns a typed `#emitter` by composition, and
420
540
  * the emitter isolates a throwing listener and routes the error to its own `error` handler
@@ -461,8 +581,8 @@ var NodeWebSocket = class {
461
581
  * Creates a WebSocket wrapper over an already-upgraded Duplex socket.
462
582
  *
463
583
  * @remarks
464
- * `key` selects the mode: present runs SERVER mode and writes the `101 Switching
465
- * Protocols` handshake, omitted runs CLIENT mode and masks every outgoing frame.
584
+ * `key` selects the mode: present runs server mode and writes the `101 Switching
585
+ * Protocols` handshake, omitted runs client mode and masks every outgoing frame.
466
586
  * {@link NodeWebSocketOptions} describes every member.
467
587
  *
468
588
  * @param options - The {@link NodeWebSocketOptions} the wrapper is built from
@@ -748,27 +868,25 @@ var NodeWebSocket = class {
748
868
  //#endregion
749
869
  //#region src/server/factories.ts
750
870
  /**
751
- * Creates a server-native WebSocket over a raw upgraded `node:stream` Duplex socket.
871
+ * Creates a server-native WebSocket over a raw upgraded `node:stream` Duplex socket
872
+ * server mode when a `key` is given, client mode otherwise.
752
873
  *
753
874
  * @remarks
754
- * The construction entry point for the {@link NodeWebSocketInterface}. Pass
755
- * the upgraded `socket` plus the client's `Sec-WebSocket-Key` as `key` to run in SERVER
756
- * mode the wrapper writes the `101 Switching Protocols` handshake and sends unmasked
757
- * frames; omit `key` for CLIENT mode (no handshake, masked frames). This is the
758
- * lean-native handle; it speaks only the WebSocket wire protocol — an MCP transport (the
759
- * later chunk) is built ON it. It is the WebSocket counterpart to
760
- * `createSQLiteDatabase` / `createIndexedDBDatabase`.
875
+ * The construction entry point for the {@link NodeWebSocketInterface}. In server mode the
876
+ * wrapper writes the `101 Switching Protocols` handshake and sends unmasked frames; in
877
+ * client mode it writes no handshake and masks every outgoing frame. This is the
878
+ * lean-native handle: it speaks the WebSocket wire protocol and nothing above it, so a
879
+ * message transport is built on it rather than into it.
761
880
  *
762
881
  * @param options - The {@link NodeWebSocketOptions} (`socket`, optional `key` / `head` /
763
882
  * `protocol` / `on`)
764
883
  * @returns A typed {@link NodeWebSocketInterface}
765
884
  * @throws A `WebSocketError` coded `OPTION` when `payload`, `timeout`, `key`, or `protocol` is refused, thrown before the wrapper writes to or assumes ownership of the `socket`
766
885
  *
767
- * @example
886
+ * @example Accept an upgrade and echo messages (server mode)
768
887
  * ```ts
769
- * import { createNodeWebSocket } from '@src/server'
888
+ * import { createNodeWebSocket } from '@orkestrel/websocket'
770
889
  *
771
- * // In a node:http 'upgrade' handler — server mode, identified by the client key:
772
890
  * server.on('upgrade', (request, socket, head) => {
773
891
  * const key = request.headers['sec-websocket-key']
774
892
  * if (typeof key !== 'string') {
@@ -777,10 +895,12 @@ var NodeWebSocket = class {
777
895
  * }
778
896
  * const ws = createNodeWebSocket({
779
897
  * socket,
780
- * key, // present => server mode + 101 handshake
781
- * head,
782
- * on: { message: (text) => ws.send(`echo: ${text}`) },
898
+ * key,
899
+ * head, // any bytes already buffered after the upgrade headers
900
+ * on: { message: (text) => ws.send(`echo: ${text}`) }, // wired before the first frame arrives
783
901
  * })
902
+ * ws.emitter.on('message', (text) => log('echoed', text)) // a second observer of the same event
903
+ * ws.emitter.on('close', (code, reason) => log('closed', code, reason))
784
904
  * })
785
905
  * ```
786
906
  */