quico 0.4.0 → 0.8.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,1075 @@
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
+ // A group of another session's (the browser's: InvalidStateError).
131
+ if (g != null && g._owner && stream._owner && g._owner !== stream._owner) {
132
+ throw new DOMException('this send group belongs to another WebTransport session', 'InvalidStateError');
133
+ }
134
+ if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
135
+ sendGroup = g || null;
136
+ if (sendGroup && sendGroup._add) sendGroup._add(sendStats);
137
+ schedule();
138
+ } });
139
+ }
140
+ function schedule() {
141
+ if (sends && sched && !sendDone) quic.setStreamPriority(wtStreamId, sched.forStream(sendGroup, sendOrder));
142
+ }
143
+ schedule();
144
+
145
+ var stream = kind === 'bidi' ? new Duplex({ read: function () {}, write: write, final: final })
146
+ : kind === 'uni-in' ? new Readable({ read: function () {} })
147
+ : new Writable({ write: write, final: final });
148
+
149
+ stream._streamId = wtStreamId;
150
+ stream._sessionId = sessionId;
151
+ if (kind === 'bidi') Object.defineProperty(stream, '_wtBidirectional', { value: true }); // instanceof WebTransportBidirectionalStream
152
+
153
+ // The code what is still open goes with when the stream is destroyed:
154
+ // destroy(err) with err.streamErrorCode — the browser's
155
+ // WebTransportError({ streamErrorCode }) — that application code;
156
+ // its session gone (_sessionGone) — WT_SESSION_GONE, an HTTP/3 code of
157
+ // its own, not an application one (draft-ietf-webtrans-http3 §4.6:
158
+ // "MUST reset ... with WT_SESSION_GONE");
159
+ // otherwise application code 0.
160
+ // destroy() always sent code 0: the application's code was dropped, and a
161
+ // session's end reached its streams as an ordinary cancel.
162
+ var closeH3Code = null;
163
+ var WT_SESSION_GONE = 0x170d7b68;
164
+ // At once, not when 'close' comes a tick later: the session's end is
165
+ // often the connection's (a WebTransport client's own), whose
166
+ // CONNECTION_CLOSE would go first and these with it.
167
+ //
168
+ // `err` is what the browser's views fail with (the WHATWG readable and
169
+ // writable): the session's own error — the very object its `closed`
170
+ // rejects with when the session was lost, an AbortError when we closed it
171
+ // (W3C WebTransport, "cleanup"). Without one, a WebTransportError, source
172
+ // 'session'. The views failed only on the next read or write, and a
173
+ // reader or writer waiting on `closed` waited for good.
174
+ stream._sessionGone = function (err) {
175
+ if (!stream._sessionError) stream._sessionError = err || new WebTransportError('the session is gone', { source: 'session' });
176
+ failViews(stream._sessionError);
177
+ if (closeH3Code === null) closeH3Code = WT_SESSION_GONE;
178
+ if (!sendDone) { sendDone = true; sendReset = true; resetSending(closeH3Code, 0); }
179
+ if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, closeH3Code); }
180
+ };
181
+ var baseDestroy = stream.destroy;
182
+ stream.destroy = function (err) {
183
+ if (closeH3Code === null && err && typeof err.streamErrorCode === 'number') closeH3Code = wtErrorToH3(err.streamErrorCode);
184
+ return baseDestroy.apply(this, arguments);
185
+ };
186
+ stream.streamErrorCode = null;
187
+ stream._bytesWritten = function () { return stats.bytesWritten; }; // a writer's commit()
188
+ // Its QUIC stream ID and its session's — read-only, for logs and maps.
189
+ Object.defineProperty(stream, 'id', { enumerable: true, get: function () { return wtStreamId; } });
190
+ Object.defineProperty(stream, 'sessionId', { enumerable: true, get: function () { return sessionId; } });
191
+
192
+ // The WHATWG views (see above), made on first use — with what the
193
+ // browser's WebTransportReceiveStream / WebTransportSendStream add.
194
+ //
195
+ // Each view fails at once when its direction is given up on — the peer's
196
+ // RESET_STREAM fails the readable, its STOP_SENDING the writable, the
197
+ // session's end both — so a reader or writer waiting on `closed` hears it,
198
+ // as in the browser; not only the next read or write. One made after that
199
+ // starts failed.
200
+ var webReadable = null, webWritable = null;
201
+ var readView = null, writeView = null; // { fail(err) } of each, once made
202
+ function failRead(err) { if (readView) readView.fail(err); }
203
+ function failWrite(err) { if (writeView) writeView.fail(err); }
204
+ function failViews(err) { failRead(err); failWrite(err); }
205
+ if (reads) {
206
+ Object.defineProperty(stream, 'readable', { configurable: true, get: function () {
207
+ if (!webReadable) {
208
+ readView = webReadableOf(stream, recvStats);
209
+ webReadable = readView.stream;
210
+ if (stream._sessionError) failRead(stream._sessionError);
211
+ else if (stream._readReset && !stream.readableLength) failRead(endedError(stream));
212
+ }
213
+ return webReadable;
214
+ } });
215
+ }
216
+ if (sends) {
217
+ sendProps(stream);
218
+ Object.defineProperty(stream, 'writable', { configurable: true, get: function () {
219
+ if (!webWritable) {
220
+ writeView = webWritableOf(stream, sendStats, quic, wtStreamId);
221
+ webWritable = writeView.stream;
222
+ sendProps(webWritable);
223
+ if (stream._sessionError) failWrite(stream._sessionError);
224
+ else if (stream._writeStopped) failWrite(endedError(stream));
225
+ }
226
+ return webWritable;
227
+ } });
228
+ }
229
+ stream.getStats = function () {
230
+ return Promise.resolve(Object.assign({}, sends ? sendStats() : {}, reads ? recvStats() : {}));
231
+ };
232
+
233
+ // Credit for what the application has read (utils' creditAsRead): a
234
+ // stream nobody reads holds one window, not a window and Node's buffer.
235
+ var received = reads ? creditAsRead(stream, function (n) { quic.creditStream(wtStreamId, n); }) : null;
236
+ stream._pushData = function (data) {
237
+ if (recvDone) return;
238
+ if (data instanceof Uint8Array) data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
239
+ stats.bytesReceived += data.byteLength;
240
+ received(data.byteLength);
241
+ stream.push(data);
242
+ };
243
+ stream._endData = function () {
244
+ if (recvDone) return;
245
+ recvDone = true;
246
+ stream.push(null);
247
+ };
248
+
249
+ // The stream closes once both directions are over — each ended by a FIN
250
+ // (sent, or received and read to the end), a reset, or a stop, by either
251
+ // side, in any order. That was checked at some of those points and not
252
+ // others: a stream whose reading we had stopped stayed open for good once
253
+ // its writing finished after.
254
+ var readStopped = false; // we stopped reading it (stopSending)
255
+ var sendReset = false; // we reset it (resetStream; or the stream went with its direction open)
256
+ stream._readReset = false; // the peer reset it (RESET_STREAM)
257
+ stream._writeStopped = false; // the peer stopped reading it (STOP_SENDING)
258
+
259
+ // Each direction's state, read-only — null for the one a unidirectional
260
+ // stream does not have. A 'drain' says the writer may write; sendState
261
+ // says whether what it writes still goes anywhere.
262
+ // sendState: 'open', 'finished' (our FIN is queued), 'reset' (we gave up
263
+ // sending), 'stopped' (the peer asked us to stop: STOP_SENDING)
264
+ // recvState: 'open', 'finished' (the peer's FIN arrived), 'reset' (the
265
+ // peer gave up sending: RESET_STREAM), 'stopped' (we stopped
266
+ // reading: stopSending)
267
+ Object.defineProperty(stream, 'sendState', { enumerable: true, get: function () {
268
+ if (!sends) return null;
269
+ return stream._writeStopped ? 'stopped' : sendReset ? 'reset' : committed ? 'committed' : finSent ? 'finished' : 'open';
270
+ } });
271
+ Object.defineProperty(stream, 'recvState', { enumerable: true, get: function () {
272
+ if (!reads) return null;
273
+ return stream._readReset ? 'reset' : readStopped ? 'stopped' : recvDone ? 'finished' : 'open';
274
+ } });
275
+ function readOver() { return !reads || readStopped || stream._readReset || stream.readableEnded; }
276
+ function writeOver() { return !sends || sendDone; }
277
+ function closeIfDone(err) {
278
+ if (!stream.destroyed && readOver() && writeOver()) stream.destroy(err || null);
279
+ }
280
+ if (reads) stream.on('end', function () { closeIfDone(); });
281
+ if (sends) stream.on('finish', function () { closeIfDone(); });
282
+
283
+ // options.reliableSize: the first bytes the application wrote that still
284
+ // reach the peer, whole, before the reset (RESET_STREAM_AT, draft-ietf-
285
+ // quic-reliable-stream-reset) — a MOQT subgroup's header, say. More than
286
+ // was written: what was written. Only with a peer that takes
287
+ // RESET_STREAM_AT (stream.reliableResetSupported); without, a RESET_STREAM.
288
+ //
289
+ // After end() too (RFC 9000 §3.1: "Data Sent" → "Reset Sent"), for as long
290
+ // as what was written is not all acknowledged: a MOQT subgroup past its
291
+ // delivery timeout, ended but still on its way, is dropped instead of sent
292
+ // to the end. It did nothing once end() had been called — the FIN queued
293
+ // counted as the direction being over — and the stream went on being sent
294
+ // and resent until it had all arrived. Everything acknowledged already:
295
+ // nothing to reset (false). Returns whether a reset was sent.
296
+ stream.resetStream = function (code, options) {
297
+ if (!sends || sendReset || stream._writeStopped) return false;
298
+ var reliable = options && options.reliableSize != null ? Number(options.reliableSize) : 0;
299
+ if (!(reliable >= 0) || Math.floor(reliable) !== reliable) throw new RangeError('reliableSize must be a non-negative integer, got ' + (options && options.reliableSize));
300
+ var wasOpen = !sendDone;
301
+ if (!resetSending(wtErrorToH3(code || 0), reliable) && !wasOpen) return false; // all delivered
302
+ sendDone = true;
303
+ sendReset = true;
304
+ if (stream._onClosePending) stream._onClosePending(true); // given up before it was open
305
+ if (wasOpen) closeIfDone();
306
+ return true;
307
+ };
308
+ // The sending part reset. Our stream's header (signal value and session
309
+ // ID) is always delivered (draft-ietf-webtrans-http3 §4.4: "MUST use the
310
+ // RESET_STREAM_AT frame with a Reliable Size set to at least the size of
311
+ // the WebTransport header") — without it the peer cannot tell which
312
+ // session the reset is for — and `reliable` bytes of the application's
313
+ // after it. QUIC sends RESET_STREAM_AT when the peer takes it, RESET_STREAM
314
+ // otherwise.
315
+ function resetSending(h3code, reliable) {
316
+ return quic.resetStream(wtStreamId, h3code, prefixLength + (reliable || 0));
317
+ }
318
+ Object.defineProperty(stream, 'reliableResetSupported', { enumerable: true,
319
+ get: function () { return !!quic.resetStreamAtSupported; } });
320
+ stream.stopSending = function (code) {
321
+ if (recvDone) return;
322
+ recvDone = true;
323
+ readStopped = true;
324
+ quic.stopSending(wtStreamId, wtErrorToH3(code || 0)); // and its held credit goes
325
+ closeIfDone();
326
+ };
327
+
328
+ // The peer gave up on one direction, with the application's code:
329
+ // RESET_STREAM ends our reading, STOP_SENDING our sending (QUIC has
330
+ // answered that one with RESET_STREAM already). The other direction goes
331
+ // on, as in the browser — a bidirectional stream's two halves are
332
+ // independent; the peer giving up on one used to destroy the stream, and
333
+ // reset the half it had not touched. 'peerReset' / 'peerStopSending' (code)
334
+ // say which ended, stream.streamErrorCode holds the code, and the stream
335
+ // closes once both directions are over: at once when the other one already
336
+ // is — then with an Error carrying the code, for an 'error' listener only
337
+ // (a cancelled stream is ordinary traffic, not a crash).
338
+ //
339
+ // Reading reset: what was read stands, and the readable side ends (its
340
+ // WHATWG view fails with the code instead — a reset is not a FIN). Sending
341
+ // stopped: writes from then on are dropped (the WHATWG view's fail).
342
+ stream._peerCancelled = function (how, h3code) {
343
+ if (stream.destroyed) return;
344
+ // WT_SESSION_GONE is not the peer giving up on this stream: it is its
345
+ // session ending (draft-ietf-webtrans-http3 §4.6 — every stream of a
346
+ // closed session is reset or stopped with it). The stream ends as its
347
+ // session's streams end here (_sessionGone): both directions, the views
348
+ // failed with the session's error, closed without an 'error'. Taken as a
349
+ // cancel, it was destroyed with one whenever an 'error' listener was there
350
+ // — and pipe() adds one of its own, which re-throws: a server echoing a
351
+ // stream with s.pipe(s) crashed whenever a client closed its session.
352
+ if (h3code === WT_SESSION_GONE) {
353
+ stream._sessionGone();
354
+ stream.destroy();
355
+ return;
356
+ }
357
+ var stop = how === 'stop_sending';
358
+ if (stop ? stream._writeStopped : stream._readReset) return;
359
+ // The RESET_STREAM that answers our own STOP_SENDING (RFC 9000 §3.5): our
360
+ // doing, not the peer giving up — reported as 'peerReset' it was.
361
+ if (!stop && readStopped) return;
362
+ stream.streamErrorCode = h3ErrorToWt(h3code);
363
+ if (stop) { sendDone = true; stream._writeStopped = true; }
364
+ else { recvDone = true; stream._readReset = true; }
365
+ stream.emit(stop ? 'peerStopSending' : 'peerReset', stream.streamErrorCode);
366
+ // The writable fails at once. The readable too, unless bytes received
367
+ // before the reset are still unread — those a RESET_STREAM_AT sent
368
+ // reliably (its Reliable Size) must reach the reader whole: the view
369
+ // reads them, then fails (see its pull).
370
+ if (stop) failWrite(endedError(stream));
371
+ else if (!stream.readableLength) failRead(endedError(stream));
372
+ if (readOver() && writeOver()) {
373
+ var err = new Error(stop ? 'the peer stopped reading this stream' : 'the peer reset this stream');
374
+ err.code = stop ? 'ERR_WEBTRANSPORT_STOP_SENDING' : 'ERR_WEBTRANSPORT_STREAM_RESET';
375
+ err.streamErrorCode = stream.streamErrorCode;
376
+ closeIfDone(stream.listenerCount('error') > 0 ? err : null);
377
+ } else if (!stop) {
378
+ stream.push(null);
379
+ }
380
+ };
381
+
382
+ stream.once('close', function () {
383
+ if (sends) sendStats(); // the last figures, while QUIC still has them
384
+ if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
385
+ var code = closeH3Code !== null ? closeH3Code : wtErrorToH3(0);
386
+ if (!sendDone) { sendDone = true; sendReset = true; resetSending(code, 0); }
387
+ if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, code); }
388
+ // Still waiting to be opened (openWtStream): given up if its sending was
389
+ // cut short; ended normally (end() before it opened), its bytes still go
390
+ // once it opens.
391
+ if (stream._onClosePending) stream._onClosePending(sendReset);
392
+ quic.releaseStreamCredit(wtStreamId);
393
+ if (onClose) onClose();
394
+ });
395
+
396
+ return stream;
397
+ }
398
+
399
+ // ---- The WHATWG views ----
400
+
401
+ // Made only here: the browser's interfaces have no constructor a page can
402
+ // call ("Illegal constructor"), and neither do these.
403
+ var INTERNAL = Symbol('quico.webtransport.internal');
404
+ function illegal() { throw new TypeError('Illegal constructor'); }
405
+
406
+ // WebIDL [Clamp] unsigned long: clamped to 0 – 2^32-1, rounded half to even.
407
+ function clampUint32(v) {
408
+ v = Number(v);
409
+ if (isNaN(v)) return 0;
410
+ v = Math.min(Math.max(v, 0), 4294967295);
411
+ var f = Math.floor(v), d = v - f;
412
+ return d > 0.5 || (d === 0.5 && f % 2 === 1) ? f + 1 : f;
413
+ }
414
+
415
+ // The browser's WebTransportError — a DOMException, as the spec has it (so
416
+ // `err instanceof DOMException`, and `err.name` is 'WebTransportError'): what
417
+ // a stream's reads and writes fail with when the peer reset or stopped it
418
+ // (source 'stream', its streamErrorCode), what a session's end fails things
419
+ // with (source 'session'), and what an application passes to cancel() /
420
+ // abort() to send a code. It was an Error.
421
+ //
422
+ // Takes either form: the spec's (message, { source, streamErrorCode }) or the
423
+ // one browsers implement, a single { message, source, streamErrorCode }. A
424
+ // Node code (err.code, 'ERR_…') is set with setErrorCode: a DOMException's
425
+ // own `code` is a read-only number.
426
+ class WebTransportError extends DOMException {
427
+ constructor(message, options) {
428
+ if (message && typeof message === 'object') { options = message; message = message.message; }
429
+ super(message === undefined || message === null ? '' : String(message), 'WebTransportError');
430
+ var source = options && options.source != null ? String(options.source) : 'stream';
431
+ if (source !== 'stream' && source !== 'session') throw new TypeError("WebTransportError source must be 'stream' or 'session', got " + JSON.stringify(source));
432
+ var code = options && options.streamErrorCode != null ? clampUint32(options.streamErrorCode) : null;
433
+ Object.defineProperty(this, '_wt', { value: { source: source, streamErrorCode: code } });
434
+ }
435
+ get source() { return this._wt.source; }
436
+ get streamErrorCode() { return this._wt.streamErrorCode; }
437
+ }
438
+ function setErrorCode(err, code) {
439
+ Object.defineProperty(err, 'code', { value: code, writable: true, configurable: true, enumerable: false });
440
+ return err;
441
+ }
442
+
443
+ // The browser's stream classes. A bidirectional stream here is a Node Duplex
444
+ // (with the browser's .readable and .writable), so WebTransportBidirectionalStream
445
+ // recognises it rather than being its class; the other two are the
446
+ // WHATWG streams themselves.
447
+ class WebTransportBidirectionalStream {
448
+ constructor() { illegal(); }
449
+ static [Symbol.hasInstance](o) { return o != null && o._wtBidirectional === true; }
450
+ }
451
+ class WebTransportReceiveStream extends ReadableStream {
452
+ constructor(token, source, strategy, stats) {
453
+ if (token !== INTERNAL) illegal();
454
+ super(source, strategy);
455
+ Object.defineProperty(this, '_wtStats', { value: stats });
456
+ }
457
+ /** { bytesReceived, bytesRead } */
458
+ getStats() { return Promise.resolve(this._wtStats()); }
459
+ }
460
+ class WebTransportSendStream extends WritableStream {
461
+ constructor(token, sink, strategy, hooks) {
462
+ if (token !== INTERNAL) illegal();
463
+ super(sink, strategy);
464
+ Object.defineProperty(this, '_wtHooks', { value: hooks });
465
+ }
466
+ /** A WebTransportWriter: the browser's writer, with atomicWrite() and commit(). */
467
+ getWriter() { return new WebTransportWriter(this); }
468
+ /** { bytesWritten, bytesSent, bytesAcknowledged } */
469
+ getStats() { return Promise.resolve(this._wtHooks.stats()); }
470
+ }
471
+
472
+ // The browser's WebTransportWriter (getWriter() of a send stream):
473
+ // atomicWrite(chunk) written only if all of it fits the flow-control
474
+ // window as it is when its turn comes — otherwise not
475
+ // sent at all, and the Promise rejects (AbortError),
476
+ // with any atomic write queued behind it; a plain write
477
+ // queued behind it then fails the stream, as the spec
478
+ // has it. The stream itself goes on.
479
+ // commit() what was written so far is delivered even if the
480
+ // stream is aborted later: abort() then resets it with
481
+ // RESET_STREAM_AT, that many bytes reliable (a MOQT
482
+ // subgroup's header, say) — with a peer that takes it;
483
+ // without, a plain RESET_STREAM.
484
+ class WebTransportWriter extends WritableStreamDefaultWriter {
485
+ constructor(stream) {
486
+ super(stream);
487
+ Object.defineProperty(this, '_wtHooks', { value: stream && stream._wtHooks ? stream._wtHooks : null });
488
+ }
489
+ write(chunk) {
490
+ var h = this._wtHooks, p = super.write(chunk);
491
+ if (!h) return p;
492
+ h.plainPending++;
493
+ var done = function () { h.plainPending--; };
494
+ p.then(done, done);
495
+ return p;
496
+ }
497
+ atomicWrite(chunk) {
498
+ var h = this._wtHooks;
499
+ if (!h) return super.write(chunk);
500
+ var ticket = h.markAtomic(chunk);
501
+ var p = super.write(chunk);
502
+ return p.then(function () {
503
+ if (ticket.aborted) throw new DOMException('the chunk does not fit the flow-control window', 'AbortError');
504
+ }, function (err) { ticket.aborted = true; throw err; });
505
+ }
506
+ commit() {
507
+ if (this._wtHooks) this._wtHooks.commit();
508
+ }
509
+ }
510
+
511
+ // The browser's WebTransportSendGroup: streams (and datagram writables) that
512
+ // share a send-order space; getStats() sums its streams' figures.
513
+ class WebTransportSendGroup {
514
+ constructor(token, owner) {
515
+ if (token !== INTERNAL) illegal();
516
+ Object.defineProperty(this, '_owner', { value: owner || null });
517
+ Object.defineProperty(this, '_members', { value: new Set() });
518
+ }
519
+ _add(f) { this._members.add(f); }
520
+ _remove(f) { this._members.delete(f); }
521
+ getStats() {
522
+ var t = { bytesWritten: 0, bytesSent: 0, bytesAcknowledged: 0 };
523
+ this._members.forEach(function (f) { var s = f(); t.bytesWritten += s.bytesWritten; t.bytesSent += s.bytesSent; t.bytesAcknowledged += s.bytesAcknowledged; });
524
+ return Promise.resolve(t);
525
+ }
526
+ }
527
+
528
+ class WebTransportDatagramsWritable extends WritableStream {
529
+ constructor(token, sink, strategy) {
530
+ if (token !== INTERNAL) illegal();
531
+ super(sink, strategy);
532
+ }
533
+ }
534
+ class WebTransportDatagramDuplexStream {
535
+ constructor() { illegal(); }
536
+ }
537
+
538
+ // The code a cancel() or abort() reason carries; 0 for any other reason.
539
+ function codeOf(reason) {
540
+ return reason && typeof reason.streamErrorCode === 'number' ? reason.streamErrorCode : 0;
541
+ }
542
+
543
+ // Why the stream ended early: the peer's code if it gave one.
544
+ function endedError(stream) {
545
+ return new WebTransportError(stream.streamErrorCode != null ? 'the peer gave up on this stream' : 'stream closed',
546
+ { streamErrorCode: stream.streamErrorCode });
547
+ }
548
+
549
+ // What a write into a WHATWG view may carry: a BufferSource — an ArrayBuffer
550
+ // or a view onto one, not shared memory — and not an empty one (as the
551
+ // browsers reject it; WPT sendstream-bad-chunk, datagram-bad-chunk). A string
552
+ // was turned into whatever its missing .buffer gave. Copied: the caller may
553
+ // reuse its buffer once write() returns.
554
+ function bufferSourceBytes(chunk, what) {
555
+ var u8 = null;
556
+ if (chunk instanceof ArrayBuffer) u8 = new Uint8Array(chunk);
557
+ else if (ArrayBuffer.isView(chunk) && !(typeof SharedArrayBuffer !== 'undefined' && chunk.buffer instanceof SharedArrayBuffer)) {
558
+ u8 = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
559
+ }
560
+ if (!u8) throw new TypeError(what + ' must be an ArrayBuffer or an ArrayBufferView');
561
+ if (u8.byteLength === 0) throw new TypeError(what + ' must not be empty');
562
+ return u8.slice();
563
+ }
564
+
565
+ // Each returns { stream, fail(err) }: the WHATWG stream, and how its owner
566
+ // fails it from outside — the peer giving up on that direction, or the
567
+ // session's end — while nothing is reading or writing.
568
+ //
569
+ // The readable is a byte stream, as the browser's WebTransportReceiveStream:
570
+ // a BYOB reader (getReader({ mode: 'byob' })) fills its own buffer, what does
571
+ // not fit kept for the next read. A default reader gets each chunk in a
572
+ // buffer of its own — a copy: enqueue() takes the buffer over, and a chunk
573
+ // read here can share its memory with others.
574
+ function webReadableOf(stream, stats) {
575
+ var controller = null, failed = false;
576
+ var rs = new WebTransportReceiveStream(INTERNAL, {
577
+ type: 'bytes',
578
+ start: function (c) { controller = c; },
579
+ pull: function (controller) {
580
+ return new Promise(function (resolve) {
581
+ function attempt() {
582
+ if (failed) return done();
583
+ var chunk = stream.read();
584
+ if (chunk !== null) {
585
+ var req = controller.byobRequest;
586
+ if (req && req.view) {
587
+ var v = req.view, n = Math.min(v.byteLength, chunk.length);
588
+ new Uint8Array(v.buffer, v.byteOffset, n).set(chunk.subarray(0, n));
589
+ if (chunk.length > n) stream.unshift(chunk.subarray(n));
590
+ req.respond(n);
591
+ } else {
592
+ controller.enqueue(new Uint8Array(chunk));
593
+ }
594
+ return done();
595
+ }
596
+ if (stream._readReset) { fail(endedError(stream)); return done(); }
597
+ if (stream.readableEnded) {
598
+ controller.close();
599
+ if (controller.byobRequest) controller.byobRequest.respond(0);
600
+ return done();
601
+ }
602
+ if (stream.destroyed) { fail(stream._sessionError || endedError(stream)); return done(); }
603
+ return false;
604
+ }
605
+ function done() {
606
+ stream.removeListener('readable', attempt);
607
+ stream.removeListener('end', attempt);
608
+ stream.removeListener('close', attempt);
609
+ resolve();
610
+ return true;
611
+ }
612
+ if (attempt()) return;
613
+ stream.on('readable', attempt);
614
+ stream.on('end', attempt);
615
+ stream.on('close', attempt);
616
+ });
617
+ },
618
+ cancel: function (reason) { failed = true; stream.stopSending(codeOf(reason)); }
619
+ }, { highWaterMark: 0 }, stats); // read only when the reader asks: backpressure
620
+ function fail(err) {
621
+ if (failed) return;
622
+ failed = true;
623
+ try { controller.error(err); } catch (e) {} // already closed: nothing to fail
624
+ }
625
+ return { stream: rs, fail: fail };
626
+ }
627
+
628
+ function webWritableOf(stream, stats, quic, streamId) {
629
+ var controller = null, failed = false;
630
+ var committed = 0; // commit(): bytes delivered even if aborted
631
+ var atomic = new Map(); // chunk → its atomic tickets, in write order
632
+ var hooks = {
633
+ stats: stats,
634
+ plainPending: 0,
635
+ markAtomic: function (chunk) {
636
+ var t = { aborted: false };
637
+ var list = atomic.get(chunk);
638
+ if (list) list.push(t); else atomic.set(chunk, [t]);
639
+ return t;
640
+ },
641
+ commit: function () { committed = stream._bytesWritten(); }
642
+ };
643
+ // The sink is told a chunk is atomic by its ticket (markAtomic, just before
644
+ // the writer queues it); plain writes are counted by the writer's write().
645
+ function takeTicket(chunk) {
646
+ var list = atomic.get(chunk);
647
+ if (!list) return null;
648
+ var t = list.shift();
649
+ if (list.length === 0) atomic.delete(chunk);
650
+ return t;
651
+ }
652
+ // An atomic write that does not fit: it, and every atomic write still
653
+ // queued, are not sent; a plain write queued behind it fails the stream.
654
+ function abortAtomic(t) {
655
+ t.aborted = true;
656
+ atomic.forEach(function (list) { list.forEach(function (x) { x.aborted = true; }); });
657
+ if (hooks.plainPending > 0) fail(new DOMException('a write queued behind an atomic write that did not fit', 'AbortError'));
658
+ }
659
+ var ws = new WebTransportSendStream(INTERNAL, {
660
+ start: function (c) { controller = c; },
661
+ write: function (chunk) {
662
+ var ticket = takeTicket(chunk);
663
+ var u8 = bufferSourceBytes(chunk, 'a chunk written to a WebTransport stream');
664
+ if (ticket) {
665
+ if (ticket.aborted) return;
666
+ if (!quic.streamSendWindow || quic.streamSendWindow(streamId) < u8.byteLength) { abortAtomic(ticket); return; }
667
+ }
668
+ return new Promise(function (resolve, reject) {
669
+ if (stream.destroyed || stream._writeStopped) { reject(stream._sessionError || endedError(stream)); return; }
670
+ stream.write(Buffer.from(u8.buffer, u8.byteOffset, u8.byteLength), function (err) {
671
+ if (err || stream.destroyed || stream._writeStopped) reject(stream._sessionError || endedError(stream)); else resolve();
672
+ });
673
+ });
674
+ },
675
+ close: function () {
676
+ return new Promise(function (resolve, reject) {
677
+ if (stream.destroyed || stream._writeStopped) { reject(stream._sessionError || endedError(stream)); return; }
678
+ stream.end(resolve);
679
+ });
680
+ },
681
+ // RESET_STREAM_AT with what was committed, when anything was.
682
+ abort: function (reason) {
683
+ failed = true;
684
+ stream.resetStream(codeOf(reason), committed > 0 ? { reliableSize: committed } : undefined);
685
+ }
686
+ }, undefined, hooks);
687
+ function fail(err) {
688
+ if (failed) return;
689
+ failed = true;
690
+ try { controller.error(err); } catch (e) {} // already closed: nothing to fail
691
+ }
692
+ return { stream: ws, fail: fail };
693
+ }
694
+
695
+ // ---- Opening a stream of our own ----
696
+ //
697
+ // The browser's way, for both sides (the server's res.create…Stream, the
698
+ // client's wt.create…Stream): options { sendOrder, sendGroup,
699
+ // waitUntilAvailable }. Without waitUntilAvailable, a stream past the peer's
700
+ // MAX_STREAMS is refused at once — null here; the browser rejects — rather
701
+ // than opened to wait unseen; with it, it is opened and its bytes wait for
702
+ // the peer to allow more. make(streamId, kind, prefixLength) builds the
703
+ // stream object, recorded by its owner.
704
+ //
705
+ // Opened is not open yet: past the peer's limit the stream waits for it to
706
+ // allow more. stream.pending says so (as net.Socket's does) until it is
707
+ // open; then 'ready', and callback(null, stream) — or callback(err) if it
708
+ // closes first (its session or connection did). Both after this returns,
709
+ // as Node's are, whether or not it had to wait. The client's Promise is
710
+ // settled from the same callback: resolved once the stream is open, as the
711
+ // browser's is — it was resolved at once, open or not.
712
+ function openWtStream(h3, quic, sessionId, bidi, options, make, callback) {
713
+ options = options || {};
714
+ if (!options.waitUntilAvailable && !quic.canOpenStream(bidi)) return null;
715
+ var streamId = h3.createWebTransportStream(sessionId, bidi);
716
+ var stream = make(streamId, bidi ? 'bidi' : 'uni-out', wt_stream_header(bidi, sessionId).byteLength);
717
+ if (options.sendGroup) stream.sendGroup = options.sendGroup;
718
+ if (options.sendOrder != null) stream.sendOrder = options.sendOrder;
719
+ stream.pending = true;
720
+ var settled = false;
721
+ function settle(err) {
722
+ if (settled) return;
723
+ settled = true;
724
+ stream._onClosePending = null;
725
+ stream.pending = false;
726
+ if (err) { err = new Error('the stream closed before it was open'); err.code = 'ERR_WEBTRANSPORT_STREAM_NOT_OPEN'; }
727
+ process.nextTick(function () {
728
+ if (!err) stream.emit('ready');
729
+ if (typeof callback === 'function') callback(err || null, stream);
730
+ });
731
+ }
732
+ stream._onClosePending = function (aborted) { if (aborted) settle(true); };
733
+ quic.whenStreamPermitted(streamId, settle);
734
+ return stream;
735
+ }
736
+
737
+ // ---- A session's place in QUIC's scheduler ----
738
+ //
739
+ // The browser's model (W3C WebTransport, sendGroup / sendOrder): the streams
740
+ // of a send group share the group's turns, the highest sendOrder with
741
+ // something to send taking them, equal orders taking turns; the groups share
742
+ // the session's sending equally. A stream in no group is in its session's own
743
+ // group. QUIC's scheduler has exactly that as `group` and `order`
744
+ // (setStreamPriority); this maps one onto the other. Every stream and group
745
+ // of the session is at the urgency of its CONNECT stream — RFC 9218's
746
+ // `priority` on the CONNECT request, or what the application set for it —
747
+ // taken as each stream is placed.
748
+ //
749
+ // Datagrams go first by default, ahead of any stream (sendDatagram). Sent
750
+ // with a sendGroup or a sendOrder other than 0, they take their place among
751
+ // the streams instead, as the browser's createWritable({ sendOrder,
752
+ // sendGroup }) has it: a member of that group (or of the session's), at
753
+ // that order.
754
+ function wtScheduling(quic, sessionStreamId) {
755
+ var sessionGroup = {};
756
+ function urgency() {
757
+ var p = quic.getStreamPriority(sessionStreamId);
758
+ return p && p.urgency != null ? p.urgency : 3;
759
+ }
760
+ return {
761
+ forStream: function (group, order) {
762
+ return { urgency: urgency(), incremental: true, group: group || sessionGroup, order: Number(order) || 0 };
763
+ },
764
+ forDatagram: function (options) {
765
+ if (!options || (!options.sendGroup && !Number(options.sendOrder))) return null;
766
+ return { urgency: urgency(), group: options.sendGroup || sessionGroup, order: Number(options.sendOrder) || 0 };
767
+ }
768
+ };
769
+ }
770
+
771
+ // ---- A session's datagrams and statistics, for both sides ----
772
+ //
773
+ // The client's WebTransport and the server's res share these, so a session
774
+ // looks the same from either end. `session` is what they hand in:
775
+ // quic the QUIC connection (a getter on the client:
776
+ // made after the session object)
777
+ // connected() whether the session is up
778
+ // send(u8, options) its sendDatagram (options: sendOrder, sendGroup)
779
+ // maxDatagramSize() the largest payload it can send
780
+ // owner { maxAge, expired } — its datagrams in QUIC's
781
+ // queue: their age limit (outgoingMaxAge) and how
782
+ // many were dropped unsent (sendDatagram's owner)
783
+ // dgStats { droppedIncoming, expiredIncoming }
784
+ // connecting() (the client's) the session is still being
785
+ // set up: datagrams written meanwhile wait
786
+ // transport (the client's) its WebTransport — a send group
787
+ // of another session's is refused
788
+
789
+ // WebIDL's unsigned long, as a setter takes it: a number, its fraction
790
+ // dropped, modulo 2^32 (-1 is 4294967295); NaN and ±Infinity are 0.
791
+ function toUint32(v) {
792
+ v = Number(v);
793
+ if (!isFinite(v)) return 0;
794
+ v = Math.trunc(v) % 4294967296;
795
+ return v < 0 ? v + 4294967296 : v;
796
+ }
797
+ // An age limit, as the browser's setters take it: null, or milliseconds —
798
+ // negative or NaN is a RangeError, 0 means none (null).
799
+ function maxAgeOf(v, name) {
800
+ if (v === null || v === undefined) return null;
801
+ v = Number(v);
802
+ if (isNaN(v) || v < 0) throw new RangeError(name + ' must be null or a non-negative number, got ' + v);
803
+ return v === 0 ? null : v;
804
+ }
805
+
806
+ // The browser's WebTransportDatagramDuplexStream (W3C WebTransport):
807
+ // readable the incoming datagrams, read only when asked;
808
+ // past incomingMaxBufferedDatagrams the oldest
809
+ // goes, one older than incomingMaxAge is skipped
810
+ // createWritable({ sendOrder, sendGroup })
811
+ // a WebTransportDatagramsWritable of its own:
812
+ // each has its queue, its place among the
813
+ // session's streams, and its backpressure —
814
+ // writer.ready waits while outgoingMaxBufferedDatagrams
815
+ // of its datagrams wait to go
816
+ // maxDatagramSize, incomingMaxAge, outgoingMaxAge,
817
+ // incomingMaxBufferedDatagrams, outgoingMaxBufferedDatagrams
818
+ // What is written before the session is up waits for it (and goes once it
819
+ // is, those past outgoingMaxAge dropped); one larger than maxDatagramSize is
820
+ // dropped, as the browser drops it. outgoingMaxAge is this session's alone (a
821
+ // session on a shared connection used to set the whole connection's).
822
+ //
823
+ // Also, as before the browser's API changed, and for code written then:
824
+ // `writable` (one writable, made on first use), and incomingHighWaterMark /
825
+ // outgoingHighWaterMark — the same settings as …MaxBufferedDatagrams.
826
+ function createDatagrams(session) {
827
+ var queue = [], controller = null, pull = null, readable = null, legacy = null;
828
+ var inMax = 256, outMax = 256, inAge = null;
829
+ var writers = new Set(); // each writable's { w, q, release, fail }
830
+ var ended = null; // the session's end: { error } (null error: closed cleanly)
831
+ var flushing = false;
832
+ var dg = Object.create(WebTransportDatagramDuplexStream.prototype);
833
+ var bytesType = session.readableType === 'bytes'; // datagramsReadableType: a BYOB reader, as the browser's
834
+ var pullReject = null;
835
+
836
+ function deliver() {
837
+ if (!pull) return;
838
+ var now = Date.now();
839
+ while (queue.length > 0) {
840
+ var d = queue.shift();
841
+ if (inAge != null && now - d.t > inAge) { session.dgStats.expiredIncoming++; continue; }
842
+ var p = pull, r = pullReject; pull = pullReject = null;
843
+ var req = bytesType ? controller.byobRequest : null;
844
+ if (req && req.view) {
845
+ // A BYOB read: the whole datagram into the reader's buffer — one too
846
+ // small is a RangeError, a view of elements wider than a byte a
847
+ // TypeError (W3C pullDatagrams); either fails the readable.
848
+ var v = req.view;
849
+ if (v.byteLength < d.data.byteLength) { r(new RangeError('the buffer (' + v.byteLength + ' bytes) is smaller than the datagram (' + d.data.byteLength + ')')); return; }
850
+ if (!(v instanceof Uint8Array || v instanceof Int8Array || v instanceof Uint8ClampedArray)) { r(new TypeError('a datagram is read into a view of bytes')); return; }
851
+ new Uint8Array(v.buffer, v.byteOffset, d.data.byteLength).set(d.data);
852
+ req.respond(d.data.byteLength);
853
+ } else {
854
+ controller.enqueue(d.data);
855
+ }
856
+ p();
857
+ return;
858
+ }
859
+ }
860
+
861
+ function connecting() { return !!(session.connecting && session.connecting()); }
862
+
863
+ // What the writables hold goes to QUIC — on the next turn, so that a burst
864
+ // of writes queues up to outgoingMaxBufferedDatagrams and its writer feels
865
+ // it (writer.ready), as the browser's does; or, while the session is being
866
+ // set up, waits for it. Older than outgoingMaxAge: dropped unsent.
867
+ function scheduleFlush() {
868
+ if (flushing) return;
869
+ flushing = true;
870
+ setImmediate(flush);
871
+ }
872
+ function flush() {
873
+ flushing = false;
874
+ var up = session.connected(), now = Date.now(), age = session.owner.maxAge;
875
+ writers.forEach(function (r) {
876
+ while (r.q.length > 0 && age != null && now - r.q[0].t > age) { r.q.shift(); session.owner.expired = (session.owner.expired || 0) + 1; }
877
+ if (!up) return;
878
+ while (r.q.length > 0) {
879
+ var u8 = r.q.shift().data;
880
+ if (u8.byteLength <= session.maxDatagramSize()) session.send(u8, r.w); // its sendOrder / sendGroup, as they are now
881
+ }
882
+ r.release();
883
+ if (r.closing) writers.delete(r);
884
+ });
885
+ }
886
+
887
+ function foreignGroup(g) {
888
+ return g != null && g._owner && session.transport && g._owner !== session.transport;
889
+ }
890
+
891
+ function makeWritable(options) {
892
+ options = options || {};
893
+ var r = { q: [], bp: null, bpResolve: null, controller: null };
894
+ r.release = function () {
895
+ if (r.bpResolve && r.q.length < outMax) { var f = r.bpResolve; r.bp = r.bpResolve = null; f(); }
896
+ };
897
+ r.fail = function (err) { try { r.controller.error(err); } catch (e) {} };
898
+ var w = new WebTransportDatagramsWritable(INTERNAL, {
899
+ start: function (c) { r.controller = c; },
900
+ write: function (chunk) {
901
+ var t = Date.now();
902
+ var u8 = bufferSourceBytes(chunk, 'a datagram');
903
+ if (ended) {
904
+ if (ended.error) throw ended.error;
905
+ throw new WebTransportError('the session is closed', { source: 'session' });
906
+ }
907
+ if (!session.connected() && !connecting()) throw new WebTransportError('the session is not connected', { source: 'session' });
908
+ // Larger than can be sent: dropped, the write resolves (as the browser's).
909
+ if (session.connected() && u8.byteLength > session.maxDatagramSize()) return;
910
+ r.q.push({ data: u8, t: t });
911
+ scheduleFlush();
912
+ if (r.q.length >= outMax) {
913
+ if (!r.bp) r.bp = new Promise(function (resolve) { r.bpResolve = resolve; });
914
+ return r.bp;
915
+ }
916
+ },
917
+ // Closed: what it holds still goes, then it is forgotten (flush).
918
+ close: function () { r.closing = true; scheduleFlush(); },
919
+ abort: function () { r.q = []; writers.delete(r); }
920
+ }, { highWaterMark: 1 });
921
+ var sendOrder = Number(options.sendOrder) || 0, sendGroup = options.sendGroup || null;
922
+ Object.defineProperty(w, 'sendOrder', { enumerable: true,
923
+ get: function () { return sendOrder; }, set: function (v) { sendOrder = Number(v) || 0; } });
924
+ Object.defineProperty(w, 'sendGroup', { enumerable: true,
925
+ get: function () { return sendGroup; },
926
+ set: function (g) {
927
+ if (foreignGroup(g)) throw new DOMException('this send group belongs to another WebTransport session', 'InvalidStateError');
928
+ sendGroup = g || null;
929
+ } });
930
+ r.w = w;
931
+ writers.add(r);
932
+ return w;
933
+ }
934
+
935
+ Object.defineProperty(dg, 'readable', { enumerable: true, get: function () {
936
+ if (!readable) {
937
+ var source = {
938
+ start: function (c) {
939
+ controller = c;
940
+ if (ended) { if (ended.error) c.error(ended.error); else c.close(); }
941
+ },
942
+ pull: function () { return new Promise(function (resolve, reject) { pull = resolve; pullReject = reject; deliver(); }); }
943
+ };
944
+ if (bytesType) source.type = 'bytes';
945
+ readable = new ReadableStream(source, { highWaterMark: 0 });
946
+ }
947
+ return readable;
948
+ } });
949
+ // The browser's createWritable: a group of another session's is a
950
+ // TypeError, a session that is over an InvalidStateError — in that order.
951
+ dg.createWritable = function (options) {
952
+ if (options && foreignGroup(options.sendGroup)) throw new TypeError('options.sendGroup belongs to another WebTransport session');
953
+ if (ended || (!session.connected() && !connecting())) throw new DOMException('the session is closed', 'InvalidStateError');
954
+ return makeWritable(options);
955
+ };
956
+ Object.defineProperty(dg, 'writable', { enumerable: true, get: function () {
957
+ return legacy || (legacy = makeWritable());
958
+ } });
959
+ Object.defineProperty(dg, 'maxDatagramSize', { enumerable: true, get: function () { return session.maxDatagramSize(); } });
960
+ Object.defineProperty(dg, 'incomingMaxAge', { enumerable: true,
961
+ get: function () { return inAge; },
962
+ set: function (v) { inAge = maxAgeOf(v, 'incomingMaxAge'); } });
963
+ Object.defineProperty(dg, 'outgoingMaxAge', { enumerable: true,
964
+ get: function () { return session.owner.maxAge; },
965
+ set: function (v) { session.owner.maxAge = maxAgeOf(v, 'outgoingMaxAge'); } });
966
+ function inMaxProp() {
967
+ return { enumerable: true, get: function () { return inMax; },
968
+ set: function (v) { inMax = Math.max(1, toUint32(v)); } };
969
+ }
970
+ function outMaxProp() {
971
+ return { enumerable: true, get: function () { return outMax; },
972
+ set: function (v) { outMax = Math.max(1, toUint32(v)); writers.forEach(function (r) { r.release(); }); } };
973
+ }
974
+ Object.defineProperty(dg, 'incomingMaxBufferedDatagrams', inMaxProp());
975
+ Object.defineProperty(dg, 'outgoingMaxBufferedDatagrams', outMaxProp());
976
+ Object.defineProperty(dg, 'incomingHighWaterMark', inMaxProp());
977
+ Object.defineProperty(dg, 'outgoingHighWaterMark', outMaxProp());
978
+
979
+ // A datagram arrived: for the readable, once anything reads it.
980
+ dg._push = function (data) {
981
+ if (!readable || ended) return;
982
+ queue.push({ data: data, t: Date.now() });
983
+ while (queue.length > inMax) { queue.shift(); session.dgStats.droppedIncoming++; }
984
+ deliver();
985
+ };
986
+ // The session is up: what was written meanwhile goes.
987
+ dg._flush = function () { scheduleFlush(); };
988
+ // The session is over (W3C "cleanup"): the queues emptied, each writer
989
+ // released; closed cleanly, the readable closes; lost (`err`), the readable
990
+ // and every writable fail with the session's error.
991
+ dg._close = function (err) {
992
+ if (ended) return;
993
+ ended = { error: err || null };
994
+ queue = [];
995
+ writers.forEach(function (r) { r.q = []; r.release(); if (err) r.fail(err); });
996
+ if (controller) {
997
+ try {
998
+ if (err) controller.error(err);
999
+ else { controller.close(); if (controller.byobRequest) controller.byobRequest.respond(0); }
1000
+ } catch (e) {}
1001
+ }
1002
+ if (pull) { var p = pull; pull = pullReject = null; p(); }
1003
+ };
1004
+ return dg;
1005
+ }
1006
+
1007
+ // The browser's getStats(): the connection's figures (QUIC's getStats — shared
1008
+ // by every session on it, as in the browser), and the session's datagrams.
1009
+ function sessionStats(session) {
1010
+ var q = session.quic && session.quic.getStats ? session.quic.getStats() : {};
1011
+ var out = { timestamp: Date.now() };
1012
+ ['packetsSent', 'packetsLost', 'bytesLost', 'bytesReceived', 'packetsReceived', 'smoothedRtt', 'rttVariation',
1013
+ 'minRtt', 'estimatedSendRate', 'atSendCapacity', 'congestionWindow', 'bytesInFlight'].forEach(function (k) {
1014
+ out[k] = q[k] !== undefined ? q[k] : null;
1015
+ });
1016
+ // As the spec counts them: bytesSent the application's bytes (new stream
1017
+ // data and datagram payloads — no framing, no retransmissions),
1018
+ // bytesSentOverhead what the packets carried besides, bytesAcknowledged
1019
+ // the payload the peer acknowledged. (bytesSent was the packets' size.)
1020
+ out.bytesSent = q.payloadBytesSent !== undefined ? q.payloadBytesSent : null;
1021
+ out.bytesSentOverhead = q.payloadBytesSent !== undefined && q.bytesSent !== undefined ? Math.max(0, q.bytesSent - q.payloadBytesSent) : null;
1022
+ out.bytesAcknowledged = q.payloadBytesAcknowledged !== undefined ? q.payloadBytesAcknowledged : null;
1023
+ out.datagrams = {
1024
+ droppedIncoming: session.dgStats.droppedIncoming,
1025
+ expiredIncoming: session.dgStats.expiredIncoming,
1026
+ expiredOutgoing: session.owner.expired || 0,
1027
+ lostOutgoing: null // a datagram's packet lost is not traced back to it
1028
+ };
1029
+ return Promise.resolve(out);
1030
+ }
1031
+
1032
+ // owner: the client's WebTransport, so that a group is not used on another
1033
+ // session (its streams and datagram writables check it).
1034
+ function createSendGroup(owner) { return new WebTransportSendGroup(INTERNAL, owner); }
1035
+
1036
+ // ---- Keying material (the browser's exportKeyingMaterial) ----
1037
+ //
1038
+ // draft-ietf-webtrans-http3 ("Exporting Keying Material"): the TLS exporter
1039
+ // (RFC 8446 §7.5) with the label "EXPORTER-WebTransport" and, as its context,
1040
+ // WebTransport Session ID (64), Label Length (8), Label, Context Length (8), Context
1041
+ // — so a session's keys are its own, not the connection's. Checked as the
1042
+ // browser checks them: all three arguments required (TypeError), the label
1043
+ // and the context BufferSources of at most 255 bytes, the length 1 to 4096
1044
+ // (RangeError).
1045
+ function bufferSourceView(v, name) {
1046
+ var shared = typeof SharedArrayBuffer !== 'undefined' &&
1047
+ (v instanceof SharedArrayBuffer || (ArrayBuffer.isView(v) && v.buffer instanceof SharedArrayBuffer));
1048
+ if (!shared && v instanceof ArrayBuffer) return new Uint8Array(v);
1049
+ if (!shared && ArrayBuffer.isView(v)) return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
1050
+ throw new TypeError(name + ' must be an ArrayBuffer or an ArrayBufferView');
1051
+ }
1052
+ function exporterArgs(args) {
1053
+ if (args.length < 3) throw new TypeError('exportKeyingMaterial(label, context, outputLength): all three are required');
1054
+ var label = bufferSourceView(args[0], 'label'), context = bufferSourceView(args[1], 'context');
1055
+ if (label.byteLength > 255) throw new RangeError('the label is longer than 255 bytes');
1056
+ if (context.byteLength > 255) throw new RangeError('the context is longer than 255 bytes');
1057
+ var length = toUint32(args[2]);
1058
+ if (length === 0 || length > 4096) throw new RangeError('outputLength must be 1 to 4096, got ' + args[2]);
1059
+ return { label: label, context: context, length: length };
1060
+ }
1061
+ function exportSessionKeyingMaterial(quic, sessionId, a) {
1062
+ var L = a.label.byteLength, C = a.context.byteLength;
1063
+ var ctx = new Uint8Array(8 + 1 + L + 1 + C);
1064
+ new DataView(ctx.buffer).setBigUint64(0, BigInt(sessionId));
1065
+ ctx[8] = L; ctx.set(a.label, 9);
1066
+ ctx[9 + L] = C; ctx.set(a.context, 10 + L);
1067
+ var out = quic && quic.exportKeyingMaterial ? quic.exportKeyingMaterial(a.length, 'EXPORTER-WebTransport', ctx) : null;
1068
+ if (!out) throw new DOMException('the connection cannot export keying material', 'InvalidStateError');
1069
+ return new Uint8Array(out.buffer, out.byteOffset, out.byteLength).slice();
1070
+ }
1071
+
1072
+ export { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, setErrorCode,
1073
+ exporterArgs, exportSessionKeyingMaterial,
1074
+ WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
1075
+ WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream };