quico 0.7.0 → 0.9.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.
package/src/wt_stream.js CHANGED
@@ -127,6 +127,10 @@ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, se
127
127
  get: function () { return sendOrder; }, set: function (v) { sendOrder = Number(v) || 0; schedule(); } });
128
128
  Object.defineProperty(target, 'sendGroup', { configurable: true, enumerable: true,
129
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
+ }
130
134
  if (sendGroup && sendGroup._remove) sendGroup._remove(sendStats);
131
135
  sendGroup = g || null;
132
136
  if (sendGroup && sendGroup._add) sendGroup._add(sendStats);
@@ -144,6 +148,7 @@ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, se
144
148
 
145
149
  stream._streamId = wtStreamId;
146
150
  stream._sessionId = sessionId;
151
+ if (kind === 'bidi') Object.defineProperty(stream, '_wtBidirectional', { value: true }); // instanceof WebTransportBidirectionalStream
147
152
 
148
153
  // The code what is still open goes with when the stream is destroyed:
149
154
  // destroy(err) with err.streamErrorCode — the browser's
@@ -159,7 +164,16 @@ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, se
159
164
  // At once, not when 'close' comes a tick later: the session's end is
160
165
  // often the connection's (a WebTransport client's own), whose
161
166
  // CONNECTION_CLOSE would go first and these with it.
162
- stream._sessionGone = function () {
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);
163
177
  if (closeH3Code === null) closeH3Code = WT_SESSION_GONE;
164
178
  if (!sendDone) { sendDone = true; sendReset = true; resetSending(closeH3Code, 0); }
165
179
  if (!recvDone) { recvDone = true; readStopped = true; quic.stopSending(wtStreamId, closeH3Code); }
@@ -170,18 +184,31 @@ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, se
170
184
  return baseDestroy.apply(this, arguments);
171
185
  };
172
186
  stream.streamErrorCode = null;
187
+ stream._bytesWritten = function () { return stats.bytesWritten; }; // a writer's commit()
173
188
  // Its QUIC stream ID and its session's — read-only, for logs and maps.
174
189
  Object.defineProperty(stream, 'id', { enumerable: true, get: function () { return wtStreamId; } });
175
190
  Object.defineProperty(stream, 'sessionId', { enumerable: true, get: function () { return sessionId; } });
176
191
 
177
192
  // The WHATWG views (see above), made on first use — with what the
178
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.
179
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); }
180
205
  if (reads) {
181
206
  Object.defineProperty(stream, 'readable', { configurable: true, get: function () {
182
207
  if (!webReadable) {
183
- webReadable = webReadableOf(stream);
184
- webReadable.getStats = function () { return Promise.resolve(recvStats()); };
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));
185
212
  }
186
213
  return webReadable;
187
214
  } });
@@ -190,9 +217,11 @@ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, se
190
217
  sendProps(stream);
191
218
  Object.defineProperty(stream, 'writable', { configurable: true, get: function () {
192
219
  if (!webWritable) {
193
- webWritable = webWritableOf(stream);
220
+ writeView = webWritableOf(stream, sendStats, quic, wtStreamId);
221
+ webWritable = writeView.stream;
194
222
  sendProps(webWritable);
195
- webWritable.getStats = function () { return Promise.resolve(sendStats()); };
223
+ if (stream._sessionError) failWrite(stream._sessionError);
224
+ else if (stream._writeStopped) failWrite(endedError(stream));
196
225
  }
197
226
  return webWritable;
198
227
  } });
@@ -312,6 +341,19 @@ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, se
312
341
  // stopped: writes from then on are dropped (the WHATWG view's fail).
313
342
  stream._peerCancelled = function (how, h3code) {
314
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
+ }
315
357
  var stop = how === 'stop_sending';
316
358
  if (stop ? stream._writeStopped : stream._readReset) return;
317
359
  // The RESET_STREAM that answers our own STOP_SENDING (RFC 9000 §3.5): our
@@ -321,6 +363,12 @@ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, se
321
363
  if (stop) { sendDone = true; stream._writeStopped = true; }
322
364
  else { recvDone = true; stream._readReset = true; }
323
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));
324
372
  if (readOver() && writeOver()) {
325
373
  var err = new Error(stop ? 'the peer stopped reading this stream' : 'the peer reset this stream');
326
374
  err.code = stop ? 'ERR_WEBTRANSPORT_STOP_SENDING' : 'ERR_WEBTRANSPORT_STREAM_RESET';
@@ -350,20 +398,141 @@ function createWtStream(quic, wtStreamId, kind, onClose, prefixLength, sched, se
350
398
 
351
399
  // ---- The WHATWG views ----
352
400
 
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.
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.
356
421
  //
357
422
  // 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 {
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 {
360
427
  constructor(message, options) {
361
428
  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;
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 } });
366
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(); }
367
536
  }
368
537
 
369
538
  // The code a cancel() or abort() reason carries; 0 for any other reason.
@@ -377,16 +546,60 @@ function endedError(stream) {
377
546
  { streamErrorCode: stream.streamErrorCode });
378
547
  }
379
548
 
