quico 0.7.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.
- package/README.md +245 -31
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +7 -4
- package/src/connect_udp_client.js +9 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +29 -60
- package/src/h3_client.js +24 -5
- package/src/h3_server.js +101 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/quic_connection.js +200 -23
- package/src/quic_server.js +94 -13
- package/src/server.js +266 -22
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +4 -0
- package/src/webtransport.js +424 -54
- package/src/ws.js +120 -0
- package/src/ws_client.js +68 -88
- package/src/ws_server.js +35 -53
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +533 -76
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
|
-
|
|
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
|
-
|
|
184
|
-
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));
|
|
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
|
-
|
|
220
|
+
writeView = webWritableOf(stream, sendStats, quic, wtStreamId);
|
|
221
|
+
webWritable = writeView.stream;
|
|
194
222
|
sendProps(webWritable);
|
|
195
|
-
|
|
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
|
-
//
|
|
354
|
-
//
|
|
355
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
381
|
-
|
|
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) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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,
|
|
527
|
-
var
|
|
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 (
|
|
535
|
-
|
|
536
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
544
|
-
var u8 = chunk
|
|
545
|
-
if (
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
556
|
-
start: function (c) {
|
|
557
|
-
|
|
558
|
-
|
|
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
|
|
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
|
|
569
|
-
|
|
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 >
|
|
983
|
+
while (queue.length > inMax) { queue.shift(); session.dgStats.droppedIncoming++; }
|
|
575
984
|
deliver();
|
|
576
985
|
};
|
|
577
|
-
|
|
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
|
-
|
|
580
|
-
if (
|
|
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
|
-
['
|
|
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
|
-
//
|
|
604
|
-
//
|
|
605
|
-
function createSendGroup() {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
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,
|
|
1072
|
+
export { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats, setErrorCode,
|
|
1073
|
+
exporterArgs, exportSessionKeyingMaterial,
|
|
1074
|
+
WebTransportError, WebTransportBidirectionalStream, WebTransportReceiveStream, WebTransportSendStream,
|
|
1075
|
+
WebTransportWriter, WebTransportSendGroup, WebTransportDatagramsWritable, WebTransportDatagramDuplexStream };
|