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