380
- function webReadableOf(stream) {
381
- return new ReadableStream({
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; },
382
579
  pull: function (controller) {
383
580
  return new Promise(function (resolve) {
384
581
  function attempt() {
582
+ if (failed) return done();
385
583
  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(); }
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(); }
390
603
  return false;
391
604
  }
392
605
  function done() {
@@ -402,28 +615,81 @@ function webReadableOf(stream) {
402
615
  stream.on('close', attempt);
403
616
  });
404
617
  },
405
- cancel: function (reason) { stream.stopSending(codeOf(reason)); }
406
- }, { highWaterMark: 0 }); // read only when the reader asks: backpressure
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 };
407
626
  }
408
627
 
409
- function webWritableOf(stream) {
410
- return new WritableStream({
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; },
411
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
+ }
412
668
  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();
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();
416
672
  });
417
673
  });
418
674
  },
419
675
  close: function () {
420
676
  return new Promise(function (resolve, reject) {
421
- if (stream.destroyed || stream._writeStopped) { reject(endedError(stream)); return; }
677
+ if (stream.destroyed || stream._writeStopped) { reject(stream._sessionError || endedError(stream)); return; }
422
678
  stream.end(resolve);
423
679
  });
424
680
  },
425
- abort: function (reason) { stream.resetStream(codeOf(reason)); }
426
- });
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 };
427
693
  }
428
694
 
429
695
  // ---- Opening a stream of our own ----
@@ -515,69 +781,225 @@ function wtScheduling(quic, sessionStreamId) {
515
781
  // queue: their age limit (outgoingMaxAge) and how
516
782
  // many were dropped unsent (sendDatagram's owner)
517
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
518
788
 
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).
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.
525
826
  function createDatagrams(session) {
526
- var queue = [], controller = null, pull = null, readable = null, writable = null;
527
- var dg = { incomingMaxAge: null, incomingHighWaterMark: 256, outgoingHighWaterMark: 256 };
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;
528
835
 
529
836
  function deliver() {
530
837
  if (!pull) return;
531
838
  var now = Date.now();
532
839
  while (queue.length > 0) {
533
840
  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();
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();
537
857
  return;
538
858
  }
539
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
+
540
891
  function makeWritable(options) {
541
- var w = new WritableStream({
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; },
542
900
  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;
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);
550
932
  return w;
551
933
  }
552
934
 
553
935
  Object.defineProperty(dg, 'readable', { enumerable: true, get: function () {
554
936
  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 });
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 });
559
946
  }
560
947
  return readable;
561
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
+ };
562
956
  Object.defineProperty(dg, 'writable', { enumerable: true, get: function () {
563
- return writable || (writable = makeWritable());
957
+ return legacy || (legacy = makeWritable());
564
958
  } });
565
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'); } });
566
963
  Object.defineProperty(dg, 'outgoingMaxAge', { enumerable: true,
567
964
  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); };
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
+
570
979
  // A datagram arrived: for the readable, once anything reads it.
571
980
  dg._push = function (data) {
572
- if (!readable) return;
981
+ if (!readable || ended) return;
573
982
  queue.push({ data: data, t: Date.now() });
574
- while (queue.length > dg.incomingHighWaterMark) { queue.shift(); session.dgStats.droppedIncoming++; }
983
+ while (queue.length > inMax) { queue.shift(); session.dgStats.droppedIncoming++; }
575
984
  deliver();
576
985
  };
577
- dg._close = function () {
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 };
578
994
  queue = [];
579
- if (controller) { try { controller.close(); } catch (e) {} }
580
- if (pull) { var p = pull; pull = null; p(); }
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(); }
581
1003
  };
582
1004
  return dg;
583
1005
  }
@@ -587,10 +1009,17 @@ function createDatagrams(session) {
587
1009
  function sessionStats(session) {
588
1010
  var q = session.quic && session.quic.getStats ? session.quic.getStats() : {};
589
1011
  var out = { timestamp: Date.now() };
590
- ['bytesSent', 'packetsSent', 'packetsLost', 'bytesReceived', 'packetsReceived', 'smoothedRtt', 'rttVariation',
1012
+ ['packetsSent', 'packetsLost', 'bytesLost', 'bytesReceived', 'packetsReceived', 'smoothedRtt', 'rttVariation',
591
1013
  'minRtt', 'estimatedSendRate', 'atSendCapacity', 'congestionWindow', 'bytesInFlight'].forEach(function (k) {
592
1014
  out[k] = q[k] !== undefined ? q[k] : null;
593
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;
594
1023
  out.datagrams = {
595
1024
  droppedIncoming: session.dgStats.droppedIncoming,
596
1025
  expiredIncoming: session.dgStats.expiredIncoming,
@@ -600,19 +1029,47 @@ function sessionStats(session) {
600
1029
  return Promise.resolve(out);
601
1030
  }
602
1031
 
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
- };
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();
616
1070
  }
617
1071
 
618
- export { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, WebTransportError };
1072
+ export { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, setErrorCode,
1073
+ exporterArgs, exportSessionKeyingMaterial,
1074
+ WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
1075
+ WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream };