quico 0.1.3 → 0.2.0

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.
@@ -0,0 +1,1238 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import flat_ranges from 'flat-ranges';
16
+
17
+ import {
18
+ Emitter,
19
+ concatUint8Arrays,
20
+ uint8Equal,
21
+ ack_frame_to_ranges,
22
+ ranges_to_ack_frame
23
+ } from './utils.js';
24
+
25
+ import {
26
+ quic_derive_init_secrets,
27
+ quic_derive_from_tls_secrets,
28
+ quic_derive_key_update,
29
+ encrypt_quic_packet,
30
+ decrypt_quic_packet,
31
+ extract_tls_messages_from_chunks
32
+ } from './crypto.js';
33
+
34
+ import {
35
+ parse_quic_datagram,
36
+ encode_quic_frames,
37
+ parse_quic_frames
38
+ } from './transport.js';
39
+
40
+ import { TLSBridge } from './tls_bridge.js';
41
+
42
+
43
+ function QUICConnection(options) {
44
+ if (!(this instanceof QUICConnection)) return new QUICConnection(options);
45
+ options = options || {};
46
+
47
+ var ev = Emitter();
48
+ var tls = null;
49
+
50
+ // ============================================================
51
+ // Context
52
+ // ============================================================
53
+
54
+ var context = {
55
+ isServer: options.isServer !== false,
56
+ state: 'idle',
57
+ handshake_done: false,
58
+ handshake_done_sent: false,
59
+
60
+ version: 1,
61
+ original_dcid: null,
62
+ my_cids: [],
63
+ their_cids: [],
64
+
65
+ // Keys
66
+ initial_read: null,
67
+ initial_write: null,
68
+ handshake_read: null,
69
+ handshake_write: null,
70
+ app_read: null,
71
+ app_write: null,
72
+ app_prev_read: null, // previous read keys (kept during transition)
73
+ key_phase: false, // our current send key_phase
74
+ read_key_phase: false, // expected key_phase on incoming packets
75
+ app_read_secret: null, // current read secret (for deriving next)
76
+ app_write_secret: null, // current write secret (for deriving next)
77
+ cipher_hash: 'sha256', // hash for key derivation
78
+ cipher_suite: null, // TLS cipher suite code (for key length)
79
+
80
+ // Packet numbers — sending
81
+ send_pn: { initial: 0, handshake: 0, app: 0 },
82
+
83
+ // Packet numbers — receiving
84
+ recv_pn_largest: { initial: -1, handshake: -1, app: -1 },
85
+ recv_pn_ranges: { initial: [], handshake: [], app: [] },
86
+
87
+ // CRYPTO
88
+ crypto_chunks: { initial: {}, handshake: {} },
89
+ crypto_offset: { initial: 0, handshake: 0 },
90
+ crypto_send_offset: { initial: 0, handshake: 0, app: 0 },
91
+
92
+ // ACK
93
+ pending_ack: { initial: [], handshake: [], app: [] },
94
+
95
+ // Receiving streams
96
+ recv_streams: {},
97
+
98
+ // Sending streams
99
+ send_streams: {},
100
+
101
+ // In-flight tracking (Phase 1)
102
+ sending_app_pn_in_flight: new Set(),
103
+ sending_app_pn_history: [],
104
+
105
+ // Burst scheduler config (Phase 2)
106
+ max_packets_per_burst: 20,
107
+ max_packets_per_sec: 5000,
108
+ max_bytes_per_sec: 10000000, // 10MB/s
109
+ max_packet_payload: 1100,
110
+ max_packets_in_flight: 100,
111
+ max_bytes_in_flight: 5000000, // 5MB
112
+ burst_timer: null,
113
+
114
+ // Pending app packets
115
+ pending_app_packets: [],
116
+
117
+ // Flow control — connection level
118
+ bytes_sent: 0, // total STREAM bytes sent
119
+ bytes_received: 0, // total STREAM bytes received
120
+ remote_max_data: 1048576, // peer's limit on what we can send (default 1MB until parsed)
121
+ local_max_data: 1048576, // our limit on what peer can send (matches transport params)
122
+ local_max_data_consumed: 0, // how much of local_max_data has been used
123
+ local_max_data_threshold: 0.5, // send MAX_DATA when consumed > threshold × local_max_data
124
+ remote_max_streams_bidi: 100,
125
+ remote_max_streams_uni: 3,
126
+
127
+ // RTT
128
+ rtt_history: [],
129
+
130
+ // Timers
131
+ idle_timeout: options.idleTimeout || 30000,
132
+ handshake_timeout: options.handshakeTimeout || 10000,
133
+ last_activity: Date.now(),
134
+ idle_timer: null,
135
+ handshake_timer: null,
136
+
137
+ SNICallback: options.SNICallback || null,
138
+ hostname: options.hostname || null,
139
+ };
140
+
141
+
142
+ // ============================================================
143
+ // set_context
144
+ // ============================================================
145
+
146
+ function set_context(updates) {
147
+ if (!updates || typeof updates !== 'object') return;
148
+ var changed = {};
149
+
150
+ if ('state' in updates && updates.state !== context.state) {
151
+ context.state = updates.state;
152
+ changed.state = true;
153
+ }
154
+ if ('handshake_done' in updates && updates.handshake_done !== context.handshake_done) {
155
+ context.handshake_done = updates.handshake_done;
156
+ changed.handshake_done = true;
157
+ }
158
+ if ('handshake_done_sent' in updates) {
159
+ context.handshake_done_sent = updates.handshake_done_sent;
160
+ }
161
+ if ('original_dcid' in updates && updates.original_dcid !== null && context.original_dcid === null) {
162
+ context.original_dcid = updates.original_dcid;
163
+ changed.original_dcid = true;
164
+ }
165
+ if ('version' in updates && updates.version !== context.version) {
166
+ context.version = updates.version;
167
+ }
168
+ if ('add_their_cid' in updates) {
169
+ var cid = updates.add_their_cid;
170
+ var found = false;
171
+ for (var i = 0; i < context.their_cids.length; i++) {
172
+ if (uint8Equal(cid, context.their_cids[i])) { found = true; break; }
173
+ }
174
+ if (!found) context.their_cids.push(cid);
175
+ }
176
+ if ('initial_read' in updates) { context.initial_read = updates.initial_read; changed.initial_read = true; }
177
+ if ('initial_write' in updates) { context.initial_write = updates.initial_write; changed.initial_write = true; }
178
+ if ('handshake_read' in updates) { context.handshake_read = updates.handshake_read; }
179
+ if ('handshake_write' in updates){ context.handshake_write = updates.handshake_write; }
180
+ if ('app_read' in updates) { context.app_read = updates.app_read; }
181
+ if ('app_write' in updates) { context.app_write = updates.app_write; }
182
+ if ('remote_max_data' in updates && updates.remote_max_data > context.remote_max_data) {
183
+ context.remote_max_data = updates.remote_max_data; changed.remote_max_data = true;
184
+ }
185
+ if ('remote_max_streams_bidi' in updates) { context.remote_max_streams_bidi = updates.remote_max_streams_bidi; }
186
+ if ('remote_max_streams_uni' in updates) { context.remote_max_streams_uni = updates.remote_max_streams_uni; }
187
+ if ('key_phase' in updates && updates.key_phase !== context.key_phase) {
188
+ context.key_phase = updates.key_phase; changed.key_phase = true;
189
+ }
190
+
191
+ // ---- Reactive ----
192
+
193
+ if (changed.initial_read || changed.initial_write) {
194
+ if (context.state === 'idle') { context.state = 'handshaking'; changed.state = true; }
195
+ }
196
+
197
+ if (changed.handshake_done && context.handshake_done === true) {
198
+ console.log('[quic] handshake done — flushing ' + context.pending_app_packets.length + ' pending');
199
+ if (context.pending_app_packets.length > 0) {
200
+ var pending = context.pending_app_packets;
201
+ context.pending_app_packets = [];
202
+ for (var i = 0; i < pending.length; i++) {
203
+ processDecryptedPacket('app', pending[i].packet_number, pending[i].plaintext);
204
+ }
205
+ }
206
+ startIdleTimer();
207
+ }
208
+
209
+ if (changed.state && context.state === 'connected') {
210
+ // 'connect' event is emitted explicitly by TLS handlers (not here)
211
+ // Server emits on appSecrets, Client emits on secureConnect
212
+ }
213
+
214
+ if (changed.state && context.state === 'draining') {
215
+ clearIdleTimer();
216
+ var _drainTimer = setTimeout(function () {
217
+ if (context.state === 'draining') { context.state = 'closed'; ev.emit('close'); }
218
+ }, Math.min(3000, context.idle_timeout / 3));
219
+ if (_drainTimer.unref) _drainTimer.unref();
220
+ }
221
+
222
+ if (changed.state && context.state === 'closed') { clearIdleTimer(); ev.emit('close'); }
223
+ if (changed.remote_max_data) { plan_quic_burst(); }
224
+ if (changed.key_phase) {
225
+ // We initiated key update — derive new write keys
226
+ if (context.app_write_secret) {
227
+ var next = quic_derive_key_update(context.app_write_secret, context.cipher_hash, context.cipher_suite);
228
+ console.log('[quic] key update initiated — new write keys');
229
+ context.app_write = { key: next.key, iv: next.iv, hp: context.app_write.hp };
230
+ context.app_write_secret = next.secret;
231
+ }
232
+ }
233
+ }
234
+
235
+
236
+ // ============================================================
237
+ // Idle timeout
238
+ // ============================================================
239
+
240
+ function touchActivity() { context.last_activity = Date.now(); }
241
+
242
+ function startIdleTimer() {
243
+ clearIdleTimer();
244
+ if (context.idle_timeout <= 0) return;
245
+ context.idle_timer = setInterval(function () {
246
+ if (Date.now() - context.last_activity >= context.idle_timeout) {
247
+ console.log('[quic] idle timeout');
248
+ close(0, 'idle timeout');
249
+ }
250
+ }, Math.max(1000, Math.floor(context.idle_timeout / 4)));
251
+ if (context.idle_timer.unref) context.idle_timer.unref();
252
+ }
253
+
254
+ function clearIdleTimer() {
255
+ if (context.idle_timer !== null) { clearInterval(context.idle_timer); context.idle_timer = null; }
256
+ }
257
+
258
+
259
+ // ============================================================
260
+ // TLS Bridge
261
+ // ============================================================
262
+
263
+ function initTLS() {
264
+ // Start handshake timeout — if TLS doesn't complete in time, close
265
+ if (context.handshake_timeout > 0 && !context.handshake_timer) {
266
+ context.handshake_timer = setTimeout(function () {
267
+ if (context.state !== 'connected' && context.state !== 'closed' && context.state !== 'draining') {
268
+ console.log('[quic] handshake timeout (' + context.handshake_timeout + 'ms)');
269
+ ev.emit('error', new Error('QUIC handshake timeout'));
270
+ close(0x100, 'handshake timeout'); // 0x100 = CRYPTO_ERROR
271
+ }
272
+ }, context.handshake_timeout);
273
+ if (context.handshake_timer.unref) context.handshake_timer.unref();
274
+ }
275
+
276
+ tls = new TLSBridge({
277
+ isServer: context.isServer,
278
+ SNICallback: context.SNICallback,
279
+ originalDcid: context.original_dcid,
280
+ localCid: context.my_cids.length > 0 ? context.my_cids[0] : new Uint8Array(0),
281
+ hostname: context.hostname
282
+ });
283
+
284
+ tls.on('send', function (epoch, data) { cryptoWrite(epoch, data); });
285
+
286
+ tls.on('handshakeSecrets', function (secrets) {
287
+ set_context({
288
+ handshake_read: quic_derive_from_tls_secrets(secrets.remote, secrets.hash, secrets.cipher),
289
+ handshake_write: quic_derive_from_tls_secrets(secrets.local, secrets.hash, secrets.cipher)
290
+ });
291
+ });
292
+
293
+ tls.on('appSecrets', function (secrets) {
294
+ // Handshake complete — clear timeout
295
+ if (context.handshake_timer) {
296
+ clearTimeout(context.handshake_timer);
297
+ context.handshake_timer = null;
298
+ }
299
+
300
+ // Store secrets for future key updates (RFC 9001 §6)
301
+ context.app_read_secret = secrets.remote;
302
+ context.app_write_secret = secrets.local;
303
+ context.cipher_hash = secrets.hash;
304
+ context.cipher_suite = secrets.cipher;
305
+
306
+ set_context({
307
+ app_read: quic_derive_from_tls_secrets(secrets.remote, secrets.hash, secrets.cipher),
308
+ app_write: quic_derive_from_tls_secrets(secrets.local, secrets.hash, secrets.cipher),
309
+ state: 'connected',
310
+ handshake_done: true
311
+ });
312
+
313
+ // Server: emit connect now (Finished already sent)
314
+ // Client: wait for secureConnect (after client Finished is generated)
315
+ if (context.isServer) {
316
+ ev.emit('connect');
317
+ }
318
+ });
319
+
320
+ tls.on('secureConnect', function () {
321
+ // Client: Finished has been generated and queued.
322
+ // Flush handshake first, then emit connect so 1-RTT goes after Finished.
323
+ if (!context.isServer) {
324
+ plan_quic_burst(); // flush any pending handshake data (Finished)
325
+ ev.emit('connect');
326
+ }
327
+ });
328
+
329
+ // Note: QUIC handles key updates via key_phase bit, not TLS KeyUpdate message.
330
+ // RFC 9001 §6: "endpoints MUST treat the receipt of a TLS KeyUpdate message
331
+ // as a connection error of type 0x010a" — but we silently ignore it.
332
+ }
333
+
334
+
335
+ // ============================================================
336
+ // Incoming packets
337
+ // ============================================================
338
+
339
+ function feedDatagram(from_ip, from_port, data) {
340
+ console.log('[quic] datagram from ' + from_ip + ':' + from_port + ' len=' + data.length);
341
+ touchActivity();
342
+ var packets = parse_quic_datagram(data);
343
+ for (var i = 0; i < packets.length; i++) {
344
+ if (packets[i] !== null) feedPacket(packets[i]);
345
+ }
346
+ }
347
+
348
+ function feedPacket(pkt) {
349
+ if (context.state === 'draining' || context.state === 'closed') return;
350
+
351
+ if (pkt.version && pkt.version !== context.version) set_context({ version: pkt.version });
352
+ if (pkt.dcid && pkt.dcid.byteLength > 0) set_context({ original_dcid: pkt.dcid });
353
+ if (pkt.scid && pkt.scid.byteLength > 0) set_context({ add_their_cid: pkt.scid });
354
+
355
+ var space = pkt.type === 'initial' ? 'initial'
356
+ : pkt.type === 'handshake' ? 'handshake'
357
+ : pkt.type === '1rtt' ? 'app' : null;
358
+ if (!space) return;
359
+
360
+ if (space === 'initial' && !context.initial_read && context.original_dcid) {
361
+ set_context({
362
+ initial_read: quic_derive_init_secrets(context.original_dcid, context.version, 'read'),
363
+ initial_write: quic_derive_init_secrets(context.original_dcid, context.version, 'write')
364
+ });
365
+ }
366
+
367
+ var readKeys = space === 'initial' ? context.initial_read
368
+ : space === 'handshake' ? context.handshake_read : context.app_read;
369
+ if (!readKeys) return;
370
+
371
+ var decrypted = decrypt_quic_packet(
372
+ pkt.raw, readKeys.key, readKeys.iv, readKeys.hp,
373
+ context.original_dcid, context.recv_pn_largest[space]
374
+ );
375
+
376
+ // Key Update: if app decrypt fails, try with derived next keys
377
+ if ((!decrypted || !decrypted.plaintext) && space === 'app' && context.app_read_secret) {
378
+ var next = quic_derive_key_update(context.app_read_secret, context.cipher_hash, context.cipher_suite);
379
+ decrypted = decrypt_quic_packet(
380
+ pkt.raw, next.key, next.iv, readKeys.hp, // HP doesn't change
381
+ context.original_dcid, context.recv_pn_largest[space]
382
+ );
383
+
384
+ if (decrypted && decrypted.plaintext && decrypted.plaintext.byteLength > 0) {
385
+ // Key update confirmed — install new read keys
386
+ console.log('[quic] key update detected — installing new read keys');
387
+ context.app_prev_read = context.app_read;
388
+ context.app_read = { key: next.key, iv: next.iv, hp: readKeys.hp };
389
+ context.app_read_secret = next.secret;
390
+ context.read_key_phase = !context.read_key_phase;
391
+ }
392
+ }
393
+
394
+ // Also try prev keys for reordered packets from before key update
395
+ if ((!decrypted || !decrypted.plaintext) && space === 'app' && context.app_prev_read) {
396
+ decrypted = decrypt_quic_packet(
397
+ pkt.raw, context.app_prev_read.key, context.app_prev_read.iv, context.app_prev_read.hp,
398
+ context.original_dcid, context.recv_pn_largest[space]
399
+ );
400
+ }
401
+
402
+ if (!decrypted || !decrypted.plaintext || decrypted.plaintext.byteLength === 0) {
403
+ console.log('[quic] decrypt failed: ' + space + ' raw_len=' + pkt.raw.byteLength + ' first20=' + Array.from(pkt.raw.slice(0, 20)).map(function(b){ return b.toString(16).padStart(2,'0'); }).join(' ') + ' dcid_len=' + (context.original_dcid ? context.original_dcid.byteLength : 'null') + ' has_keys=' + !!readKeys + ' largest_pn=' + context.recv_pn_largest[space]);
404
+ return;
405
+ }
406
+
407
+ console.log('[quic] decrypted ' + space + ' pn=' + decrypted.packet_number + ' len=' + decrypted.plaintext.byteLength);
408
+
409
+ var pn = decrypted.packet_number;
410
+ var ranges = context.recv_pn_ranges[space];
411
+ var isNew = true;
412
+ for (var ri = 0; ri < ranges.length; ri += 2) {
413
+ if (pn >= ranges[ri] && pn < ranges[ri + 1]) { isNew = false; break; }
414
+ }
415
+ if (isNew) {
416
+ flat_ranges.add(ranges, [pn, pn + 1]);
417
+ if (pn > context.recv_pn_largest[space]) context.recv_pn_largest[space] = pn;
418
+ }
419
+
420
+ console.log('[quic] pn=' + pn + ' isNew=' + isNew);
421
+ if (!isNew) return;
422
+
423
+ if (space === 'app' && !context.handshake_done) {
424
+ context.pending_app_packets.push(decrypted);
425
+ return;
426
+ }
427
+
428
+ processDecryptedPacket(space, pn, decrypted.plaintext);
429
+ }
430
+
431
+
432
+ // ============================================================
433
+ // Frame processing
434
+ // ============================================================
435
+
436
+ function processDecryptedPacket(space, packetNumber, plaintext) {
437
+ var frames = parse_quic_frames(plaintext);
438
+ console.log('[quic] frames: ' + space + ' pn=' + packetNumber + ' [' + frames.map(function(f){ return f.type; }).join(',') + ']');
439
+ var ackEliciting = false;
440
+
441
+ if (context.isServer && space === 'app' && !context.handshake_done_sent) {
442
+ set_context({ handshake_done_sent: true });
443
+ sendFrames('app', [{ type: 'handshake_done' }]);
444
+ }
445
+
446
+ for (var i = 0; i < frames.length; i++) {
447
+ var frame = frames[i];
448
+
449
+ if (frame.type === 'crypto') {
450
+ ackEliciting = true;
451
+ processCryptoFrame(space, frame.offset, frame.data);
452
+ } else if (frame.type === 'stream') {
453
+ ackEliciting = true;
454
+ processStreamFrame(frame);
455
+ } else if (frame.type === 'ack') {
456
+ processAckFrame(space, frame);
457
+ } else if (frame.type === 'ping') {
458
+ ackEliciting = true;
459
+ } else if (frame.type === 'handshake_done') {
460
+ ackEliciting = true;
461
+ } else if (frame.type === 'path_challenge') {
462
+ ackEliciting = true;
463
+ sendFrames(space, [{ type: 'path_response', data: frame.data }]);
464
+ } else if (frame.type === 'new_connection_id') {
465
+ ackEliciting = true;
466
+ } else if (frame.type === 'connection_close') {
467
+ console.log('[quic] CONNECTION_CLOSE error=0x' + (frame.error || 0).toString(16) + ' frame_type=0x' + (frame.frameType || 0).toString(16) + ' reason="' + (frame.reason || '') + '"');
468
+ set_context({ state: 'draining' });
469
+ return;
470
+ } else if (frame.type === 'stop_sending') {
471
+ // Peer says: stop sending on this stream
472
+ ackEliciting = true;
473
+ if (frame.id in context.send_streams) {
474
+ console.log('[quic] STOP_SENDING stream=' + frame.id + ' error=' + frame.error);
475
+ delete context.send_streams[frame.id];
476
+ }
477
+ } else if (frame.type === 'reset_stream') {
478
+ // Peer cancelled their stream
479
+ ackEliciting = true;
480
+ if (frame.id in context.recv_streams) {
481
+ console.log('[quic] RESET_STREAM stream=' + frame.id + ' error=' + frame.error);
482
+ delete context.recv_streams[frame.id];
483
+ }
484
+ } else if (frame.type === 'max_data') {
485
+ ackEliciting = true;
486
+ set_context({ remote_max_data: frame.max });
487
+ } else if (frame.type === 'max_streams_bidi') {
488
+ ackEliciting = true;
489
+ set_context({ remote_max_streams_bidi: frame.max });
490
+ } else if (frame.type === 'max_streams_uni') {
491
+ ackEliciting = true;
492
+ set_context({ remote_max_streams_uni: frame.max });
493
+ } else if (frame.type === 'max_stream_data') {
494
+ ackEliciting = true;
495
+ } else if (frame.type === 'datagram') {
496
+ ev.emit('datagram', frame.contextId, frame.data);
497
+ }
498
+ }
499
+
500
+ if (ackEliciting) {
501
+ flat_ranges.add(context.pending_ack[space], [packetNumber, packetNumber + 1]);
502
+ var ackFrame = ranges_to_ack_frame(context.pending_ack[space], null, 0);
503
+ if (ackFrame) {
504
+ sendFrames(space, [ackFrame]);
505
+ if (space !== 'app') context.pending_ack[space] = [];
506
+ }
507
+ }
508
+ }
509
+
510
+
511
+ // ============================================================
512
+ // CRYPTO → TLS
513
+ // ============================================================
514
+
515
+ function processCryptoFrame(space, offset, data) {
516
+ if (space !== 'initial' && space !== 'handshake') return;
517
+ console.log('[quic] CRYPTO: space=' + space + ' offset=' + offset + ' len=' + data.length);
518
+
519
+ var chunks = context.crypto_chunks[space];
520
+ var fromOffset = context.crypto_offset[space];
521
+ if (!(offset in chunks) || chunks[offset].byteLength < data.byteLength) chunks[offset] = data;
522
+
523
+ var result = extract_tls_messages_from_chunks(chunks, fromOffset);
524
+ console.log('[quic] TLS messages: ' + (result ? result.tls_messages.length : 'none'));
525
+ if (!result) return;
526
+
527
+ context.crypto_offset[space] = result.new_from_offset;
528
+ if (!tls) { console.log('[quic] initializing TLS bridge'); initTLS(); }
529
+
530
+ for (var i = 0; i < result.tls_messages.length; i++) {
531
+ var msg = result.tls_messages[i];
532
+ var msgType = msg[0];
533
+ console.log('[quic] → TLS msg #' + i + ' type=0x' + msgType.toString(16) + ' len=' + msg.length);
534
+
535
+ // Detect HelloRetryRequest (type 0x02 with special random)
536
+ if (msgType === 0x02 && msg.length >= 38) {
537
+ var hrrRandom = [0xCF,0x21,0xAD,0x74,0xE5,0x9A,0x61,0x11,0xBE,0x1D,0x8C,0x02,0x1E,0x65,0xB8,0x91,0xC2,0xA2,0x11,0x16,0x7A,0xBB,0x8C,0x5E,0x07,0x9E,0x09,0xE2,0xC8,0xA8,0x33,0x9C];
538
+ var isHRR = true;
539
+ for (var j = 0; j < 32; j++) { if (msg[6 + j] !== hrrRandom[j]) { isHRR = false; break; } }
540
+ if (isHRR) console.log('[quic] ⚠️ HelloRetryRequest detected! LemonTLS needs HRR support.');
541
+ }
542
+
543
+ tls.feedMessage(msg);
544
+ }
545
+ }
546
+
547
+
548
+ // ============================================================
549
+ // Stream receive
550
+ // ============================================================
551
+
552
+ function processStreamFrame(frame) {
553
+ var sid = frame.id;
554
+ if (!(sid in context.recv_streams)) {
555
+ context.recv_streams[sid] = { chunks: {}, ranges: [], total_size: 0, flushed_to: 0 };
556
+ }
557
+ var stream = context.recv_streams[sid];
558
+
559
+ var alreadyHave = false;
560
+ for (var ri = 0; ri < stream.ranges.length; ri += 2) {
561
+ if (frame.offset >= stream.ranges[ri] && frame.offset + frame.data.length <= stream.ranges[ri + 1]) {
562
+ alreadyHave = true; break;
563
+ }
564
+ }
565
+ if (!alreadyHave) {
566
+ flat_ranges.add(stream.ranges, [frame.offset, frame.offset + frame.data.length]);
567
+ if (!(frame.offset in stream.chunks) || stream.chunks[frame.offset].byteLength < frame.data.byteLength) {
568
+ stream.chunks[frame.offset] = frame.data;
569
+ }
570
+ // Track connection-level bytes received
571
+ context.bytes_received += frame.data.byteLength;
572
+ checkLocalFlowControl();
573
+ }
574
+ if (frame.fin && stream.total_size === 0) stream.total_size = frame.offset + frame.data.length;
575
+ flushStream(sid);
576
+ }
577
+
578
+ /**
579
+ * Check if we need to send MAX_DATA to give the peer more send capacity.
580
+ * When consumed > threshold × window, double the window and notify peer.
581
+ */
582
+ function checkLocalFlowControl() {
583
+ if (context.bytes_received > context.local_max_data * context.local_max_data_threshold) {
584
+ var newMax = context.local_max_data * 2;
585
+ context.local_max_data = newMax;
586
+ sendFrames('app', [{ type: 'max_data', max: newMax }]);
587
+ }
588
+ }
589
+
590
+ function flushStream(sid) {
591
+ var stream = context.recv_streams[sid];
592
+ if (!stream) return;
593
+ var parts = [];
594
+ var offset = stream.flushed_to;
595
+ while (offset in stream.chunks) {
596
+ var chunk = stream.chunks[offset];
597
+ delete stream.chunks[offset];
598
+ parts.push(chunk);
599
+ offset += chunk.byteLength;
600
+ }
601
+ if (parts.length === 0) return;
602
+ stream.flushed_to = offset;
603
+ var data = parts.length === 1 ? parts[0] : concatUint8Arrays(parts);
604
+ var fin = (stream.total_size > 0 && offset >= stream.total_size);
605
+ ev.emit('stream', Number(sid), data, fin);
606
+ if (fin) setTimeout(function () { delete context.recv_streams[sid]; }, 100);
607
+ }
608
+
609
+
610
+ // ============================================================
611
+ // ACK processing (Phase 1)
612
+ // ============================================================
613
+
614
+ function processAckFrame(space, frame) {
615
+ var ackedRanges = ack_frame_to_ranges(frame);
616
+ if (!ackedRanges || ackedRanges.length === 0) return;
617
+
618
+ if (space === 'app') {
619
+ // RTT measurement
620
+ if ('largest' in frame && 'delay' in frame) {
621
+ var largest_pn = frame.largest;
622
+ if (context.sending_app_pn_in_flight.has(largest_pn)) {
623
+ var now = Date.now();
624
+ var ack_delay_ms = Math.round((frame.delay * Math.pow(2, 3)) / 1000);
625
+ var pn_index = largest_pn - (context.send_pn.app - context.sending_app_pn_history.length);
626
+ if (pn_index >= 0 && pn_index < context.sending_app_pn_history.length) {
627
+ var measured_rtt = now - context.sending_app_pn_history[pn_index][0] - ack_delay_ms;
628
+ if (measured_rtt > 0) context.rtt_history.push([now, measured_rtt]);
629
+ }
630
+ }
631
+ }
632
+
633
+ // Mark in-flight as acked
634
+ for (var pn of context.sending_app_pn_in_flight) {
635
+ var is_acked = false;
636
+ for (var ri = 0; ri < ackedRanges.length; ri += 2) {
637
+ if (pn >= ackedRanges[ri] && pn <= ackedRanges[ri + 1]) { is_acked = true; break; }
638
+ }
639
+ if (is_acked) {
640
+ context.sending_app_pn_in_flight.delete(pn);
641
+ for (var sid in context.send_streams) {
642
+ var st = context.send_streams[sid];
643
+ if (st.in_flight_ranges && pn in st.in_flight_ranges) {
644
+ flat_ranges.add(st.acked_ranges, st.in_flight_ranges[pn]);
645
+ delete st.in_flight_ranges[pn];
646
+ if (st.total_size > 0 && st.acked_ranges.length === 2 &&
647
+ st.acked_ranges[0] === 0 && st.acked_ranges[1] >= st.total_size) {
648
+ delete context.send_streams[sid];
649
+ }
650
+ }
651
+ }
652
+ }
653
+ }
654
+
655
+ // ACK freed capacity — try to send more
656
+ plan_quic_burst();
657
+ }
658
+ }
659
+
660
+
661
+ // ============================================================
662
+ // Sending
663
+ // ============================================================
664
+
665
+ function cryptoWrite(epoch, data) {
666
+ var space = epoch === 'initial' ? 'initial' : epoch === 'handshake' ? 'handshake' : 'app';
667
+ var offset = context.crypto_send_offset[space];
668
+ context.crypto_send_offset[space] += data.byteLength;
669
+ sendFrames(space, [{ type: 'crypto', offset: offset, data: data }]);
670
+ }
671
+
672
+ function sendFrames(space, frameList) {
673
+ var writeKeys = space === 'initial' ? context.initial_write
674
+ : space === 'handshake' ? context.handshake_write : context.app_write;
675
+ if (!writeKeys) { console.log('[quic] sendFrames(' + space + ') — no keys'); return; }
676
+
677
+ var pn = context.send_pn[space]++;
678
+ var packetType = space === 'initial' ? 'initial' : space === 'handshake' ? 'handshake' : '1rtt';
679
+
680
+ var dcid, scid;
681
+ if (packetType === '1rtt') {
682
+ dcid = context.their_cids.length > 0 ? context.their_cids[0] : new Uint8Array(0);
683
+ scid = new Uint8Array(0);
684
+ } else if (context.isServer) {
685
+ dcid = context.their_cids.length > 0 ? context.their_cids[0] : new Uint8Array(0);
686
+ scid = context.original_dcid || new Uint8Array(0);
687
+ } else {
688
+ // Client: Initial uses original_dcid (for key derivation),
689
+ // Handshake uses server's SCID (RFC 9000 §7.2)
690
+ if (space === 'initial' || context.their_cids.length === 0) {
691
+ dcid = context.original_dcid || new Uint8Array(0);
692
+ } else {
693
+ dcid = context.their_cids[0];
694
+ }
695
+ scid = context.my_cids.length > 0 ? context.my_cids[0] : new Uint8Array(0);
696
+ }
697
+
698
+ var encoded = encode_quic_frames(frameList);
699
+
700
+ // RFC 9000 §14.1: Initial datagrams must be >= 1200 bytes
701
+ if (space === 'initial') {
702
+ var overhead = 1 + 4 + 1 + dcid.byteLength + 1 + scid.byteLength + 1 + 2 + 1 + 16;
703
+ var minPayload = 1200 - overhead;
704
+ if (encoded.length < minPayload) {
705
+ encoded = concatUint8Arrays([encoded, new Uint8Array(minPayload - encoded.length)]);
706
+ }
707
+ }
708
+
709
+ var encrypted = encrypt_quic_packet(
710
+ packetType, encoded, writeKeys.key, writeKeys.iv, writeKeys.hp, pn, dcid, scid, null,
711
+ space === 'app' ? context.key_phase : false
712
+ );
713
+
714
+ if (encrypted) {
715
+ var fnames = frameList.map(function(f){ return f.type + (f.type === 'ack' ? '(lg=' + f.largest + ')' : ''); }).join(',');
716
+ console.log('[quic] → ' + packetType + ' pn=' + pn + ' frames=[' + fnames + '] len=' + encrypted.length);
717
+ touchActivity();
718
+
719
+ if (space === 'app') {
720
+ context.sending_app_pn_history.push([Date.now(), encoded.length]);
721
+ var has_data = false;
722
+ for (var i = 0; i < frameList.length; i++) {
723
+ if (frameList[i].type === 'stream' || frameList[i].type === 'crypto') { has_data = true; break; }
724
+ }
725
+ if (has_data) context.sending_app_pn_in_flight.add(pn);
726
+
727
+ for (var i = 0; i < frameList.length; i++) {
728
+ if (frameList[i].type === 'stream') {
729
+ var sid = frameList[i].id;
730
+ var dataLen = frameList[i].data ? frameList[i].data.byteLength : 0;
731
+
732
+ // Track connection-level bytes sent
733
+ context.bytes_sent += dataLen;
734
+
735
+ if (sid in context.send_streams) {
736
+ if (!context.send_streams[sid].in_flight_ranges) context.send_streams[sid].in_flight_ranges = {};
737
+ var from = frameList[i].offset;
738
+ var to = from + (frameList[i].data ? frameList[i].data.byteLength : 0);
739
+ if (!context.send_streams[sid].in_flight_ranges[pn]) {
740
+ context.send_streams[sid].in_flight_ranges[pn] = [from, to];
741
+ } else {
742
+ flat_ranges.add(context.send_streams[sid].in_flight_ranges[pn], [from, to]);
743
+ }
744
+ }
745
+ }
746
+ }
747
+ }
748
+
749
+ ev.emit('packet', encrypted);
750
+ }
751
+ }
752
+
753
+
754
+ // ============================================================
755
+ // Stream send + Burst scheduler (Phase 2)
756
+ // Adapted from old quic_socket.js, improved with set_context
757
+ // ============================================================
758
+
759
+ /**
760
+ * Buffer data for a stream. Does NOT send immediately.
761
+ * Calls plan_quic_burst() to schedule sending.
762
+ */
763
+ function set_sending_stream(streamId, options) {
764
+ if (!(streamId in context.send_streams)) {
765
+ context.send_streams[streamId] = {
766
+ pending_data: null, pending_offset_start: 0,
767
+ write_offset: 0, send_offset: 0, total_size: 0,
768
+ fin_sent: false, acked_ranges: [], in_flight_ranges: {}
769
+ };
770
+ }
771
+
772
+ var stream = context.send_streams[streamId];
773
+
774
+ if (typeof options === 'object' && 'add_chunk' in options) {
775
+ if (options.add_chunk.data === null || options.add_chunk.data === undefined) {
776
+ if (stream.total_size === 0) {
777
+ stream.total_size = stream.write_offset;
778
+ console.log('[quic] stream ' + streamId + ' FIN via null data, total_size=' + stream.total_size);
779
+ }
780
+ } else {
781
+ var chunk = options.add_chunk.data;
782
+ if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
783
+ var start = stream.write_offset;
784
+ stream.write_offset += chunk.byteLength;
785
+ console.log('[quic] stream ' + streamId + ' add_chunk len=' + chunk.byteLength + ' write_offset=' + stream.write_offset + ' fin=' + !!options.add_chunk.fin);
786
+
787
+ if (stream.pending_data === null) {
788
+ stream.pending_data = chunk;
789
+ stream.pending_offset_start = start;
790
+ } else {
791
+ var old = stream.pending_data, old_off = stream.pending_offset_start;
792
+ var ns = Math.min(old_off, start);
793
+ var ne = Math.max(old_off + old.length, start + chunk.length);
794
+ var merged = new Uint8Array(ne - ns);
795
+ merged.set(old, old_off - ns);
796
+ merged.set(chunk, start - ns);
797
+ stream.pending_data = merged;
798
+ stream.pending_offset_start = ns;
799
+ }
800
+ if (options.add_chunk.fin) {
801
+ stream.total_size = stream.write_offset;
802
+ console.log('[quic] stream ' + streamId + ' FIN via add_chunk.fin, total_size=' + stream.total_size);
803
+ }
804
+ }
805
+ }
806
+
807
+ // Don't send directly — let burst scheduler handle it
808
+ plan_quic_burst();
809
+ }
810
+
811
+ function sendStream(streamId, data, fin) {
812
+ set_sending_stream(streamId, { add_chunk: { data: data, fin: fin } });
813
+ }
814
+
815
+ function sendDatagram(data) {
816
+ if (context.state !== 'connected') return;
817
+ if (typeof data === 'string') data = new TextEncoder().encode(data);
818
+ sendFrames('app', [{ type: 'datagram', data: data }]);
819
+ }
820
+
821
+
822
+ /**
823
+ * plan_quic_burst — calculates how many packets we can send,
824
+ * calls execute_quic_burst, schedules next burst if needed.
825
+ */
826
+ function plan_quic_burst() {
827
+ if (!context.app_write) return;
828
+ if (context.state !== 'connected') return;
829
+
830
+ // Cancel pending timer
831
+ if (context.burst_timer !== null) {
832
+ clearTimeout(context.burst_timer);
833
+ clearImmediate(context.burst_timer);
834
+ context.burst_timer = null;
835
+ }
836
+
837
+ var now = Date.now();
838
+ var oneSecAgo = now - 1000;
839
+
840
+ // Count bytes/packets sent in last second
841
+ var bytesSentLastSec = 0;
842
+ var packetsSentLastSec = 0;
843
+ for (var i = 0; i < context.sending_app_pn_history.length; i++) {
844
+ if (context.sending_app_pn_history[i][0] > oneSecAgo) {
845
+ bytesSentLastSec += context.sending_app_pn_history[i][1];
846
+ packetsSentLastSec++;
847
+ }
848
+ }
849
+
850
+ // Prune old history (older than 2 seconds)
851
+ var twoSecAgo = now - 2000;
852
+ while (context.sending_app_pn_history.length > 0 && context.sending_app_pn_history[0][0] < twoSecAgo) {
853
+ context.sending_app_pn_history.shift();
854
+ }
855
+
856
+ // Rate limits
857
+ var bytesRemaining = context.max_bytes_per_sec - bytesSentLastSec;
858
+ var packetsRemaining = context.max_packets_per_sec - packetsSentLastSec;
859
+ if (bytesRemaining < 0) bytesRemaining = 0;
860
+ if (packetsRemaining < 0) packetsRemaining = 0;
861
+
862
+ // In-flight limits
863
+ var inflightCount = context.sending_app_pn_in_flight.size;
864
+ var inflightRoom = context.max_packets_in_flight - inflightCount;
865
+ if (inflightRoom < 0) inflightRoom = 0;
866
+
867
+ // Calculate burst size
868
+ var burstCount = Math.min(
869
+ context.max_packets_per_burst,
870
+ packetsRemaining,
871
+ inflightRoom,
872
+ Math.floor(bytesRemaining / Math.max(1, 35)) // min packet ~35 bytes
873
+ );
874
+ if (burstCount < 0) burstCount = 0;
875
+
876
+ // Check if there's anything to send
877
+ var hasData = false;
878
+ for (var sid in context.send_streams) {
879
+ var st = context.send_streams[sid];
880
+ if (!st.pending_data || st.pending_data.byteLength === 0) continue;
881
+ var total = st.total_size > 0 ? st.total_size : st.write_offset;
882
+ var known = st.acked_ranges.slice();
883
+ if (st.in_flight_ranges) {
884
+ for (var pn in st.in_flight_ranges) flat_ranges.add(known, st.in_flight_ranges[pn]);
885
+ }
886
+ var missing = flat_ranges.invert(known, 0, total);
887
+ if (missing.length > 0) { hasData = true; break; }
888
+ // Also check: all data sent but FIN not yet sent?
889
+ if (st.total_size > 0 && !st.fin_sent && missing.length === 0) { hasData = true; break; }
890
+ }
891
+ // Also check pending ACK
892
+ var hasPendingAck = context.pending_ack.app.length > 0;
893
+
894
+ if (!hasData && !hasPendingAck) return;
895
+
896
+ // Execute burst
897
+ var sent = false;
898
+ if (burstCount > 0) {
899
+ sent = execute_quic_burst(burstCount);
900
+ }
901
+
902
+ // Schedule next burst if needed
903
+ if (sent && hasData) {
904
+ // More data to send — schedule next burst immediately (no timer delay)
905
+ context.burst_timer = setImmediate(function () {
906
+ context.burst_timer = null;
907
+ plan_quic_burst();
908
+ });
909
+ } else if (burstCount === 0 && hasData) {
910
+ // Rate limited — wait for capacity
911
+ var waitMs = (packetsRemaining <= 0 || bytesRemaining < 35) ? 50 : 10;
912
+ context.burst_timer = setTimeout(function () {
913
+ context.burst_timer = null;
914
+ plan_quic_burst();
915
+ }, waitMs);
916
+ if (context.burst_timer.unref) context.burst_timer.unref();
917
+ }
918
+ }
919
+
920
+
921
+ /**
922
+ * execute_quic_burst — fill up to packet_count packets.
923
+ * Round-robin across active streams.
924
+ * Returns true if at least one packet was sent.
925
+ */
926
+ function execute_quic_burst(packetCount) {
927
+ var MAX_PAYLOAD = context.max_packet_payload;
928
+ var OVERHEAD = 24; // STREAM frame header overhead estimate
929
+ var sentAny = false;
930
+
931
+ function getActiveIds() {
932
+ var ids = [];
933
+ for (var sid in context.send_streams) {
934
+ var st = context.send_streams[sid];
935
+ if (!st.pending_data || st.pending_data.byteLength === 0) continue;
936
+ var total = st.total_size > 0 ? st.total_size : st.write_offset;
937
+ // Build known_sent = acked + in_flight
938
+ var known = st.acked_ranges.slice();
939
+ if (st.in_flight_ranges) {
940
+ for (var pn in st.in_flight_ranges) flat_ranges.add(known, st.in_flight_ranges[pn]);
941
+ }
942
+ var missing = flat_ranges.invert(known, 0, total);
943
+ if (missing.length > 0) ids.push(Number(sid));
944
+ }
945
+ return ids;
946
+ }
947
+
948
+ var activeIds = getActiveIds();
949
+ var burstYielded = {}; // track ranges yielded per-stream within this burst
950
+
951
+ for (var p = 0; p < packetCount; p++) {
952
+ var frames = [];
953
+ var used = 0;
954
+
955
+ // (1) ACK in first packet if pending
956
+ if (p === 0 && context.pending_ack.app.length > 0) {
957
+ var ackFrame = ranges_to_ack_frame(context.pending_ack.app, null, 0);
958
+ if (ackFrame) {
959
+ var ackEncoded = encode_quic_frames([ackFrame]);
960
+ if (ackEncoded.byteLength <= MAX_PAYLOAD) {
961
+ frames.push(ackFrame);
962
+ used += ackEncoded.byteLength;
963
+ context.pending_ack.app = [];
964
+ }
965
+ }
966
+ }
967
+
968
+ // (2) Round-robin STREAM data across active streams
969
+ if (activeIds.length > 0 && used < MAX_PAYLOAD) {
970
+ var progress = true;
971
+ while (used < MAX_PAYLOAD && progress) {
972
+ progress = false;
973
+
974
+ for (var i = 0; i < activeIds.length; i++) {
975
+ if (used >= MAX_PAYLOAD) break;
976
+
977
+ var sid = activeIds[i];
978
+ var st = context.send_streams[sid];
979
+ if (!st || !st.pending_data) continue;
980
+
981
+ var budget = Math.max(0, MAX_PAYLOAD - used - OVERHEAD);
982
+ if (budget <= 0) break;
983
+
984
+ var chunks = get_stream_chunks(sid, budget);
985
+ if (!chunks || chunks.length === 0) continue;
986
+
987
+ for (var c = 0; c < chunks.length; c++) {
988
+ var ch = chunks[c];
989
+ var fin = (st.total_size > 0 && ch.offset + ch.data.byteLength >= st.total_size);
990
+ frames.push({ type: 'stream', id: sid, offset: ch.offset, fin: fin, data: ch.data });
991
+ used += ch.data.byteLength + OVERHEAD;
992
+ progress = true;
993
+
994
+ // Immediately track as yielded so next round-robin iteration won't resend
995
+ if (!burstYielded[sid]) burstYielded[sid] = [];
996
+ flat_ranges.add(burstYielded[sid], [ch.offset, ch.offset + ch.data.byteLength]);
997
+ // Also add to in_flight with temp key so get_stream_chunks sees it
998
+ if (!st.in_flight_ranges) st.in_flight_ranges = {};
999
+ if (!st.in_flight_ranges['_burst']) st.in_flight_ranges['_burst'] = [];
1000
+ flat_ranges.add(st.in_flight_ranges['_burst'], [ch.offset, ch.offset + ch.data.byteLength]);
1001
+ }
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ // (3) FIN-only frames: if all data sent/in-flight but FIN not yet sent
1007
+ for (var sid in context.send_streams) {
1008
+ var st = context.send_streams[sid];
1009
+ if (!st || st.total_size <= 0 || st.fin_sent) continue;
1010
+ // Check if all bytes are covered (acked + in_flight)
1011
+ var known = st.acked_ranges.slice();
1012
+ if (st.in_flight_ranges) {
1013
+ for (var pn in st.in_flight_ranges) flat_ranges.add(known, st.in_flight_ranges[pn]);
1014
+ }
1015
+ var missing = flat_ranges.invert(known, 0, st.total_size);
1016
+ console.log('[quic] FIN-only check stream=' + sid + ' total_size=' + st.total_size + ' fin_sent=' + st.fin_sent + ' missing=' + missing.length + ' known=' + JSON.stringify(known));
1017
+ if (missing.length === 0) {
1018
+ console.log('[quic] → sending FIN-only for stream ' + sid);
1019
+ // All data sent — send FIN-only STREAM frame
1020
+ frames.push({ type: 'stream', id: Number(sid), offset: st.total_size, fin: true, data: new Uint8Array(0) });
1021
+ st.fin_sent = true;
1022
+ }
1023
+ }
1024
+
1025
+ // Send packet if it has content beyond just ACK-only
1026
+ if (frames.length > 0) {
1027
+ sendFrames('app', frames);
1028
+ sentAny = true;
1029
+ // Clean up temp _burst in_flight keys (real PN keys now recorded by sendFrames)
1030
+ for (var sid in burstYielded) {
1031
+ var st = context.send_streams[sid];
1032
+ if (st && st.in_flight_ranges && st.in_flight_ranges['_burst']) {
1033
+ delete st.in_flight_ranges['_burst'];
1034
+ }
1035
+ }
1036
+ } else {
1037
+ break; // nothing to fill further packets
1038
+ }
1039
+
1040
+ // Refresh active list
1041
+ activeIds = getActiveIds();
1042
+ if (activeIds.length === 0) break;
1043
+ }
1044
+
1045
+ return sentAny;
1046
+ }
1047
+
1048
+
1049
+ /**
1050
+ * get_stream_chunks — find what bytes are missing (not acked) and
1051
+ * extract up to maxBytes from pending_data.
1052
+ * Uses flat_ranges.invert(acked_ranges) — the heart of retransmit.
1053
+ * Adapted from old get_stream_chunks_to_send().
1054
+ *
1055
+ * Strategy: first forward from send_offset (new data),
1056
+ * then fill gaps before send_offset (retransmit).
1057
+ * pending_data is NEVER deleted — only when stream is fully acked.
1058
+ */
1059
+ function get_stream_chunks(streamId, maxBytes) {
1060
+ var stream = context.send_streams[streamId];
1061
+ if (!stream || !stream.pending_data || stream.pending_data.byteLength === 0) return [];
1062
+
1063
+ // Flow control: cap by remaining remote_max_data budget
1064
+ var fcBudget = context.remote_max_data - context.bytes_sent;
1065
+ if (fcBudget <= 0) return []; // blocked by flow control
1066
+ maxBytes = Math.min(maxBytes, fcBudget);
1067
+
1068
+ var data = stream.pending_data;
1069
+ var baseOffset = stream.pending_offset_start;
1070
+ var totalSize = stream.total_size > 0 ? stream.total_size : stream.write_offset;
1071
+
1072
+ // Build "known sent" = acked + in_flight (don't resend what's already in transit)
1073
+ var knownSent = stream.acked_ranges.slice(); // copy
1074
+ if (stream.in_flight_ranges) {
1075
+ for (var pn in stream.in_flight_ranges) {
1076
+ flat_ranges.add(knownSent, stream.in_flight_ranges[pn]);
1077
+ }
1078
+ }
1079
+
1080
+ // What truly needs sending? = invert of knownSent over [0, totalSize)
1081
+ var missing = flat_ranges.invert(knownSent, 0, totalSize);
1082
+ if (!missing || missing.length === 0) return [];
1083
+
1084
+ var sendOffset = stream.send_offset;
1085
+ var chunks = [];
1086
+ var used = 0;
1087
+
1088
+ // Phase 1: forward from send_offset (new data, priority)
1089
+ for (var i = 0; i < missing.length; i += 2) {
1090
+ if (used >= maxBytes) break;
1091
+ var mFrom = missing[i];
1092
+ var mTo = missing[i + 1];
1093
+
1094
+ // Only ranges at or after send_offset
1095
+ if (mTo <= sendOffset) continue;
1096
+ var from = Math.max(mFrom, sendOffset);
1097
+ var to = mTo;
1098
+
1099
+ // Clamp to budget
1100
+ var len = Math.min(to - from, maxBytes - used);
1101
+ if (len <= 0) continue;
1102
+
1103
+ // Clamp to buffer bounds
1104
+ var relStart = from - baseOffset;
1105
+ var relEnd = relStart + len;
1106
+ if (relStart < 0 || relStart >= data.byteLength) continue;
1107
+ if (relEnd > data.byteLength) { relEnd = data.byteLength; len = relEnd - relStart; }
1108
+ if (len <= 0) continue;
1109
+
1110
+ chunks.push({ offset: from, data: data.slice(relStart, relEnd) });
1111
+ used += len;
1112
+
1113
+ // Advance send_offset
1114
+ if (from + len > stream.send_offset) {
1115
+ stream.send_offset = from + len;
1116
+ }
1117
+ }
1118
+
1119
+ // Phase 2: fill gaps before send_offset (retransmit lost data)
1120
+ // First update knownSent with what Phase 1 just yielded
1121
+ if (chunks.length > 0) {
1122
+ for (var ci = 0; ci < chunks.length; ci++) {
1123
+ flat_ranges.add(knownSent, [chunks[ci].offset, chunks[ci].offset + chunks[ci].data.byteLength]);
1124
+ }
1125
+ missing = flat_ranges.invert(knownSent, 0, totalSize);
1126
+ }
1127
+ if (used < maxBytes) {
1128
+ for (var i = 0; i < missing.length; i += 2) {
1129
+ if (used >= maxBytes) break;
1130
+ var mFrom = missing[i];
1131
+ var mTo = missing[i + 1];
1132
+
1133
+ // Only ranges before send_offset
1134
+ if (mFrom >= sendOffset) break;
1135
+ var to = Math.min(mTo, sendOffset);
1136
+
1137
+ var len = Math.min(to - mFrom, maxBytes - used);
1138
+ if (len <= 0) continue;
1139
+
1140
+ var relStart = mFrom - baseOffset;
1141
+ var relEnd = relStart + len;
1142
+ if (relStart < 0 || relStart >= data.byteLength) continue;
1143
+ if (relEnd > data.byteLength) { relEnd = data.byteLength; len = relEnd - relStart; }
1144
+ if (len <= 0) continue;
1145
+
1146
+ chunks.push({ offset: mFrom, data: data.slice(relStart, relEnd) });
1147
+ used += len;
1148
+ }
1149
+ }
1150
+
1151
+ // Trim front of pending_data if beginning is fully acked (optimization)
1152
+ if (stream.acked_ranges.length >= 2 && stream.acked_ranges[0] === 0 && stream.acked_ranges[1] > baseOffset) {
1153
+ var trimTo = stream.acked_ranges[1];
1154
+ var trimBytes = trimTo - baseOffset;
1155
+ if (trimBytes > 0 && trimBytes < data.byteLength) {
1156
+ stream.pending_data = data.slice(trimBytes);
1157
+ stream.pending_offset_start = trimTo;
1158
+ }
1159
+ }
1160
+
1161
+ return chunks;
1162
+ }
1163
+
1164
+
1165
+ // ============================================================
1166
+ // Close
1167
+ // ============================================================
1168
+
1169
+ function close(errorCode, reason) {
1170
+ if (context.state === 'closed' || context.state === 'draining') return;
1171
+ if (context.handshake_timer) { clearTimeout(context.handshake_timer); context.handshake_timer = null; }
1172
+ sendFrames('app', [{
1173
+ type: 'connection_close', application: false,
1174
+ error: errorCode || 0, frameType: 0, reason: reason || ''
1175
+ }]);
1176
+ set_context({ state: 'draining' });
1177
+ }
1178
+
1179
+
1180
+ // ============================================================
1181
+ // Client connect (Phase 7)
1182
+ // ============================================================
1183
+
1184
+ function connect() {
1185
+ if (context.isServer) return; // server waits for feedPacket, doesn't connect
1186
+
1187
+ // Generate random DCID (8 bytes)
1188
+ var dcid = new Uint8Array(8);
1189
+ for (var i = 0; i < 8; i++) dcid[i] = Math.floor(Math.random() * 256);
1190
+
1191
+ // Generate random SCID (8 bytes)
1192
+ var scid = new Uint8Array(8);
1193
+ for (var i = 0; i < 8; i++) scid[i] = Math.floor(Math.random() * 256);
1194
+
1195
+ context.original_dcid = dcid;
1196
+ context.my_cids.push(scid);
1197
+
1198
+ // Client initial keys: SWAPPED from server
1199
+ // Client writes with "client in" keys (direction='read' in our function)
1200
+ // Client reads with "server in" keys (direction='write' in our function)
1201
+ set_context({
1202
+ initial_write: quic_derive_init_secrets(dcid, context.version, 'read'),
1203
+ initial_read: quic_derive_init_secrets(dcid, context.version, 'write')
1204
+ });
1205
+
1206
+ // Initialize TLS (client mode)
1207
+ console.log('[quic] client connecting, dcid=' + Array.from(dcid).map(function(b){ return b.toString(16).padStart(2,'0'); }).join(''));
1208
+ initTLS();
1209
+
1210
+ // Trigger ClientHello by feeding empty message
1211
+ tls.feedMessage(new Uint8Array(0));
1212
+ }
1213
+
1214
+
1215
+ // ============================================================
1216
+ // Public API
1217
+ // ============================================================
1218
+
1219
+ var api = {
1220
+ context: context,
1221
+ on: function (name, fn) { ev.on(name, fn); },
1222
+ off: function (name, fn) { ev.off(name, fn); },
1223
+ feedDatagram: feedDatagram,
1224
+ sendStream: sendStream,
1225
+ sendDatagram: sendDatagram,
1226
+ close: close,
1227
+ connect: connect,
1228
+ set_context: set_context,
1229
+ get state() { return context.state; }
1230
+ };
1231
+
1232
+ for (var k in api) {
1233
+ if (Object.prototype.hasOwnProperty.call(api, k)) this[k] = api[k];
1234
+ }
1235
+ return this;
1236
+ }
1237
+
1238
+ export { QUICConnection };