stealth-fetch-plus 0.1.1

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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +182 -0
  3. package/dist/client.d.ts +94 -0
  4. package/dist/client.js +969 -0
  5. package/dist/compat/web.d.ts +15 -0
  6. package/dist/compat/web.js +31 -0
  7. package/dist/connection-pool.d.ts +39 -0
  8. package/dist/connection-pool.js +84 -0
  9. package/dist/dns-cache.d.ts +25 -0
  10. package/dist/dns-cache.js +44 -0
  11. package/dist/http1/chunked.d.ts +35 -0
  12. package/dist/http1/chunked.js +87 -0
  13. package/dist/http1/client.d.ts +28 -0
  14. package/dist/http1/client.js +289 -0
  15. package/dist/http1/parser.d.ts +29 -0
  16. package/dist/http1/parser.js +78 -0
  17. package/dist/http2/client.d.ts +64 -0
  18. package/dist/http2/client.js +97 -0
  19. package/dist/http2/connection.d.ts +125 -0
  20. package/dist/http2/connection.js +666 -0
  21. package/dist/http2/constants.d.ts +72 -0
  22. package/dist/http2/constants.js +74 -0
  23. package/dist/http2/flow-control.d.ts +32 -0
  24. package/dist/http2/flow-control.js +76 -0
  25. package/dist/http2/framer.d.ts +47 -0
  26. package/dist/http2/framer.js +133 -0
  27. package/dist/http2/hpack.d.ts +54 -0
  28. package/dist/http2/hpack.js +186 -0
  29. package/dist/http2/parser.d.ts +35 -0
  30. package/dist/http2/parser.js +72 -0
  31. package/dist/http2/stream.d.ts +72 -0
  32. package/dist/http2/stream.js +252 -0
  33. package/dist/index.d.ts +18 -0
  34. package/dist/index.js +33 -0
  35. package/dist/protocol-cache.d.ts +14 -0
  36. package/dist/protocol-cache.js +29 -0
  37. package/dist/socket/adapter.d.ts +59 -0
  38. package/dist/socket/adapter.js +145 -0
  39. package/dist/socket/nat64.d.ts +69 -0
  40. package/dist/socket/nat64.js +196 -0
  41. package/dist/socket/tls.d.ts +28 -0
  42. package/dist/socket/tls.js +33 -0
  43. package/dist/socket/wasm-pkg/wasm_tls.d.ts +107 -0
  44. package/dist/socket/wasm-pkg/wasm_tls.js +568 -0
  45. package/dist/socket/wasm-pkg/wasm_tls_bg.wasm +0 -0
  46. package/dist/socket/wasm-pkg/wasm_tls_bg.wasm.d.ts +20 -0
  47. package/dist/socket/wasm-tls-adapter.d.ts +39 -0
  48. package/dist/socket/wasm-tls-adapter.js +97 -0
  49. package/dist/socket/wasm-tls-bridge.d.ts +30 -0
  50. package/dist/socket/wasm-tls-bridge.js +160 -0
  51. package/dist/utils/headers.d.ts +21 -0
  52. package/dist/utils/headers.js +36 -0
  53. package/dist/utils/url.d.ts +16 -0
  54. package/dist/utils/url.js +12 -0
  55. package/package.json +87 -0
