quico 0.4.0 → 0.7.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,618 @@
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
+ // ============================================================
16
+ // A WebTransport stream, as a Node stream — one for both sides
17
+ // ============================================================
18
+ //
19
+ // The server (h3_server) and the client (webtransport.js) build their
20
+ // streams here, so they behave alike: a Duplex for a bidirectional stream,
21
+ // a Readable for the peer's unidirectional one ('uni-in'), a Writable for
22
+ // ours ('uni-out'). The client had classes of its own, which had none of
23
+ // what follows.
24
+ //
25
+ // Reading: what the peer sends is credited back to it as the application
26
+ // reads (_read), not as it arrives — as for a request body. The peer can be
27
+ // one stream window ahead of the reader, no more; a stream nobody read was
28
+ // taken in whole.
29
+ //
30
+ // Each direction can be given up on its own, with a code of the
31
+ // application's (draft-ietf-webtrans-http3 §4.4, mapped by wtErrorToH3):
32
+ // stream.resetStream(code) — stop sending: RESET_STREAM; what was written
33
+ // and not yet sent is dropped;
34
+ // stream.stopSending(code) — stop reading: STOP_SENDING; the peer stops.
35
+ // destroy() with a direction unfinished does the same with code 0 — the peer
36
+ // is told, as a browser is when a WebTransport stream is aborted or
37
+ // cancelled. It was told nothing: what was queued kept going out, and the
38
+ // peer waited for a FIN that would not come.
39
+ //
40
+ // The peer giving up on a direction (RESET_STREAM or STOP_SENDING, through
41
+ // stream._peerCancelled) ends that direction: 'peerReset' / 'peerStopSending'
42
+ // with the application's code (stream.streamErrorCode; null when the peer
43
+ // sent none). The other direction of a bidirectional stream goes on; the
44
+ // stream closes ('close') once both are over — with an Error carrying the
45
+ // code only to an 'error' listener, since a cancelled stream is ordinary
46
+ // traffic, not a crash.
47
+ //
48
+ // And the browser's shape too: stream.readable and stream.writable are WHATWG
49
+ // ReadableStream / WritableStream views of it, as a browser's
50
+ // WebTransportBidirectionalStream has them — so code written against the
51
+ // browser API runs here: getReader(), getWriter(), pipeTo(); reader.cancel()
52
+ // and writer.abort() with a WebTransportError({ streamErrorCode }) send
53
+ // STOP_SENDING / RESET_STREAM with that code; a stream the peer reset or
54
+ // stopped fails reads or writes with a WebTransportError carrying its code.
55
+ // The reader reads only when asked, so one that does not read holds the peer
56
+ // back as a Node consumer does (the client's classes read with on('data'),
57
+ // which drained the stream whatever the reader did). Node's own converters
58
+ // (Duplex.toWeb) would not do for a bidirectional stream: their reader ends
59
+ // only once our side has finished writing too, where the browser's ends when
60
+ // the peer finishes. Node's stream utilities — pipe(), finished(),
61
+ // pipeline() — are unaffected by the two names.
62
+
63
+ import { Duplex, Readable, Writable } from 'node:stream';
64
+ import { wtErrorToH3, h3ErrorToWt } from './h3.js';
65
+ import { wt_stream_header, creditAsRead } from './utils.js';
66
+
67
+ // quic: the QUIC connection. kind: 'bidi', 'uni-in' or 'uni-out'. onClose:
68
+ // called once, when the stream is done with — for the owner's bookkeeping.
69
+ // prefixLength: the bytes of WebTransport prefix we sent on it ourselves (on a
70
+ // stream we opened), which are not the application's in its statistics.
71
+ // sched: its session's wtScheduling (below) — where its sending part goes in
72
+ // QUIC's scheduler. sessionId: its session's (the CONNECT stream's) ID.
73
+ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, sessionId) {
74
+ var sends = kind !== 'uni-in', reads = kind !== 'uni-out';
75
+ var sendDone = !sends; // the FIN queued, or the direction reset
76
+ var recvDone = !reads; // the FIN received, or reading stopped
77
+ var finSent = false;
78
+ // For getStats(), as the browser's streams report them.
79
+ var stats = { bytesWritten: 0, bytesSent: 0, bytesAcknowledged: 0, bytesReceived: 0 };
80
+ prefixLength = prefixLength || 0;
81
+
82
+ function write(chunk, encoding, cb) {
83
+ if (sendDone) { cb(); return; } // reset: nothing more goes out
84
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
85
+ stats.bytesWritten += chunk.byteLength;
86
+ quic.sendStream(wtStreamId, new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), false);
87
+ quic.whenStreamSent(wtStreamId, stream.writableHighWaterMark, cb); // backpressure
88
+ }
89
+ // The FIN queued; 'committed' once it and every byte are acknowledged.
90
+ function final(cb) {
91
+ if (!sendDone) {
92
+ sendDone = true; finSent = true;
93
+ quic.sendStream(wtStreamId, new Uint8Array(0), true);
94
+ if (quic.whenStreamCommitted) quic.whenStreamCommitted(wtStreamId, function () {
95
+ if (sendReset || stream._writeStopped) return;
96
+ committed = true;
97
+ stream.emit('committed');
98
+ });
99
+ }
100
+ cb();
101
+ }
102
+ var committed = false;
103
+
104
+ // What QUIC has sent and had acknowledged of it, in the application's bytes.
105
+ // A stream whose every byte and FIN were acknowledged is gone from QUIC's
106
+ // table — all of it, then.
107
+ function sendStats() {
108
+ var st = quic.context && quic.context.send_streams[wtStreamId];
109
+ if (st) {
110
+ stats.bytesSent = Math.max(stats.bytesSent, Math.min(stats.bytesWritten, st.max_sent_offset - prefixLength));
111
+ var acked = st.acked_ranges.length >= 2 && st.acked_ranges[0] === 0 ? st.acked_ranges[1] - prefixLength : 0;
112
+ stats.bytesAcknowledged = Math.max(stats.bytesAcknowledged, Math.min(stats.bytesWritten, acked));
113
+ } else if (finSent) {
114
+ stats.bytesSent = stats.bytesAcknowledged = stats.bytesWritten;
115
+ }
116
+ return { bytesWritten: stats.bytesWritten, bytesSent: stats.bytesSent, bytesAcknowledged: stats.bytesAcknowledged };
117
+ }
118
+ function recvStats() {
119
+ return { bytesReceived: stats.bytesReceived, bytesRead: stats.bytesReceived - (stream.readableLength || 0) };
120
+ }
121
+
122
+ // The browser's send order and group (WebTransportSendStream), handed to
123
+ // QUIC's scheduler as they change (see wtScheduling).
124
+ var sendOrder = 0, sendGroup = null;
125
+ function sendProps(target) {
126
+ Object.defineProperty(target, 'sendOrder', { configurable: true, enumerable: true,
127
+ get: function () { return sendOrder; }, set: function (v) { sendOrder = Number(v) || 0; schedule(); } });
128
+ Object.defineProperty(target, 'sendGroup', { configurable: true, enumerable: true,
129
+ get: function () { return sendGroup; }, set: function (g) {
130
+ if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
131
+ sendGroup = g || null;
132
+ if (sendGroup && sendGroup._add) sendGroup._add(sendStats);
133
+ schedule();
134
+ } });
135
+ }
136
+ function schedule() {
137
+ if (sends && sched && !sendDone) quic.setStreamPriority(wtStreamId, sched.forStream(sendGroup, sendOrder));
138
+ }
139
+ schedule();
140
+
141
+ var stream = kind === 'bidi' ? new Duplex({ read: function () {}, write: write, final: final })
142
+ : kind === 'uni-in' ? new Readable({ read: function () {} })
143
+ : new Writable({ write: write, final: final });
144
+
145
+ stream._streamId = wtStreamId;
146
+ stream._sessionId = sessionId;
147
+
148
+ // The code what is still open goes with when the stream is destroyed:
149
+ // destroy(err) with err.streamErrorCode — the browser's
150
+ // WebTransportError({ streamErrorCode }) — that application code;
151
+ // its session gone (_sessionGone) — WT_SESSION_GONE, an HTTP/3 code of
152
+ // its own, not an application one (draft-ietf-webtrans-http3 §4.6:
153
+ // "MUST reset ... with WT_SESSION_GONE");
154
+ // otherwise application code 0.
155
+ // destroy() always sent code 0: the application's code was dropped, and a
156
+ // session's end reached its streams as an ordinary cancel.
157
+ var closeH3Code = null;
158
+ var WT_SESSION_GONE = 0x170d7b68;
159
+ // At once, not when 'close' comes a tick later: the session's end is
160
+ // often the connection's (a WebTransport client's own), whose
161
+ // CONNECTION_CLOSE would go first and these with it.
162
+ stream._sessionGone = function () {
163
+ if (closeH3Code === null) closeH3Code = WT_SESSION_GONE;
164
+ if (!sendDone) { sendDone = true; sendReset = true; resetSending(closeH3Code, 0); }
165
+ if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, closeH3Code); }
166
+ };
167
+ var baseDestroy = stream.destroy;
168
+ stream.destroy = function (err) {
169
+ if (closeH3Code === null && err && typeof err.streamErrorCode === 'number') closeH3Code = wtErrorToH3(err.streamErrorCode);
170
+ return baseDestroy.apply(this, arguments);
171
+ };
172
+ stream.streamErrorCode = null;
173
+ // Its QUIC stream ID and its session's — read-only, for logs and maps.
174
+ Object.defineProperty(stream, 'id', { enumerable: true, get: function () { return wtStreamId; } });
175
+ Object.defineProperty(stream, 'sessionId', { enumerable: true, get: function () { return sessionId; } });
176
+
177
+ // The WHATWG views (see above), made on first use — with what the
178
+ // browser's WebTransportReceiveStream / WebTransportSendStream add.
179
+ var webReadable = null, webWritable = null;
180
+ if (reads) {
181
+ Object.defineProperty(stream, 'readable', { configurable: true, get: function () {
182
+ if (!webReadable) {
183
+ webReadable = webReadableOf(stream);
184
+ webReadable.getStats = function () { return Promise.resolve(recvStats()); };
185
+ }
186
+ return webReadable;
187
+ } });
188
+ }
189
+ if (sends) {
190
+ sendProps(stream);
191
+ Object.defineProperty(stream, 'writable', { configurable: true, get: function () {
192
+ if (!webWritable) {
193
+ webWritable = webWritableOf(stream);
194
+ sendProps(webWritable);
195
+ webWritable.getStats = function () { return Promise.resolve(sendStats()); };
196
+ }
197
+ return webWritable;
198
+ } });
199
+ }
200
+ stream.getStats = function () {
201
+ return Promise.resolve(Object.assign({}, sends ? sendStats() : {}, reads ? recvStats() : {}));
202
+ };
203
+
204
+ // Credit for what the application has read (utils' creditAsRead): a
205
+ // stream nobody reads holds one window, not a window and Node's buffer.
206
+ var received = reads ? creditAsRead(stream, function (n) { quic.creditStream(wtStreamId, n); }) : null;
207
+ stream._pushData = function (data) {
208
+ if (recvDone) return;
209
+ if (data instanceof Uint8Array) data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
210
+ stats.bytesReceived += data.byteLength;
211
+ received(data.byteLength);
212
+ stream.push(data);
213
+ };
214
+ stream._endData = function () {
215
+ if (recvDone) return;
216
+ recvDone = true;
217
+ stream.push(null);
218
+ };
219
+
220
+ // The stream closes once both directions are over — each ended by a FIN
221
+ // (sent, or received and read to the end), a reset, or a stop, by either
222
+ // side, in any order. That was checked at some of those points and not
223
+ // others: a stream whose reading we had stopped stayed open for good once
224
+ // its writing finished after.
225
+ var readStopped = false; // we stopped reading it (stopSending)
226
+ var sendReset = false; // we reset it (resetStream; or the stream went with its direction open)
227
+ stream._readReset = false; // the peer reset it (RESET_STREAM)
228
+ stream._writeStopped = false; // the peer stopped reading it (STOP_SENDING)
229
+
230
+ // Each direction's state, read-only — null for the one a unidirectional
231
+ // stream does not have. A 'drain' says the writer may write; sendState
232
+ // says whether what it writes still goes anywhere.
233
+ // sendState: 'open', 'finished' (our FIN is queued), 'reset' (we gave up
234
+ // sending), 'stopped' (the peer asked us to stop: STOP_SENDING)
235
+ // recvState: 'open', 'finished' (the peer's FIN arrived), 'reset' (the
236
+ // peer gave up sending: RESET_STREAM), 'stopped' (we stopped
237
+ // reading: stopSending)
238
+ Object.defineProperty(stream, 'sendState', { enumerable: true, get: function () {
239
+ if (!sends) return null;
240
+ return stream._writeStopped ? 'stopped' : sendReset ? 'reset' : committed ? 'committed' : finSent ? 'finished' : 'open';
241
+ } });
242
+ Object.defineProperty(stream, 'recvState', { enumerable: true, get: function () {
243
+ if (!reads) return null;
244
+ return stream._readReset ? 'reset' : readStopped ? 'stopped' : recvDone ? 'finished' : 'open';
245
+ } });
246
+ function readOver() { return !reads || readStopped || stream._readReset || stream.readableEnded; }
247
+ function writeOver() { return !sends || sendDone; }
248
+ function closeIfDone(err) {
249
+ if (!stream.destroyed && readOver() && writeOver()) stream.destroy(err || null);
250
+ }
251
+ if (reads) stream.on('end', function () { closeIfDone(); });
252
+ if (sends) stream.on('finish', function () { closeIfDone(); });
253
+
254
+ // options.reliableSize: the first bytes the application wrote that still
255
+ // reach the peer, whole, before the reset (RESET_STREAM_AT, draft-ietf-
256
+ // quic-reliable-stream-reset) — a MOQT subgroup's header, say. More than
257
+ // was written: what was written. Only with a peer that takes
258
+ // RESET_STREAM_AT (stream.reliableResetSupported); without, a RESET_STREAM.
259
+ //
260
+ // After end() too (RFC 9000 §3.1: "Data Sent" → "Reset Sent"), for as long
261
+ // as what was written is not all acknowledged: a MOQT subgroup past its
262
+ // delivery timeout, ended but still on its way, is dropped instead of sent
263
+ // to the end. It did nothing once end() had been called — the FIN queued
264
+ // counted as the direction being over — and the stream went on being sent
265
+ // and resent until it had all arrived. Everything acknowledged already:
266
+ // nothing to reset (false). Returns whether a reset was sent.
267
+ stream.resetStream = function (code, options) {
268
+ if (!sends || sendReset || stream._writeStopped) return false;
269
+ var reliable = options && options.reliableSize != null ? Number(options.reliableSize) : 0;
270
+ if (!(reliable >= 0) || Math.floor(reliable) !== reliable) throw new RangeError('reliableSize must be a non-negative integer, got ' + (options && options.reliableSize));
271
+ var wasOpen = !sendDone;
272
+ if (!resetSending(wtErrorToH3(code || 0), reliable) && !wasOpen) return false; // all delivered
273
+ sendDone = true;
274
+ sendReset = true;
275
+ if (stream._onClosePending) stream._onClosePending(true); // given up before it was open
276
+ if (wasOpen) closeIfDone();
277
+ return true;
278
+ };
279
+ // The sending part reset. Our stream's header (signal value and session
280
+ // ID) is always delivered (draft-ietf-webtrans-http3 §4.4: "MUST use the
281
+ // RESET_STREAM_AT frame with a Reliable Size set to at least the size of
282
+ // the WebTransport header") — without it the peer cannot tell which
283
+ // session the reset is for — and `reliable` bytes of the application's
284
+ // after it. QUIC sends RESET_STREAM_AT when the peer takes it, RESET_STREAM
285
+ // otherwise.
286
+ function resetSending(h3code, reliable) {
287
+ return quic.resetStream(wtStreamId, h3code, prefixLength + (reliable || 0));
288
+ }
289
+ Object.defineProperty(stream, 'reliableResetSupported', { enumerable: true,
290
+ get: function () { return !!quic.resetStreamAtSupported; } });
291
+ stream.stopSending = function (code) {
292
+ if (recvDone) return;
293
+ recvDone = true;
294
+ readStopped = true;
295
+ quic.stopSending(wtStreamId, wtErrorToH3(code || 0)); // and its held credit goes
296
+ closeIfDone();
297
+ };
298
+
299
+ // The peer gave up on one direction, with the application's code:
300
+ // RESET_STREAM ends our reading, STOP_SENDING our sending (QUIC has
301
+ // answered that one with RESET_STREAM already). The other direction goes
302
+ // on, as in the browser — a bidirectional stream's two halves are
303
+ // independent; the peer giving up on one used to destroy the stream, and
304
+ // reset the half it had not touched. 'peerReset' / 'peerStopSending' (code)
305
+ // say which ended, stream.streamErrorCode holds the code, and the stream
306
+ // closes once both directions are over: at once when the other one already
307
+ // is — then with an Error carrying the code, for an 'error' listener only
308
+ // (a cancelled stream is ordinary traffic, not a crash).
309
+ //
310
+ // Reading reset: what was read stands, and the readable side ends (its
311
+ // WHATWG view fails with the code instead — a reset is not a FIN). Sending
312
+ // stopped: writes from then on are dropped (the WHATWG view's fail).
313
+ stream._peerCancelled = function (how, h3code) {
314
+ if (stream.destroyed) return;
315
+ var stop = how === 'stop_sending';
316
+ if (stop ? stream._writeStopped : stream._readReset) return;
317
+ // The RESET_STREAM that answers our own STOP_SENDING (RFC 9000 §3.5): our
318
+ // doing, not the peer giving up — reported as 'peerReset' it was.
319
+ if (!stop && readStopped) return;
320
+ stream.streamErrorCode = h3ErrorToWt(h3code);
321
+ if (stop) { sendDone = true; stream._writeStopped = true; }
322
+ else { recvDone = true; stream._readReset = true; }
323
+ stream.emit(stop ? 'peerStopSending' : 'peerReset', stream.streamErrorCode);
324
+ if (readOver() && writeOver()) {
325
+ var err = new Error(stop ? 'the peer stopped reading this stream' : 'the peer reset this stream');
326
+ err.code = stop ? 'ERR_WEBTRANSPORT_STOP_SENDING' : 'ERR_WEBTRANSPORT_STREAM_RESET';
327
+ err.streamErrorCode = stream.streamErrorCode;
328
+ closeIfDone(stream.listenerCount('error') > 0 ? err : null);
329
+ } else if (!stop) {
330
+ stream.push(null);
331
+ }
332
+ };
333
+
334
+ stream.once('close', function () {
335
+ if (sends) sendStats(); // the last figures, while QUIC still has them
336
+ if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
337
+ var code = closeH3Code !== null ? closeH3Code : wtErrorToH3(0);
338
+ if (!sendDone) { sendDone = true; sendReset = true; resetSending(code, 0); }
339
+ if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, code); }
340
+ // Still waiting to be opened (openWtStream): given up if its sending was
341
+ // cut short; ended normally (end() before it opened), its bytes still go
342
+ // once it opens.
343
+ if (stream._onClosePending) stream._onClosePending(sendReset);
344
+ quic.releaseStreamCredit(wtStreamId);
345
+ if (onClose) onClose();
346
+ });
347
+
348
+ return stream;
349
+ }
350
+
351
+ // ---- The WHATWG views ----
352
+
353
+ // The browser's WebTransportError: what a stream's reads and writes fail with
354
+ // when the peer reset or stopped it (source 'stream', its streamErrorCode),
355
+ // and what an application passes to cancel() / abort() to send a code.
356
+ //
357
+ // Takes either form: the spec's (message, { source, streamErrorCode }) or the
358
+ // one browsers implement, a single { message, source, streamErrorCode }.
359
+ class WebTransportError extends Error {
360
+ constructor(message, options) {
361
+ if (message && typeof message === 'object') { options = message; message = message.message; }
362
+ super(message || '');
363
+ this.name = 'WebTransportError';
364
+ this.source = (options && options.source) || 'stream';
365
+ this.streamErrorCode = options && options.streamErrorCode != null ? options.streamErrorCode : null;
366
+ }
367
+ }
368
+
369
+ // The code a cancel() or abort() reason carries; 0 for any other reason.
370
+ function codeOf(reason) {
371
+ return reason && typeof reason.streamErrorCode === 'number' ? reason.streamErrorCode : 0;
372
+ }
373
+
374
+ // Why the stream ended early: the peer's code if it gave one.
375
+ function endedError(stream) {
376
+ return new WebTransportError(stream.streamErrorCode != null ? 'the peer gave up on this stream' : 'stream closed',
377
+ { streamErrorCode: stream.streamErrorCode });
378
+ }
379
+
380
+ function webReadableOf(stream) {
381
+ return new ReadableStream({
382
+ pull: function (controller) {
383
+ return new Promise(function (resolve) {
384
+ function attempt() {
385
+ var chunk = stream.read();
386
+ if (chunk !== null) { controller.enqueue(new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)); return done(); }
387
+ if (stream._readReset) { controller.error(endedError(stream)); return done(); }
388
+ if (stream.readableEnded) { controller.close(); return done(); }
389
+ if (stream.destroyed) { controller.error(endedError(stream)); return done(); }
390
+ return false;
391
+ }
392
+ function done() {
393
+ stream.removeListener('readable', attempt);
394
+ stream.removeListener('end', attempt);
395
+ stream.removeListener('close', attempt);
396
+ resolve();
397
+ return true;
398
+ }
399
+ if (attempt()) return;
400
+ stream.on('readable', attempt);
401
+ stream.on('end', attempt);
402
+ stream.on('close', attempt);
403
+ });
404
+ },
405
+ cancel: function (reason) { stream.stopSending(codeOf(reason)); }
406
+ }, { highWaterMark: 0 }); // read only when the reader asks: backpressure
407
+ }
408
+
409
+ function webWritableOf(stream) {
410
+ return new WritableStream({
411
+ write: function (chunk) {
412
+ return new Promise(function (resolve, reject) {
413
+ if (stream.destroyed || stream._writeStopped) { reject(endedError(stream)); return; }
414
+ stream.write(Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength), function (err) {
415
+ if (err || stream.destroyed || stream._writeStopped) reject(endedError(stream)); else resolve();
416
+ });
417
+ });
418
+ },
419
+ close: function () {
420
+ return new Promise(function (resolve, reject) {
421
+ if (stream.destroyed || stream._writeStopped) { reject(endedError(stream)); return; }
422
+ stream.end(resolve);
423
+ });
424
+ },
425
+ abort: function (reason) { stream.resetStream(codeOf(reason)); }
426
+ });
427
+ }
428
+
429
+ // ---- Opening a stream of our own ----
430
+ //
431
+ // The browser's way, for both sides (the server's res.create…Stream, the
432
+ // client's wt.create…Stream): options { sendOrder, sendGroup,
433
+ // waitUntilAvailable }. Without waitUntilAvailable, a stream past the peer's
434
+ // MAX_STREAMS is refused at once — null here; the browser rejects — rather
435
+ // than opened to wait unseen; with it, it is opened and its bytes wait for
436
+ // the peer to allow more. make(streamId, kind, prefixLength) builds the
437
+ // stream object, recorded by its owner.
438
+ //
439
+ // Opened is not open yet: past the peer's limit the stream waits for it to
440
+ // allow more. stream.pending says so (as net.Socket's does) until it is
441
+ // open; then 'ready', and callback(null, stream) — or callback(err) if it
442
+ // closes first (its session or connection did). Both after this returns,
443
+ // as Node's are, whether or not it had to wait. The client's Promise is
444
+ // settled from the same callback: resolved once the stream is open, as the
445
+ // browser's is — it was resolved at once, open or not.
446
+ function openWtStream(h3, quic, sessionId, bidi, options, make, callback) {
447
+ options = options || {};
448
+ if (!options.waitUntilAvailable && !quic.canOpenStream(bidi)) return null;
449
+ var streamId = h3.createWebTransportStream(sessionId, bidi);
450
+ var stream = make(streamId, bidi ? 'bidi' : 'uni-out', wt_stream_header(bidi, sessionId).byteLength);
451
+ if (options.sendGroup) stream.sendGroup = options.sendGroup;
452
+ if (options.sendOrder != null) stream.sendOrder = options.sendOrder;
453
+ stream.pending = true;
454
+ var settled = false;
455
+ function settle(err) {
456
+ if (settled) return;
457
+ settled = true;
458
+ stream._onClosePending = null;
459
+ stream.pending = false;
460
+ if (err) { err = new Error('the stream closed before it was open'); err.code = 'ERR_WEBTRANSPORT_STREAM_NOT_OPEN'; }
461
+ process.nextTick(function () {
462
+ if (!err) stream.emit('ready');
463
+ if (typeof callback === 'function') callback(err || null, stream);
464
+ });
465
+ }
466
+ stream._onClosePending = function (aborted) { if (aborted) settle(true); };
467
+ quic.whenStreamPermitted(streamId, settle);
468
+ return stream;
469
+ }
470
+
471
+ // ---- A session's place in QUIC's scheduler ----
472
+ //
473
+ // The browser's model (W3C WebTransport, sendGroup / sendOrder): the streams
474
+ // of a send group share the group's turns, the highest sendOrder with
475
+ // something to send taking them, equal orders taking turns; the groups share
476
+ // the session's sending equally. A stream in no group is in its session's own
477
+ // group. QUIC's scheduler has exactly that as `group` and `order`
478
+ // (setStreamPriority); this maps one onto the other. Every stream and group
479
+ // of the session is at the urgency of its CONNECT stream — RFC 9218's
480
+ // `priority` on the CONNECT request, or what the application set for it —
481
+ // taken as each stream is placed.
482
+ //
483
+ // Datagrams go first by default, ahead of any stream (sendDatagram). Sent
484
+ // with a sendGroup or a sendOrder other than 0, they take their place among
485
+ // the streams instead, as the browser's createWritable({ sendOrder,
486
+ // sendGroup }) has it: a member of that group (or of the session's), at
487
+ // that order.
488
+ function wtScheduling(quic, sessionStreamId) {
489
+ var sessionGroup = {};
490
+ function urgency() {
491
+ var p = quic.getStreamPriority(sessionStreamId);
492
+ return p && p.urgency != null ? p.urgency : 3;
493
+ }
494
+ return {
495
+ forStream: function (group, order) {
496
+ return { urgency: urgency(), incremental: true, group: group || sessionGroup, order: Number(order) || 0 };
497
+ },
498
+ forDatagram: function (options) {
499
+ if (!options || (!options.sendGroup && !Number(options.sendOrder))) return null;
500
+ return { urgency: urgency(), group: options.sendGroup || sessionGroup, order: Number(options.sendOrder) || 0 };
501
+ }
502
+ };
503
+ }
504
+
505
+ // ---- A session's datagrams and statistics, for both sides ----
506
+ //
507
+ // The client's WebTransport and the server's res share these, so a session
508
+ // looks the same from either end. `session` is what they hand in:
509
+ // quic the QUIC connection (a getter on the client:
510
+ // made after the session object)
511
+ // connected() whether the session is up
512
+ // send(u8, options) its sendDatagram (options: sendOrder, sendGroup)
513
+ // maxDatagramSize() the largest payload it can send
514
+ // owner { maxAge, expired } — its datagrams in QUIC's
515
+ // queue: their age limit (outgoingMaxAge) and how
516
+ // many were dropped unsent (sendDatagram's owner)
517
+ // dgStats { droppedIncoming, expiredIncoming }
518
+
519
+ // The browser's WebTransportDatagramDuplexStream: readable, writable,
520
+ // createWritable({ sendOrder, sendGroup }), maxDatagramSize, the age limits
521
+ // and high-water marks. The readable reads only when asked; past
522
+ // incomingHighWaterMark the oldest goes, and one older than incomingMaxAge
523
+ // when read is skipped. outgoingMaxAge is this session's alone (a session
524
+ // on a shared connection used to set the whole connection's).
525
+ function createDatagrams(session) {
526
+ var queue = [], controller = null, pull = null, readable = null, writable = null;
527
+ var dg = { incomingMaxAge: null, incomingHighWaterMark: 256, outgoingHighWaterMark: 256 };
528
+
529
+ function deliver() {
530
+ if (!pull) return;
531
+ var now = Date.now();
532
+ while (queue.length > 0) {
533
+ var d = queue.shift();
534
+ if (dg.incomingMaxAge != null && now - d.t > dg.incomingMaxAge) { session.dgStats.expiredIncoming++; continue; }
535
+ controller.enqueue(d.data);
536
+ var p = pull; pull = null; p();
537
+ return;
538
+ }
539
+ }
540
+ function makeWritable(options) {
541
+ var w = new WritableStream({
542
+ write: function (chunk) {
543
+ if (!session.connected()) throw new WebTransportError('the session is not connected', { source: 'session' });
544
+ var u8 = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
545
+ if (u8.byteLength <= session.maxDatagramSize()) session.send(u8, w); // its sendOrder / sendGroup, as they are now
546
+ }
547
+ }, { highWaterMark: dg.outgoingHighWaterMark });
548
+ w.sendOrder = (options && options.sendOrder) || 0;
549
+ w.sendGroup = (options && options.sendGroup) || null;
550
+ return w;
551
+ }
552
+
553
+ Object.defineProperty(dg, 'readable', { enumerable: true, get: function () {
554
+ if (!readable) {
555
+ readable = new ReadableStream({
556
+ start: function (c) { controller = c; },
557
+ pull: function () { return new Promise(function (resolve) { pull = resolve; deliver(); }); }
558
+ }, { highWaterMark: 0 });
559
+ }
560
+ return readable;
561
+ } });
562
+ Object.defineProperty(dg, 'writable', { enumerable: true, get: function () {
563
+ return writable || (writable = makeWritable());
564
+ } });
565
+ Object.defineProperty(dg, 'maxDatagramSize', { enumerable: true, get: function () { return session.maxDatagramSize(); } });
566
+ Object.defineProperty(dg, 'outgoingMaxAge', { enumerable: true,
567
+ get: function () { return session.owner.maxAge; },
568
+ set: function (v) { session.owner.maxAge = v == null ? null : Number(v); } });
569
+ dg.createWritable = function (options) { return makeWritable(options); };
570
+ // A datagram arrived: for the readable, once anything reads it.
571
+ dg._push = function (data) {
572
+ if (!readable) return;
573
+ queue.push({ data: data, t: Date.now() });
574
+ while (queue.length > dg.incomingHighWaterMark) { queue.shift(); session.dgStats.droppedIncoming++; }
575
+ deliver();
576
+ };
577
+ dg._close = function () {
578
+ queue = [];
579
+ if (controller) { try { controller.close(); } catch (e) {} }
580
+ if (pull) { var p = pull; pull = null; p(); }
581
+ };
582
+ return dg;
583
+ }
584
+
585
+ // The browser's getStats(): the connection's figures (QUIC's getStats — shared
586
+ // by every session on it, as in the browser), and the session's datagrams.
587
+ function sessionStats(session) {
588
+ var q = session.quic && session.quic.getStats ? session.quic.getStats() : {};
589
+ var out = { timestamp: Date.now() };
590
+ ['bytesSent', 'packetsSent', 'packetsLost', 'bytesReceived', 'packetsReceived', 'smoothedRtt', 'rttVariation',
591
+ 'minRtt', 'estimatedSendRate', 'atSendCapacity', 'congestionWindow', 'bytesInFlight'].forEach(function (k) {
592
+ out[k] = q[k] !== undefined ? q[k] : null;
593
+ });
594
+ out.datagrams = {
595
+ droppedIncoming: session.dgStats.droppedIncoming,
596
+ expiredIncoming: session.dgStats.expiredIncoming,
597
+ expiredOutgoing: session.owner.expired || 0,
598
+ lostOutgoing: null // a datagram's packet lost is not traced back to it
599
+ };
600
+ return Promise.resolve(out);
601
+ }
602
+
603
+ // The browser's WebTransportSendGroup: streams that share a send order space.
604
+ // getStats() sums its streams' figures.
605
+ function createSendGroup() {
606
+ var members = new Set();
607
+ return {
608
+ _add: function (f) { members.add(f); },
609
+ _remove: function (f) { members.delete(f); },
610
+ getStats: function () {
611
+ var t = { bytesWritten: 0, bytesSent: 0, bytesAcknowledged: 0 };
612
+ members.forEach(function (f) { var s = f(); t.bytesWritten += s.bytesWritten; t.bytesSent += s.bytesSent; t.bytesAcknowledged += s.bytesAcknowledged; });
613
+ return Promise.resolve(t);
614
+ }
615
+ };
616
+ }
617
+
618
+ export { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, WebTransportError };