@orkestrel/websocket 0.0.4 → 0.0.6

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.
@@ -93,6 +93,61 @@ export declare function encodeWebSocketFrame(opcode: number, payload: Buffer | s
93
93
  */
94
94
  export declare function isCloseCode(code: number): boolean;
95
95
 
96
+ /**
97
+ * Whether the next frame uses the shortest valid RFC 6455 payload-length encoding.
98
+ *
99
+ * @remarks
100
+ * Returns `undefined` until the complete length prefix is buffered. The 16-bit form
101
+ * is canonical only for lengths at least 126; the 64-bit form only for lengths at
102
+ * least 65,536 and with its most-significant bit clear (RFC 6455 §5.2).
103
+ *
104
+ * @param buffer - The accumulation buffer containing the next frame header
105
+ * @returns Its canonicality, or `undefined` while the length prefix is incomplete
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * if (isWebSocketFrameCanonical(buffer) === false) fail(WEBSOCKET_CLOSE_PROTOCOL)
110
+ * ```
111
+ */
112
+ export declare function isWebSocketFrameCanonical(buffer: Buffer): boolean | undefined;
113
+
114
+ /**
115
+ * Whether a value is a canonical RFC 6455 `Sec-WebSocket-Key`.
116
+ *
117
+ * @remarks
118
+ * A valid key is exactly 16 random bytes encoded as 24 characters of base64, ending
119
+ * in `==` (RFC 6455 §4.1). This predicate is suitable at an HTTP upgrade boundary:
120
+ * malformed or non-canonical encodings return `false`; nothing is thrown.
121
+ *
122
+ * @param key - The proposed `Sec-WebSocket-Key` header value
123
+ * @returns `true` when `key` is the canonical base64 encoding of 16 bytes
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const key = request.headers['sec-websocket-key']
128
+ * if (typeof key !== 'string' || !isWebSocketKey(key)) socket.destroy()
129
+ * ```
130
+ */
131
+ export declare function isWebSocketKey(key: string): boolean;
132
+
133
+ /**
134
+ * Whether a value is one valid WebSocket subprotocol token.
135
+ *
136
+ * @remarks
137
+ * Subprotocols use the HTTP `token` grammar. Whitespace, separators, commas, and
138
+ * control characters are rejected, preventing an untrusted value from injecting a
139
+ * second handshake header.
140
+ *
141
+ * @param protocol - The negotiated subprotocol to validate
142
+ * @returns `true` when `protocol` is one non-empty HTTP token
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * if (!isWebSocketProtocol(protocol)) throw new RangeError('invalid protocol')
147
+ * ```
148
+ */
149
+ export declare function isWebSocketProtocol(protocol: string): boolean;
150
+
96
151
  /**
97
152
  * Read the declared payload length off the front of a buffer, without buffering or
98
153
  * reading the payload itself.
@@ -131,8 +186,9 @@ export declare function measureWebSocketFrame(buffer: Buffer): number | undefine
131
186
  * socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
132
187
  * frame; `destroy` tears down immediately. It owns a typed `#emitter` (AGENTS §13) that
133
188
  * isolates a throwing listener and routes the error to its own `error` handler (the `error`
134
- * option) — the socket never crashes. The untyped socket `data` is narrowed to a `Buffer`
135
- * with a guard, never an assertion (AGENTS §14).
189
+ * option) — the socket never crashes. An underlying socket error emits the domain
190
+ * `error` event and terminates the wrapper. The untyped socket `data` is narrowed to a
191
+ * `Buffer` with a guard, never an assertion (AGENTS §14).
136
192
  */
137
193
  export declare class NodeWebSocket implements NodeWebSocketInterface {
138
194
  #private;
@@ -152,18 +208,19 @@ export declare class NodeWebSocket implements NodeWebSocketInterface {
152
208
  * `open` — the handshake completed and the socket is ready. `message` — a text frame
153
209
  * arrived (its decoded UTF-8 string). `close` — the connection ended (its
154
210
  * {@link WebSocketClose} metadata). `error` — the underlying socket faulted (a DOMAIN
155
- * event). `ping` / `pong` — a control frame arrived (a ping is auto-answered with a pong).
211
+ * event and then terminates the wrapper). `ping` / `pong` — a control frame arrived
212
+ * (a ping is auto-answered with a pong).
156
213
  * Listener isolation is the emitter's (AGENTS §13): a listener throw is routed to the
157
214
  * emitter's `error` handler (the `error` option), never onto this map, so a buggy observer
158
215
  * never breaks the socket.
159
216
  */
160
217
  export declare type NodeWebSocketEventMap = {
161
- open: [];
162
- message: [message: string];
163
- close: [code: number | undefined, reason: string | undefined];
164
- error: [error: unknown];
165
- ping: [];
166
- pong: [];
218
+ readonly open: readonly [];
219
+ readonly message: readonly [message: string];
220
+ readonly close: readonly [code: number | undefined, reason: string | undefined];
221
+ readonly error: readonly [error: unknown];
222
+ readonly ping: readonly [];
223
+ readonly pong: readonly [];
167
224
  };
168
225
 
169
226
  /**
@@ -271,6 +328,9 @@ export declare const WEBSOCKET_CLOSE_NORMAL = 1000;
271
328
  /** Protocol-error status code (RFC 6455 §7.4.1) — a framing/state rule was violated. */
272
329
  export declare const WEBSOCKET_CLOSE_PROTOCOL = 1002;
273
330
 
331
+ /** The maximum UTF-8 close-reason length after the two-byte status code. */
332
+ export declare const WEBSOCKET_CLOSE_REASON_MAXLEN: number;
333
+
274
334
  /** The default close-handshake timeout in milliseconds — how long `close()` waits for the peer's echo before tearing the socket down. */
275
335
  export declare const WEBSOCKET_CLOSE_TIMEOUT_MS = 30000;
276
336
 
@@ -305,6 +365,9 @@ export declare const WEBSOCKET_OPCODE_BINARY = 2;
305
365
  /** Close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */
306
366
  export declare const WEBSOCKET_OPCODE_CLOSE = 8;
307
367
 
368
+ /** Continuation frame opcode — the next fragment of an open data message (RFC 6455 §5.4). */
369
+ export declare const WEBSOCKET_OPCODE_CONTINUATION = 0;
370
+
308
371
  /** Ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */
309
372
  export declare const WEBSOCKET_OPCODE_PING = 9;
310
373
 
@@ -93,6 +93,61 @@ export declare function encodeWebSocketFrame(opcode: number, payload: Buffer | s
93
93
  */
94
94
  export declare function isCloseCode(code: number): boolean;
95
95
 
96
+ /**
97
+ * Whether the next frame uses the shortest valid RFC 6455 payload-length encoding.
98
+ *
99
+ * @remarks
100
+ * Returns `undefined` until the complete length prefix is buffered. The 16-bit form
101
+ * is canonical only for lengths at least 126; the 64-bit form only for lengths at
102
+ * least 65,536 and with its most-significant bit clear (RFC 6455 §5.2).
103
+ *
104
+ * @param buffer - The accumulation buffer containing the next frame header
105
+ * @returns Its canonicality, or `undefined` while the length prefix is incomplete
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * if (isWebSocketFrameCanonical(buffer) === false) fail(WEBSOCKET_CLOSE_PROTOCOL)
110
+ * ```
111
+ */
112
+ export declare function isWebSocketFrameCanonical(buffer: Buffer): boolean | undefined;
113
+
114
+ /**
115
+ * Whether a value is a canonical RFC 6455 `Sec-WebSocket-Key`.
116
+ *
117
+ * @remarks
118
+ * A valid key is exactly 16 random bytes encoded as 24 characters of base64, ending
119
+ * in `==` (RFC 6455 §4.1). This predicate is suitable at an HTTP upgrade boundary:
120
+ * malformed or non-canonical encodings return `false`; nothing is thrown.
121
+ *
122
+ * @param key - The proposed `Sec-WebSocket-Key` header value
123
+ * @returns `true` when `key` is the canonical base64 encoding of 16 bytes
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const key = request.headers['sec-websocket-key']
128
+ * if (typeof key !== 'string' || !isWebSocketKey(key)) socket.destroy()
129
+ * ```
130
+ */
131
+ export declare function isWebSocketKey(key: string): boolean;
132
+
133
+ /**
134
+ * Whether a value is one valid WebSocket subprotocol token.
135
+ *
136
+ * @remarks
137
+ * Subprotocols use the HTTP `token` grammar. Whitespace, separators, commas, and
138
+ * control characters are rejected, preventing an untrusted value from injecting a
139
+ * second handshake header.
140
+ *
141
+ * @param protocol - The negotiated subprotocol to validate
142
+ * @returns `true` when `protocol` is one non-empty HTTP token
143
+ *
144
+ * @example
145
+ * ```ts
146
+ * if (!isWebSocketProtocol(protocol)) throw new RangeError('invalid protocol')
147
+ * ```
148
+ */
149
+ export declare function isWebSocketProtocol(protocol: string): boolean;
150
+
96
151
  /**
97
152
  * Read the declared payload length off the front of a buffer, without buffering or
98
153
  * reading the payload itself.
@@ -131,8 +186,9 @@ export declare function measureWebSocketFrame(buffer: Buffer): number | undefine
131
186
  * socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
132
187
  * frame; `destroy` tears down immediately. It owns a typed `#emitter` (AGENTS §13) that
133
188
  * isolates a throwing listener and routes the error to its own `error` handler (the `error`
134
- * option) — the socket never crashes. The untyped socket `data` is narrowed to a `Buffer`
135
- * with a guard, never an assertion (AGENTS §14).
189
+ * option) — the socket never crashes. An underlying socket error emits the domain
190
+ * `error` event and terminates the wrapper. The untyped socket `data` is narrowed to a
191
+ * `Buffer` with a guard, never an assertion (AGENTS §14).
136
192
  */
137
193
  export declare class NodeWebSocket implements NodeWebSocketInterface {
138
194
  #private;
@@ -152,18 +208,19 @@ export declare class NodeWebSocket implements NodeWebSocketInterface {
152
208
  * `open` — the handshake completed and the socket is ready. `message` — a text frame
153
209
  * arrived (its decoded UTF-8 string). `close` — the connection ended (its
154
210
  * {@link WebSocketClose} metadata). `error` — the underlying socket faulted (a DOMAIN
155
- * event). `ping` / `pong` — a control frame arrived (a ping is auto-answered with a pong).
211
+ * event and then terminates the wrapper). `ping` / `pong` — a control frame arrived
212
+ * (a ping is auto-answered with a pong).
156
213
  * Listener isolation is the emitter's (AGENTS §13): a listener throw is routed to the
157
214
  * emitter's `error` handler (the `error` option), never onto this map, so a buggy observer
158
215
  * never breaks the socket.
159
216
  */
160
217
  export declare type NodeWebSocketEventMap = {
161
- open: [];
162
- message: [message: string];
163
- close: [code: number | undefined, reason: string | undefined];
164
- error: [error: unknown];
165
- ping: [];
166
- pong: [];
218
+ readonly open: readonly [];
219
+ readonly message: readonly [message: string];
220
+ readonly close: readonly [code: number | undefined, reason: string | undefined];
221
+ readonly error: readonly [error: unknown];
222
+ readonly ping: readonly [];
223
+ readonly pong: readonly [];
167
224
  };
168
225
 
169
226
  /**
@@ -271,6 +328,9 @@ export declare const WEBSOCKET_CLOSE_NORMAL = 1000;
271
328
  /** Protocol-error status code (RFC 6455 §7.4.1) — a framing/state rule was violated. */
272
329
  export declare const WEBSOCKET_CLOSE_PROTOCOL = 1002;
273
330
 
331
+ /** The maximum UTF-8 close-reason length after the two-byte status code. */
332
+ export declare const WEBSOCKET_CLOSE_REASON_MAXLEN: number;
333
+
274
334
  /** The default close-handshake timeout in milliseconds — how long `close()` waits for the peer's echo before tearing the socket down. */
275
335
  export declare const WEBSOCKET_CLOSE_TIMEOUT_MS = 30000;
276
336
 
@@ -305,6 +365,9 @@ export declare const WEBSOCKET_OPCODE_BINARY = 2;
305
365
  /** Close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */
306
366
  export declare const WEBSOCKET_OPCODE_CLOSE = 8;
307
367
 
368
+ /** Continuation frame opcode — the next fragment of an open data message (RFC 6455 §5.4). */
369
+ export declare const WEBSOCKET_OPCODE_CONTINUATION = 0;
370
+
308
371
  /** Ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */
309
372
  export declare const WEBSOCKET_OPCODE_PING = 9;
310
373
 
@@ -16,6 +16,8 @@ var WEBSOCKET_VERSION = "13";
16
16
  var WEBSOCKET_OPCODE_TEXT = 1;
17
17
  /** Binary frame opcode — a raw byte payload (RFC 6455 §5.6). */
18
18
  var WEBSOCKET_OPCODE_BINARY = 2;
19
+ /** Continuation frame opcode — the next fragment of an open data message (RFC 6455 §5.4). */
20
+ var WEBSOCKET_OPCODE_CONTINUATION = 0;
19
21
  /** Close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */
20
22
  var WEBSOCKET_OPCODE_CLOSE = 8;
21
23
  /** Ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */
@@ -48,6 +50,8 @@ var WEBSOCKET_CLOSE_TIMEOUT_MS = 3e4;
48
50
  var WEBSOCKET_FAIL_TIMEOUT_MS = 1e3;
49
51
  /** The maximum control-frame payload length in bytes (RFC 6455 §5.5). */
50
52
  var WEBSOCKET_CONTROL_MAXLEN = 125;
53
+ /** The maximum UTF-8 close-reason length after the two-byte status code. */
54
+ var WEBSOCKET_CLOSE_REASON_MAXLEN = 123;
51
55
  //#endregion
52
56
  //#region src/server/helpers.ts
53
57
  /**
@@ -65,6 +69,46 @@ function computeWebSocketAccept(key) {
65
69
  return createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
66
70
  }
67
71
  /**
72
+ * Whether a value is a canonical RFC 6455 `Sec-WebSocket-Key`.
73
+ *
74
+ * @remarks
75
+ * A valid key is exactly 16 random bytes encoded as 24 characters of base64, ending
76
+ * in `==` (RFC 6455 §4.1). This predicate is suitable at an HTTP upgrade boundary:
77
+ * malformed or non-canonical encodings return `false`; nothing is thrown.
78
+ *
79
+ * @param key - The proposed `Sec-WebSocket-Key` header value
80
+ * @returns `true` when `key` is the canonical base64 encoding of 16 bytes
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const key = request.headers['sec-websocket-key']
85
+ * if (typeof key !== 'string' || !isWebSocketKey(key)) socket.destroy()
86
+ * ```
87
+ */
88
+ function isWebSocketKey(key) {
89
+ if (!/^[A-Za-z0-9+/]{22}==$/.test(key)) return false;
90
+ return Buffer.from(key, "base64").length === 16;
91
+ }
92
+ /**
93
+ * Whether a value is one valid WebSocket subprotocol token.
94
+ *
95
+ * @remarks
96
+ * Subprotocols use the HTTP `token` grammar. Whitespace, separators, commas, and
97
+ * control characters are rejected, preventing an untrusted value from injecting a
98
+ * second handshake header.
99
+ *
100
+ * @param protocol - The negotiated subprotocol to validate
101
+ * @returns `true` when `protocol` is one non-empty HTTP token
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * if (!isWebSocketProtocol(protocol)) throw new RangeError('invalid protocol')
106
+ * ```
107
+ */
108
+ function isWebSocketProtocol(protocol) {
109
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(protocol);
110
+ }
111
+ /**
68
112
  * Decode a single RFC 6455 frame from the front of a buffer.
69
113
  *
70
114
  * @remarks
@@ -84,8 +128,8 @@ function computeWebSocketAccept(key) {
84
128
  */
85
129
  function parseWebSocketFrame(buffer) {
86
130
  if (buffer.length < 2) return void 0;
87
- const firstByte = buffer[0] ?? 0;
88
- const secondByte = buffer[1] ?? 0;
131
+ const firstByte = buffer.readUInt8(0);
132
+ const secondByte = buffer.readUInt8(1);
89
133
  const fin = (firstByte & 128) !== 0;
90
134
  const rsv = (firstByte & 112) >> 4;
91
135
  const opcode = firstByte & 15;
@@ -112,7 +156,7 @@ function parseWebSocketFrame(buffer) {
112
156
  if (buffer.length < offset + length) return void 0;
113
157
  const payload = Buffer.alloc(length);
114
158
  buffer.copy(payload, 0, offset, offset + length);
115
- if (mask !== void 0) for (let index = 0; index < length; index += 1) payload[index] = (payload[index] ?? 0) ^ (mask[index % 4] ?? 0);
159
+ if (mask !== void 0) for (let index = 0; index < length; index += 1) payload[index] = payload.readUInt8(index) ^ mask.readUInt8(index % 4);
116
160
  return {
117
161
  fin,
118
162
  opcode,
@@ -144,7 +188,7 @@ function parseWebSocketFrame(buffer) {
144
188
  */
145
189
  function measureWebSocketFrame(buffer) {
146
190
  if (buffer.length < 2) return void 0;
147
- let length = (buffer[1] ?? 0) & 127;
191
+ let length = buffer.readUInt8(1) & 127;
148
192
  const offset = 2;
149
193
  if (length === 126) {
150
194
  if (buffer.length < 4) return void 0;
@@ -158,6 +202,36 @@ function measureWebSocketFrame(buffer) {
158
202
  return length;
159
203
  }
160
204
  /**
205
+ * Whether the next frame uses the shortest valid RFC 6455 payload-length encoding.
206
+ *
207
+ * @remarks
208
+ * Returns `undefined` until the complete length prefix is buffered. The 16-bit form
209
+ * is canonical only for lengths at least 126; the 64-bit form only for lengths at
210
+ * least 65,536 and with its most-significant bit clear (RFC 6455 §5.2).
211
+ *
212
+ * @param buffer - The accumulation buffer containing the next frame header
213
+ * @returns Its canonicality, or `undefined` while the length prefix is incomplete
214
+ *
215
+ * @example
216
+ * ```ts
217
+ * if (isWebSocketFrameCanonical(buffer) === false) fail(WEBSOCKET_CLOSE_PROTOCOL)
218
+ * ```
219
+ */
220
+ function isWebSocketFrameCanonical(buffer) {
221
+ if (buffer.length < 2) return void 0;
222
+ const lengthCode = buffer.readUInt8(1) & 127;
223
+ if (lengthCode < 126) return true;
224
+ if (lengthCode === 126) {
225
+ if (buffer.length < 4) return void 0;
226
+ return buffer.readUInt16BE(2) >= 126;
227
+ }
228
+ if (buffer.length < 10) return void 0;
229
+ const high = buffer.readUInt32BE(2);
230
+ const low = buffer.readUInt32BE(6);
231
+ if ((high & 2147483648) !== 0) return false;
232
+ return high > 0 || low >= 65536;
233
+ }
234
+ /**
161
235
  * Decode a byte sequence as strict UTF-8, or signal it is malformed.
162
236
  *
163
237
  * @remarks
@@ -203,6 +277,7 @@ function parseUTF8(bytes) {
203
277
  * ```
204
278
  */
205
279
  function isCloseCode(code) {
280
+ if (!Number.isInteger(code)) return false;
206
281
  if (code >= 1e3 && code <= 1003) return true;
207
282
  if (code >= 1007 && code <= 1014) return true;
208
283
  if (code >= 3e3 && code <= 4999) return true;
@@ -228,6 +303,9 @@ function isCloseCode(code) {
228
303
  * @returns The complete frame as wire bytes
229
304
  */
230
305
  function encodeWebSocketFrame(opcode, payload, options) {
306
+ if (!Number.isInteger(opcode) || opcode < 0 || opcode > 15) throw new RangeError("opcode must be an integer between 0 and 15");
307
+ if (options?.mask !== void 0 && options.mask.length !== 4) throw new RangeError("mask must contain exactly 4 bytes");
308
+ if (options?.mask !== void 0 && options.masked !== true) throw new RangeError("mask requires masked: true");
231
309
  const body = typeof payload === "string" ? Buffer.from(payload, "utf-8") : payload;
232
310
  const length = body.length;
233
311
  const masked = options?.masked === true;
@@ -248,7 +326,7 @@ function encodeWebSocketFrame(opcode, payload, options) {
248
326
  if (mask === void 0) return Buffer.concat([header, body]);
249
327
  mask.copy(header, header.length - 4);
250
328
  const maskedBody = Buffer.alloc(length);
251
- for (let index = 0; index < length; index += 1) maskedBody[index] = (body[index] ?? 0) ^ (mask[index % 4] ?? 0);
329
+ for (let index = 0; index < length; index += 1) maskedBody[index] = body.readUInt8(index) ^ mask.readUInt8(index % 4);
252
330
  return Buffer.concat([header, maskedBody]);
253
331
  }
254
332
  //#endregion
@@ -269,72 +347,71 @@ function encodeWebSocketFrame(opcode, payload, options) {
269
347
  * socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
270
348
  * frame; `destroy` tears down immediately. It owns a typed `#emitter` (AGENTS §13) that
271
349
  * isolates a throwing listener and routes the error to its own `error` handler (the `error`
272
- * option) — the socket never crashes. The untyped socket `data` is narrowed to a `Buffer`
273
- * with a guard, never an assertion (AGENTS §14).
350
+ * option) — the socket never crashes. An underlying socket error emits the domain
351
+ * `error` event and terminates the wrapper. The untyped socket `data` is narrowed to a
352
+ * `Buffer` with a guard, never an assertion (AGENTS §14).
274
353
  */
275
354
  var NodeWebSocket = class {
276
355
  #emitter;
277
356
  #socket;
278
- #protocol;
279
357
  #masked;
280
358
  #payload;
281
359
  #timeout;
282
- #requireMask;
283
360
  #signal;
361
+ #dataListener;
362
+ #closeListener;
363
+ #errorListener;
364
+ #abortListener;
284
365
  #buffer = Buffer.alloc(0);
285
366
  #readyState = 0;
286
- #code = void 0;
287
- #reason = void 0;
367
+ #code;
368
+ #reason;
288
369
  #fragments = [];
289
- #messageOpcode = void 0;
370
+ #messageOpcode;
290
371
  #fragmentBytes = 0;
291
- #closeTimer = void 0;
372
+ #closeTimer;
292
373
  #destroyed = false;
293
374
  #detached = false;
294
- #onData = (chunk) => {
295
- if (this.#readyState === 3) return;
296
- const bytes = this.#bytes(chunk);
297
- if (bytes === void 0) return;
298
- this.#ingest(bytes);
299
- };
300
- #onClose = () => {
301
- this.#finish();
302
- };
303
- #onError = (error) => {
304
- this.#emitter.emit("error", error);
305
- };
306
- #onDetachedError = () => void 0;
307
- #onAbort = () => {
308
- this.destroy();
309
- };
310
375
  constructor(options) {
376
+ const payload = options.payload ?? 104857600;
377
+ if (!Number.isSafeInteger(payload) || payload < 0) throw new RangeError("payload must be a non-negative safe integer");
378
+ const timeout = options.timeout ?? 3e4;
379
+ if (!Number.isSafeInteger(timeout) || timeout < 0) throw new RangeError("timeout must be a non-negative safe integer");
380
+ if (options.key !== void 0 && !isWebSocketKey(options.key)) throw new RangeError("key must be the canonical base64 encoding of 16 bytes");
381
+ if (options.protocol !== void 0 && !isWebSocketProtocol(options.protocol)) throw new RangeError("protocol must be a valid WebSocket subprotocol token");
382
+ if (options.protocol !== void 0 && options.key === void 0) throw new RangeError("protocol requires a server key");
311
383
  this.#emitter = new Emitter({
312
- on: options.on,
313
- error: options.error
384
+ ...options.on === void 0 ? {} : { on: options.on },
385
+ ...options.error === void 0 ? {} : { error: options.error }
314
386
  });
315
387
  this.#socket = options.socket;
316
- this.#protocol = options.protocol;
317
388
  this.#masked = options.key === void 0;
318
- this.#payload = options.payload ?? 104857600;
319
- this.#timeout = options.timeout ?? 3e4;
320
- this.#requireMask = !this.#masked;
389
+ this.#payload = payload;
390
+ this.#timeout = timeout;
321
391
  this.#signal = options.signal;
392
+ this.#dataListener = this.#handleData.bind(this);
393
+ this.#closeListener = this.#finish.bind(this);
394
+ this.#errorListener = this.#handleError.bind(this);
395
+ this.#abortListener = this.destroy.bind(this);
322
396
  if (options.key !== void 0) {
323
- const protocol = this.#protocol === void 0 ? "" : `Sec-WebSocket-Protocol: ${this.#protocol}\r\n`;
324
- this.#socket.write(`HTTP/1.1 101 Switching Protocols\r
325
- Upgrade: websocket\r
326
- Connection: Upgrade\r
327
- Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "\r\n");
397
+ const headers = [
398
+ "HTTP/1.1 101 Switching Protocols",
399
+ "Upgrade: websocket",
400
+ "Connection: Upgrade",
401
+ `Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}`
402
+ ];
403
+ if (options.protocol !== void 0) headers.push(`Sec-WebSocket-Protocol: ${options.protocol}`);
404
+ this.#socket.write(`${headers.join("\r\n")}\r\n\r\n`);
328
405
  }
329
406
  this.#readyState = 1;
330
- this.#socket.on("data", this.#onData);
331
- this.#socket.on("close", this.#onClose);
332
- this.#socket.on("error", this.#onError);
407
+ this.#socket.on("data", this.#dataListener);
408
+ this.#socket.on("close", this.#closeListener);
409
+ this.#socket.on("error", this.#errorListener);
333
410
  this.#emitter.emit("open");
334
411
  const head = options.head;
335
412
  if (head !== void 0 && head.length > 0) this.#ingest(head);
336
413
  if (this.#readyState !== 3) if (this.#signal?.aborted === true) this.destroy();
337
- else this.#signal?.addEventListener("abort", this.#onAbort, { once: true });
414
+ else this.#signal?.addEventListener("abort", this.#abortListener, { once: true });
338
415
  }
339
416
  get emitter() {
340
417
  return this.#emitter;
@@ -354,7 +431,7 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
354
431
  close(code, reason) {
355
432
  if (this.#readyState === 2 || this.#readyState === 3) return;
356
433
  if (code !== void 0 && !isCloseCode(code)) throw new RangeError("invalid close code");
357
- if (reason !== void 0 && Buffer.byteLength(reason, "utf-8") > 123) throw new RangeError("close reason exceeds 123 bytes");
434
+ if (reason !== void 0 && Buffer.byteLength(reason, "utf-8") > 123) throw new RangeError(`close reason exceeds 123 bytes`);
358
435
  this.#readyState = 2;
359
436
  this.#code = code ?? 1e3;
360
437
  this.#reason = reason === void 0 || reason.length === 0 ? void 0 : reason;
@@ -367,7 +444,7 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
367
444
  if (this.#destroyed) return;
368
445
  this.#destroyed = true;
369
446
  this.#detach();
370
- this.#signal?.removeEventListener("abort", this.#onAbort);
447
+ this.#signal?.removeEventListener("abort", this.#abortListener);
371
448
  clearTimeout(this.#closeTimer);
372
449
  this.#closeTimer = void 0;
373
450
  if (!this.#socket.destroyed) this.#socket.destroy();
@@ -376,6 +453,15 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
376
453
  }
377
454
  #drain() {
378
455
  for (;;) {
456
+ if (isWebSocketFrameCanonical(this.#buffer) === false) {
457
+ this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
458
+ return;
459
+ }
460
+ const declared = measureWebSocketFrame(this.#buffer);
461
+ if (declared !== void 0 && declared > this.#payload) {
462
+ this.#fail(WEBSOCKET_CLOSE_TOOBIG);
463
+ return;
464
+ }
379
465
  const frame = parseWebSocketFrame(this.#buffer);
380
466
  if (frame === void 0) return;
381
467
  this.#buffer = this.#buffer.subarray(frame.consumed);
@@ -388,7 +474,7 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
388
474
  this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
389
475
  return;
390
476
  }
391
- if (masked !== this.#requireMask) {
477
+ if (masked === this.#masked) {
392
478
  this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
393
479
  return;
394
480
  }
@@ -479,10 +565,10 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
479
565
  #detach() {
480
566
  if (this.#detached) return;
481
567
  this.#detached = true;
482
- this.#socket.off("data", this.#onData);
483
- this.#socket.off("close", this.#onClose);
484
- this.#socket.off("error", this.#onError);
485
- this.#socket.on("error", this.#onDetachedError);
568
+ this.#socket.off("data", this.#dataListener);
569
+ this.#socket.off("close", this.#closeListener);
570
+ this.#socket.off("error", this.#errorListener);
571
+ this.#socket.on("error", () => void 0);
486
572
  }
487
573
  #write(opcode, payload) {
488
574
  if (this.#socket.destroyed) return;
@@ -511,7 +597,12 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
511
597
  this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
512
598
  return false;
513
599
  }
514
- const reason = payload.length > 2 ? parseUTF8(payload.subarray(2)) : "";
600
+ if (payload.length === 2) {
601
+ this.#code = code;
602
+ this.#reason = void 0;
603
+ return true;
604
+ }
605
+ const reason = parseUTF8(payload.subarray(2));
515
606
  if (reason === void 0) {
516
607
  this.#fail(WEBSOCKET_CLOSE_INVALID);
517
608
  return false;
@@ -525,19 +616,24 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
525
616
  this.#detach();
526
617
  clearTimeout(this.#closeTimer);
527
618
  this.#closeTimer = void 0;
528
- this.#signal?.removeEventListener("abort", this.#onAbort);
619
+ this.#signal?.removeEventListener("abort", this.#abortListener);
529
620
  this.#readyState = 3;
530
621
  this.#emitter.emit("close", this.#code, this.#reason);
531
622
  }
532
623
  #ingest(bytes) {
533
624
  this.#buffer = Buffer.concat([this.#buffer, bytes]);
534
- const declared = measureWebSocketFrame(this.#buffer);
535
- if (declared !== void 0 && declared > this.#payload) {
536
- this.#fail(WEBSOCKET_CLOSE_TOOBIG);
537
- return;
538
- }
539
625
  this.#drain();
540
626
  }
627
+ #handleData(chunk) {
628
+ if (this.#readyState === 3) return;
629
+ const bytes = this.#bytes(chunk);
630
+ if (bytes === void 0) return;
631
+ this.#ingest(bytes);
632
+ }
633
+ #handleError(error) {
634
+ this.#emitter.emit("error", error);
635
+ this.destroy();
636
+ }
541
637
  #bytes(chunk) {
542
638
  if (Buffer.isBuffer(chunk)) return chunk;
543
639
  if (typeof chunk === "string") return Buffer.from(chunk, "utf-8");
@@ -580,6 +676,6 @@ function createNodeWebSocket(options) {
580
676
  return new NodeWebSocket(options);
581
677
  }
582
678
  //#endregion
583
- export { NodeWebSocket, WEBSOCKET_CLOSE_INVALID, WEBSOCKET_CLOSE_NORMAL, WEBSOCKET_CLOSE_PROTOCOL, WEBSOCKET_CLOSE_TIMEOUT_MS, WEBSOCKET_CLOSE_TOOBIG, WEBSOCKET_CLOSE_UNSUPPORTED, WEBSOCKET_CONTROL_MAXLEN, WEBSOCKET_FAIL_TIMEOUT_MS, WEBSOCKET_GUID, WEBSOCKET_MAX_PAYLOAD, WEBSOCKET_OPCODE_BINARY, WEBSOCKET_OPCODE_CLOSE, WEBSOCKET_OPCODE_PING, WEBSOCKET_OPCODE_PONG, WEBSOCKET_OPCODE_TEXT, WEBSOCKET_READY_CLOSED, WEBSOCKET_READY_CLOSING, WEBSOCKET_READY_CONNECTING, WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket, encodeWebSocketFrame, isCloseCode, measureWebSocketFrame, parseUTF8, parseWebSocketFrame };
679
+ export { NodeWebSocket, WEBSOCKET_CLOSE_INVALID, WEBSOCKET_CLOSE_NORMAL, WEBSOCKET_CLOSE_PROTOCOL, WEBSOCKET_CLOSE_REASON_MAXLEN, WEBSOCKET_CLOSE_TIMEOUT_MS, WEBSOCKET_CLOSE_TOOBIG, WEBSOCKET_CLOSE_UNSUPPORTED, WEBSOCKET_CONTROL_MAXLEN, WEBSOCKET_FAIL_TIMEOUT_MS, WEBSOCKET_GUID, WEBSOCKET_MAX_PAYLOAD, WEBSOCKET_OPCODE_BINARY, WEBSOCKET_OPCODE_CLOSE, WEBSOCKET_OPCODE_CONTINUATION, WEBSOCKET_OPCODE_PING, WEBSOCKET_OPCODE_PONG, WEBSOCKET_OPCODE_TEXT, WEBSOCKET_READY_CLOSED, WEBSOCKET_READY_CLOSING, WEBSOCKET_READY_CONNECTING, WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket, encodeWebSocketFrame, isCloseCode, isWebSocketFrameCanonical, isWebSocketKey, isWebSocketProtocol, measureWebSocketFrame, parseUTF8, parseWebSocketFrame };
584
680
 
585
681
  //# sourceMappingURL=index.js.map