@@ -0,0 +1,666 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { EventEmitter } from "node:events";
3
+ import {
4
+ FrameType,
5
+ FrameFlags,
6
+ SettingsId,
7
+ ErrorCode,
8
+ CONNECTION_PREFACE,
9
+ DEFAULT_INITIAL_WINDOW_SIZE,
10
+ DEFAULT_MAX_FRAME_SIZE,
11
+ DEFAULT_MAX_HEADER_LIST_SIZE,
12
+ OPTIMIZED_MAX_FRAME_SIZE,
13
+ OPTIMIZED_HEADER_TABLE_SIZE,
14
+ OPTIMIZED_STREAM_WINDOW_SIZE,
15
+ OPTIMIZED_CONNECTION_WINDOW_SIZE
16
+ } from "./constants.js";
17
+ import {
18
+ encodeSettings,
19
+ encodeWindowUpdate,
20
+ encodePing,
21
+ encodeGoaway,
22
+ encodeHeaders,
23
+ encodeData,
24
+ encodeRstStream,
25
+ encodeContinuation
26
+ } from "./framer.js";
27
+ import { FrameParser } from "./parser.js";
28
+ import { HpackEncoder, HpackDecoder } from "./hpack.js";
29
+ import { FlowControlWindow } from "./flow-control.js";
30
+ import { Http2Stream } from "./stream.js";
31
+ class Http2Connection extends EventEmitter {
32
+ socket;
33
+ parser;
34
+ hpackEncoder;
35
+ hpackDecoder;
36
+ // Connection state
37
+ remoteSettings = /* @__PURE__ */ new Map();
38
+ localSettings = /* @__PURE__ */ new Map();
39
+ nextStreamId = 1;
40
+ // client uses odd IDs
41
+ streams = /* @__PURE__ */ new Map();
42
+ connectionSendWindow;
43
+ connectionRecvWindowSize;
44
+ connectionRecvWindowConsumed = 0;
45
+ goawayReceived = false;
46
+ goawaySent = false;
47
+ initialized = false;
48
+ closed = false;
49
+ lastRemoteInitialWindowSize = DEFAULT_INITIAL_WINDOW_SIZE;
50
+ // Write coalescing
51
+ pendingWrites = [];
52
+ flushScheduled = false;
53
+ flushPromiseResolvers = [];
54
+ // Settings exchange
55
+ settingsAckResolve = null;
56
+ remoteSettingsResolve = null;
57
+ readyPromise = null;
58
+ settingsTimeout;
59
+ // Continuation state
60
+ continuationStreamId = null;
61
+ continuationBuffer = [];
62
+ continuationBufferSize = 0;
63
+ continuationEndStream = false;
64
+ constructor(socket, options = {}) {
65
+ super();
66
+ this.socket = socket;
67
+ this.parser = new FrameParser(OPTIMIZED_MAX_FRAME_SIZE);
68
+ this.connectionSendWindow = new FlowControlWindow(DEFAULT_INITIAL_WINDOW_SIZE);
69
+ this.connectionRecvWindowSize = options.connectionWindowSize ?? OPTIMIZED_CONNECTION_WINDOW_SIZE;
70
+ this.settingsTimeout = options.settingsTimeout ?? 5e3;
71
+ const tableSize = options.headerTableSize ?? OPTIMIZED_HEADER_TABLE_SIZE;
72
+ this.hpackEncoder = new HpackEncoder(tableSize);
73
+ this.hpackDecoder = new HpackDecoder(tableSize);
74
+ this.localSettings.set(SettingsId.ENABLE_PUSH, 0);
75
+ this.localSettings.set(
76
+ SettingsId.INITIAL_WINDOW_SIZE,
77
+ options.initialWindowSize ?? OPTIMIZED_STREAM_WINDOW_SIZE
78
+ );
79
+ this.localSettings.set(SettingsId.MAX_FRAME_SIZE, OPTIMIZED_MAX_FRAME_SIZE);
80
+ this.localSettings.set(SettingsId.HEADER_TABLE_SIZE, tableSize);
81
+ this.parser.on("frame", (frame) => this.handleFrame(frame));
82
+ this.parser.on("error", (err) => this.handleError(err));
83
+ this.socket.on("data", (chunk) => {
84
+ this.parser.feed(chunk);
85
+ });
86
+ this.socket.on("error", (err) => this.handleError(err));
87
+ this.socket.on("close", () => this.handleSocketClose());
88
+ }
89
+ /**
90
+ * Initialize the HTTP/2 connection.
91
+ * Sends connection preface + SETTINGS, waits for peer SETTINGS + ACK.
92
+ */
93
+ async initialize(timeout = 5e3) {
94
+ await this.startInitialize();
95
+ await this.waitForReady(timeout);
96
+ }
97
+ /**
98
+ * Non-blocking initialization: sends connection preface + SETTINGS
99
+ * immediately without waiting for the peer's response.
100
+ * Call waitForReady() before sending the first request.
101
+ */
102
+ async startInitialize() {
103
+ await this.writeRaw(CONNECTION_PREFACE);
104
+ const settings = [];
105
+ for (const [id, value] of this.localSettings) {
106
+ settings.push([id, value]);
107
+ }
108
+ const initFrames = [encodeSettings(settings)];
109
+ const connectionWindowDelta = this.connectionRecvWindowSize - DEFAULT_INITIAL_WINDOW_SIZE;
110
+ if (connectionWindowDelta > 0) {
111
+ initFrames.push(encodeWindowUpdate(0, connectionWindowDelta));
112
+ }
113
+ await this.writeMulti(initFrames);
114
+ const remoteSettingsPromise = new Promise((resolve) => {
115
+ this.remoteSettingsResolve = resolve;
116
+ });
117
+ const settingsAckPromise = new Promise((resolve) => {
118
+ this.settingsAckResolve = resolve;
119
+ });
120
+ this.readyPromise = Promise.all([remoteSettingsPromise, settingsAckPromise]).then(() => {
121
+ });
122
+ }
123
+ /**
124
+ * Wait for the peer's SETTINGS + ACK to complete the handshake.
125
+ * Must be called after startInitialize().
126
+ */
127
+ async waitForReady(timeout = 5e3) {
128
+ if (this.initialized) return;
129
+ if (!this.readyPromise) {
130
+ throw new Error("startInitialize() must be called first");
131
+ }
132
+ const timeoutPromise = new Promise((_, reject) => {
133
+ setTimeout(() => reject(new Error("HTTP/2 SETTINGS exchange timeout")), timeout);
134
+ });
135
+ await Promise.race([this.readyPromise, timeoutPromise]);
136
+ this.initialized = true;
137
+ }
138
+ /**
139
+ * Create a new HTTP/2 stream for a request.
140
+ */
141
+ createStream() {
142
+ if (this.goawayReceived || this.closed) {
143
+ throw new Error("Connection is closed or received GOAWAY");
144
+ }
145
+ if (this.nextStreamId > 2147483647) {
146
+ throw new Error("Stream ID space exhausted");
147
+ }
148
+ const maxStreams = this.maxConcurrentStreams;
149
+ if (this.streams.size >= maxStreams) {
150
+ throw new Error(`MAX_CONCURRENT_STREAMS limit reached (${maxStreams})`);
151
+ }
152
+ const streamId = this.nextStreamId;
153
+ this.nextStreamId += 2;
154
+ const initialSendWindowSize = this.remoteSettings.get(SettingsId.INITIAL_WINDOW_SIZE) ?? DEFAULT_INITIAL_WINDOW_SIZE;
155
+ const recvWindowSize = this.localSettings.get(SettingsId.INITIAL_WINDOW_SIZE) ?? OPTIMIZED_STREAM_WINDOW_SIZE;
156
+ const stream = new Http2Stream(streamId, initialSendWindowSize, recvWindowSize);
157
+ this.streams.set(streamId, stream);
158
+ stream.setOnBodyCancel((id) => {
159
+ if (this.streams.has(id) && !this.closed) {
160
+ this.writeRaw(encodeRstStream(id, ErrorCode.CANCEL)).catch(() => {
161
+ });
162
+ stream.handleRstStream(ErrorCode.CANCEL);
163
+ }
164
+ });
165
+ stream.setOnSendRst((id, code) => {
166
+ if (this.streams.has(id) && !this.closed) {
167
+ this.writeRaw(encodeRstStream(id, code)).catch(() => {
168
+ });
169
+ }
170
+ });
171
+ stream.on("close", () => {
172
+ this.streams.delete(streamId);
173
+ });
174
+ return stream;
175
+ }
176
+ /**
177
+ * Send HEADERS frame for a stream.
178
+ * Handles CONTINUATION if header block exceeds max frame size.
179
+ * Auto-waits for SETTINGS exchange if not yet complete.
180
+ */
181
+ async sendHeaders(stream, headers, endStream) {
182
+ if (stream.state === "half-closed-local" || stream.state === "closed") {
183
+ throw new Error(`Cannot send HEADERS on stream in state: ${stream.state}`);
184
+ }
185
+ const [headerBlock] = await Promise.all([
186
+ this.hpackEncoder.encode(headers),
187
+ this.initialized ? Promise.resolve() : this.waitForReady(this.settingsTimeout)
188
+ ]);
189
+ const maxPayload = this.remoteSettings.get(SettingsId.MAX_FRAME_SIZE) ?? DEFAULT_MAX_FRAME_SIZE;
190
+ if (headerBlock.length <= maxPayload) {
191
+ await this.writeRaw(encodeHeaders(stream.id, headerBlock, endStream, true));
192
+ } else {
193
+ const frames = [];
194
+ const firstChunk = headerBlock.subarray(0, maxPayload);
195
+ frames.push(encodeHeaders(stream.id, firstChunk, endStream, false));
196
+ let offset = maxPayload;
197
+ while (offset < headerBlock.length) {
198
+ const remaining = headerBlock.length - offset;
199
+ const chunkSize = Math.min(remaining, maxPayload);
200
+ const chunk = headerBlock.subarray(offset, offset + chunkSize);
201
+ const endHeaders = offset + chunkSize >= headerBlock.length;
202
+ frames.push(encodeContinuation(stream.id, chunk, endHeaders));
203
+ offset += chunkSize;
204
+ }
205
+ await this.writeMulti(frames);
206
+ }
207
+ stream.open();
208
+ if (endStream) {
209
+ stream.halfCloseLocal();
210
+ }
211
+ }
212
+ /**
213
+ * Send DATA frame(s) for a stream, respecting flow control.
214
+ * Accepts Buffer (buffered) or ReadableStream (streaming).
215
+ */
216
+ async sendData(stream, data, endStream) {
217
+ if (stream.state === "half-closed-local" || stream.state === "closed") {
218
+ throw new Error(`Cannot send DATA on stream in state: ${stream.state}`);
219
+ }
220
+ if (data instanceof ReadableStream) {
221
+ return this.sendStreamData(stream, data, endStream);
222
+ }
223
+ return this.sendBufferData(stream, data, endStream);
224
+ }
225
+ async sendBufferData(stream, data, endStream) {
226
+ const maxPayload = this.remoteSettings.get(SettingsId.MAX_FRAME_SIZE) ?? DEFAULT_MAX_FRAME_SIZE;
227
+ let offset = 0;
228
+ while (offset < data.length) {
229
+ const remaining = data.length - offset;
230
+ const chunkSize = Math.min(remaining, maxPayload);
231
+ try {
232
+ await this.connectionSendWindow.consume(chunkSize);
233
+ await stream.sendWindow.consume(chunkSize);
234
+ } catch (err) {
235
+ if (err.message?.includes("cancelled")) return;
236
+ throw err;
237
+ }
238
+ const chunk = data.subarray(offset, offset + chunkSize);
239
+ const isLast = offset + chunkSize >= data.length;
240
+ await this.writeRaw(encodeData(stream.id, chunk, isLast && endStream));
241
+ offset += chunkSize;
242
+ }
243
+ if (data.length === 0 && endStream) {
244
+ await this.writeRaw(encodeData(stream.id, Buffer.alloc(0), true));
245
+ }
246
+ if (endStream) {
247
+ stream.halfCloseLocal();
248
+ }
249
+ }
250
+ /** Stream a ReadableStream body as DATA frames with flow control (pull model). */
251
+ async sendStreamData(stream, body, endStream) {
252
+ const maxPayload = this.remoteSettings.get(SettingsId.MAX_FRAME_SIZE) ?? DEFAULT_MAX_FRAME_SIZE;
253
+ const reader = body.getReader();
254
+ try {
255
+ while (true) {
256
+ const { done, value } = await reader.read();
257
+ if (done) break;
258
+ const buf = Buffer.from(value);
259
+ let offset = 0;
260
+ while (offset < buf.length) {
261
+ const size = Math.min(buf.length - offset, maxPayload);
262
+ try {
263
+ await this.connectionSendWindow.consume(size);
264
+ await stream.sendWindow.consume(size);
265
+ } catch (err) {
266
+ if (err.message?.includes("cancelled")) {
267
+ reader.cancel().catch(() => {
268
+ });
269
+ return;
270
+ }
271
+ throw err;
272
+ }
273
+ const slice = buf.subarray(offset, offset + size);
274
+ await this.writeRaw(encodeData(stream.id, slice, false));
275
+ offset += size;
276
+ }
277
+ }
278
+ } catch (err) {
279
+ reader.cancel(err).catch(() => {
280
+ });
281
+ throw err;
282
+ } finally {
283
+ reader.releaseLock();
284
+ }
285
+ if (endStream) {
286
+ await this.writeRaw(encodeData(stream.id, Buffer.alloc(0), true));
287
+ stream.halfCloseLocal();
288
+ }
289
+ }
290
+ /**
291
+ * Gracefully close the connection.
292
+ */
293
+ async close() {
294
+ if (this.closed) return;
295
+ this.closed = true;
296
+ const lastStreamId = Math.max(0, this.nextStreamId - 2);
297
+ await this.writeRaw(encodeGoaway(lastStreamId, ErrorCode.NO_ERROR)).catch(() => {
298
+ });
299
+ for (const stream of this.streams.values()) {
300
+ stream.handleRstStream(ErrorCode.CANCEL);
301
+ }
302
+ this.streams.clear();
303
+ this.connectionSendWindow.cancel();
304
+ this.socket.end();
305
+ }
306
+ // --- Frame dispatch ---
307
+ handleFrame(frame) {
308
+ if (this.continuationStreamId !== null) {
309
+ if (frame.type !== FrameType.CONTINUATION || frame.streamId !== this.continuationStreamId) {
310
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
311
+ return;
312
+ }
313
+ this.continuationBufferSize += frame.payload.length;
314
+ if (this.continuationBufferSize > DEFAULT_MAX_HEADER_LIST_SIZE) {
315
+ this.continuationStreamId = null;
316
+ this.continuationBuffer = [];
317
+ this.continuationBufferSize = 0;
318
+ this.sendGoawayAndClose(ErrorCode.ENHANCE_YOUR_CALM);
319
+ return;
320
+ }
321
+ this.continuationBuffer.push(frame.payload);
322
+ if (frame.flags & FrameFlags.END_HEADERS) {
323
+ const fullBlock = Buffer.concat(this.continuationBuffer);
324
+ const streamId = this.continuationStreamId;
325
+ const endStream = this.continuationEndStream;
326
+ this.continuationStreamId = null;
327
+ this.continuationBuffer = [];
328
+ this.continuationBufferSize = 0;
329
+ this.continuationEndStream = false;
330
+ this.processHeaderBlock(streamId, fullBlock, endStream);
331
+ }
332
+ return;
333
+ }
334
+ switch (frame.type) {
335
+ case FrameType.DATA:
336
+ this.handleDataFrame(frame);
337
+ break;
338
+ case FrameType.HEADERS:
339
+ this.handleHeadersFrame(frame);
340
+ break;
341
+ case FrameType.SETTINGS:
342
+ this.handleSettingsFrame(frame);
343
+ break;
344
+ case FrameType.WINDOW_UPDATE:
345
+ this.handleWindowUpdateFrame(frame);
346
+ break;
347
+ case FrameType.PING:
348
+ this.handlePingFrame(frame);
349
+ break;
350
+ case FrameType.GOAWAY:
351
+ this.handleGoawayFrame(frame);
352
+ break;
353
+ case FrameType.RST_STREAM:
354
+ this.handleRstStreamFrame(frame);
355
+ break;
356
+ case FrameType.PUSH_PROMISE:
357
+ this.writeRaw(encodeGoaway(0, ErrorCode.PROTOCOL_ERROR)).catch(() => {
358
+ });
359
+ break;
360
+ default:
361
+ break;
362
+ }
363
+ }
364
+ handleDataFrame(frame) {
365
+ const stream = this.streams.get(frame.streamId);
366
+ if (!stream) return;
367
+ if (stream.state === "half-closed-remote" || stream.state === "closed") {
368
+ this.writeRaw(encodeRstStream(frame.streamId, ErrorCode.STREAM_CLOSED)).catch(() => {
369
+ });
370
+ return;
371
+ }
372
+ const endStream = !!(frame.flags & FrameFlags.END_STREAM);
373
+ let payload = frame.payload;
374
+ if (frame.flags & FrameFlags.PADDED) {
375
+ if (payload.length < 1) {
376
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
377
+ return;
378
+ }
379
+ const padLength = payload[0];
380
+ if (padLength >= payload.length) {
381
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
382
+ return;
383
+ }
384
+ payload = payload.subarray(1, payload.length - padLength);
385
+ }
386
+ stream.handleData(payload, endStream);
387
+ if (frame.payload.length > 0) {
388
+ this.connectionRecvWindowConsumed += frame.payload.length;
389
+ stream.recvWindowConsumed += frame.payload.length;
390
+ const updates = [];
391
+ if (this.connectionRecvWindowConsumed >= this.connectionRecvWindowSize >>> 1) {
392
+ updates.push(encodeWindowUpdate(0, this.connectionRecvWindowConsumed));
393
+ this.connectionRecvWindowConsumed = 0;
394
+ }
395
+ if (!endStream && stream.recvWindowConsumed >= stream.recvWindowSize >>> 1) {
396
+ updates.push(encodeWindowUpdate(frame.streamId, stream.recvWindowConsumed));
397
+ stream.recvWindowConsumed = 0;
398
+ }
399
+ if (updates.length > 0) {
400
+ this.writeMulti(updates).catch(() => {
401
+ });
402
+ }
403
+ }
404
+ }
405
+ handleHeadersFrame(frame) {
406
+ const endHeaders = !!(frame.flags & FrameFlags.END_HEADERS);
407
+ const endStream = !!(frame.flags & FrameFlags.END_STREAM);
408
+ let payload = frame.payload;
409
+ let offset = 0;
410
+ if (frame.flags & FrameFlags.PADDED) {
411
+ if (payload.length < 1) {
412
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
413
+ return;
414
+ }
415
+ const padLength = payload[0];
416
+ offset = 1;
417
+ if (padLength > payload.length - offset) {
418
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
419
+ return;
420
+ }
421
+ payload = payload.subarray(offset, payload.length - padLength);
422
+ offset = 0;
423
+ }
424
+ if (frame.flags & FrameFlags.PRIORITY) {
425
+ if (payload.length < 5) {
426
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
427
+ return;
428
+ }
429
+ payload = payload.subarray(5);
430
+ }
431
+ if (endHeaders) {
432
+ this.processHeaderBlock(frame.streamId, payload, endStream);
433
+ } else {
434
+ this.continuationStreamId = frame.streamId;
435
+ this.continuationBuffer = [payload];
436
+ this.continuationBufferSize = payload.length;
437
+ this.continuationEndStream = endStream;
438
+ }
439
+ }
440
+ async processHeaderBlock(streamId, headerBlock, endStream) {
441
+ const stream = this.streams.get(streamId);
442
+ if (!stream) return;
443
+ if (stream.state === "half-closed-remote" || stream.state === "closed") {
444
+ this.writeRaw(encodeRstStream(streamId, ErrorCode.STREAM_CLOSED)).catch(() => {
445
+ });
446
+ return;
447
+ }
448
+ try {
449
+ const headers = await this.hpackDecoder.decode(headerBlock);
450
+ stream.handleHeaders(headers, endStream);
451
+ } catch {
452
+ this.sendGoawayAndClose(ErrorCode.COMPRESSION_ERROR);
453
+ }
454
+ }
455
+ handleSettingsFrame(frame) {
456
+ if (frame.streamId !== 0) {
457
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
458
+ return;
459
+ }
460
+ if (frame.flags & FrameFlags.ACK) {
461
+ if (frame.payload.length !== 0) {
462
+ this.sendGoawayAndClose(ErrorCode.FRAME_SIZE_ERROR);
463
+ return;
464
+ }
465
+ if (this.settingsAckResolve) {
466
+ this.settingsAckResolve();
467
+ this.settingsAckResolve = null;
468
+ }
469
+ return;
470
+ }
471
+ const payload = frame.payload;
472
+ if (payload.length % 6 !== 0) {
473
+ this.sendGoawayAndClose(ErrorCode.FRAME_SIZE_ERROR);
474
+ return;
475
+ }
476
+ for (let i = 0; i + 5 < payload.length; i += 6) {
477
+ const id = payload.readUInt16BE(i);
478
+ const value = payload.readUInt32BE(i + 2);
479
+ if (id === SettingsId.INITIAL_WINDOW_SIZE && value > 2147483647) {
480
+ this.sendGoawayAndClose(ErrorCode.FLOW_CONTROL_ERROR);
481
+ return;
482
+ }
483
+ if (id === SettingsId.MAX_FRAME_SIZE && (value < 16384 || value > 16777215)) {
484
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
485
+ return;
486
+ }
487
+ if (id === SettingsId.ENABLE_PUSH && value > 1) {
488
+ this.sendGoawayAndClose(ErrorCode.PROTOCOL_ERROR);
489
+ return;
490
+ }
491
+ this.remoteSettings.set(id, value);
492
+ if (id === SettingsId.INITIAL_WINDOW_SIZE) {
493
+ const oldSize = this.lastRemoteInitialWindowSize;
494
+ for (const stream of this.streams.values()) {
495
+ stream.sendWindow.reset(value, oldSize);
496
+ }
497
+ this.lastRemoteInitialWindowSize = value;
498
+ }
499
+ }
500
+ this.writeRaw(encodeSettings([], true)).catch(() => {
501
+ });
502
+ if (this.remoteSettingsResolve) {
503
+ this.remoteSettingsResolve();
504
+ this.remoteSettingsResolve = null;
505
+ }
506
+ }
507
+ handleWindowUpdateFrame(frame) {
508
+ if (frame.payload.length !== 4) {
509
+ this.sendGoawayAndClose(ErrorCode.FRAME_SIZE_ERROR);
510
+ return;
511
+ }
512
+ const increment = frame.payload.readUInt32BE(0) & 2147483647;
513
+ if (increment === 0) {
514
+ if (frame.streamId === 0) {
515
+ this.writeRaw(encodeGoaway(0, ErrorCode.PROTOCOL_ERROR)).catch(() => {
516
+ });
517
+ } else {
518
+ this.writeRaw(encodeRstStream(frame.streamId, ErrorCode.PROTOCOL_ERROR)).catch(() => {
519
+ });
520
+ }
521
+ return;
522
+ }
523
+ try {
524
+ if (frame.streamId === 0) {
525
+ this.connectionSendWindow.update(increment);
526
+ } else {
527
+ const stream = this.streams.get(frame.streamId);
528
+ if (stream) {
529
+ stream.handleWindowUpdate(increment);
530
+ }
531
+ }
532
+ } catch {
533
+ if (frame.streamId === 0) {
534
+ this.sendGoawayAndClose(ErrorCode.FLOW_CONTROL_ERROR);
535
+ } else {
536
+ this.writeRaw(encodeRstStream(frame.streamId, ErrorCode.FLOW_CONTROL_ERROR)).catch(
537
+ () => {
538
+ }
539
+ );
540
+ }
541
+ }
542
+ }
543
+ handlePingFrame(frame) {
544
+ if (frame.payload.length !== 8) {
545
+ this.sendGoawayAndClose(ErrorCode.FRAME_SIZE_ERROR);
546
+ return;
547
+ }
548
+ if (frame.flags & FrameFlags.ACK) return;
549
+ this.writeRaw(encodePing(frame.payload, true)).catch(() => {
550
+ });
551
+ }
552
+ handleGoawayFrame(frame) {
553
+ if (frame.payload.length < 8) {
554
+ this.sendGoawayAndClose(ErrorCode.FRAME_SIZE_ERROR);
555
+ return;
556
+ }
557
+ this.goawayReceived = true;
558
+ const lastStreamId = frame.payload.readUInt32BE(0) & 2147483647;
559
+ const errorCode = frame.payload.readUInt32BE(4);
560
+ for (const [id, stream] of this.streams) {
561
+ if (id > lastStreamId) {
562
+ stream.handleRstStream(ErrorCode.REFUSED_STREAM);
563
+ this.streams.delete(id);
564
+ }
565
+ }
566
+ this.emit("goaway", { lastStreamId, errorCode });
567
+ }
568
+ handleRstStreamFrame(frame) {
569
+ if (frame.payload.length !== 4) {
570
+ this.sendGoawayAndClose(ErrorCode.FRAME_SIZE_ERROR);
571
+ return;
572
+ }
573
+ const stream = this.streams.get(frame.streamId);
574
+ if (stream) {
575
+ const errorCode = frame.payload.readUInt32BE(0);
576
+ stream.handleRstStream(errorCode);
577
+ }
578
+ }
579
+ /** Send GOAWAY with given error code and close the connection. */
580
+ sendGoawayAndClose(errorCode) {
581
+ if (this.closed) return;
582
+ const lastStreamId = Math.max(0, this.nextStreamId - 2);
583
+ this.writeRaw(encodeGoaway(lastStreamId, errorCode)).catch(() => {
584
+ });
585
+ this.closed = true;
586
+ for (const stream of this.streams.values()) {
587
+ stream.handleRstStream(ErrorCode.CANCEL);
588
+ }
589
+ this.streams.clear();
590
+ this.connectionSendWindow.cancel();
591
+ this.socket.end();
592
+ this.emit("goaway", { lastStreamId, errorCode });
593
+ }
594
+ handleError(err) {
595
+ const errorCode = err.message?.includes("Frame size") ? ErrorCode.FRAME_SIZE_ERROR : ErrorCode.PROTOCOL_ERROR;
596
+ this.sendGoawayAndClose(errorCode);
597
+ this.emit("error", err);
598
+ }
599
+ handleSocketClose() {
600
+ if (!this.closed) {
601
+ this.closed = true;
602
+ for (const stream of this.streams.values()) {
603
+ stream.handleRstStream(ErrorCode.CANCEL);
604
+ }
605
+ this.streams.clear();
606
+ this.connectionSendWindow.cancel();
607
+ this.emit("close");
608
+ }
609
+ }
610
+ /**
611
+ * Queue a frame for writing. Frames queued in the same microtask
612
+ * are coalesced into a single socket.write() call.
613
+ */
614
+ writeRaw(data) {
615
+ return new Promise((resolve, reject) => {
616
+ this.pendingWrites.push(data);
617
+ this.flushPromiseResolvers.push({ resolve, reject });
618
+ if (!this.flushScheduled) {
619
+ this.flushScheduled = true;
620
+ queueMicrotask(() => this.flush());
621
+ }
622
+ });
623
+ }
624
+ /** Flush all pending writes as a single socket.write(). */
625
+ flush() {
626
+ this.flushScheduled = false;
627
+ const writes = this.pendingWrites;
628
+ const resolvers = this.flushPromiseResolvers;
629
+ this.pendingWrites = [];
630
+ this.flushPromiseResolvers = [];
631
+ if (writes.length === 0) return;
632
+ const merged = writes.length === 1 ? writes[0] : Buffer.concat(writes);
633
+ this.socket.write(merged, (err) => {
634
+ for (const r of resolvers) {
635
+ if (err) r.reject(err);
636
+ else r.resolve();
637
+ }
638
+ });
639
+ }
640
+ /** Write multiple frames as a single socket.write() immediately (no microtask delay). */
641
+ writeMulti(frames) {
642
+ if (frames.length === 0) return Promise.resolve();
643
+ const merged = frames.length === 1 ? frames[0] : Buffer.concat(frames);
644
+ return new Promise((resolve, reject) => {
645
+ this.socket.write(merged, (err) => {
646
+ if (err) reject(err);
647
+ else resolve();
648
+ });
649
+ });
650
+ }
651
+ /** Whether the connection has been initialized and is usable */
652
+ get isReady() {
653
+ return this.initialized && !this.closed && !this.goawayReceived;
654
+ }
655
+ /** Number of currently active (open) streams */
656
+ get activeStreamCount() {
657
+ return this.streams.size;
658
+ }
659
+ /** Remote peer's MAX_CONCURRENT_STREAMS setting (default: 100) */
660
+ get maxConcurrentStreams() {
661
+ return this.remoteSettings.get(SettingsId.MAX_CONCURRENT_STREAMS) ?? 100;
662
+ }
663
+ }
664
+ export {
665
+ Http2Connection
666
+ };