mirror of
https://github.com/denoland/deno.git
synced 2025-02-01 12:16:11 -05:00
0098fddb10
Initial implementation of WebTransport client and server! This is very unstable because the interface should eventually shift to use hyper (h3 is on the [2025 roadmap](https://hyper.rs/contrib/roadmap/)) instead of manually messing with the the protocol, which will enable integration with Deno.serveHttp/etc and allow WebTransport over h2. This will also let us expose multiplexing. WebTransport stats will be a followup due to their complexity. Fixes: https://github.com/denoland/deno/issues/9017
1123 lines
30 KiB
JavaScript
1123 lines
30 KiB
JavaScript
// Copyright 2018-2025 the Deno authors. MIT license.
|
|
|
|
import { primordials } from "ext:core/mod.js";
|
|
import * as webidl from "ext:deno_webidl/00_webidl.js";
|
|
import {
|
|
connectQuic,
|
|
webtransportAccept,
|
|
webtransportConnect,
|
|
} from "ext:deno_net/03_quic.js";
|
|
import { assert } from "ext:deno_web/00_infra.js";
|
|
import { DOMException } from "ext:deno_web/01_dom_exception.js";
|
|
import {
|
|
getReadableStreamResourceBacking,
|
|
getWritableStreamResourceBacking,
|
|
ReadableStream,
|
|
readableStreamForRid,
|
|
WritableStream,
|
|
WritableStreamDefaultWriter,
|
|
writableStreamForRid,
|
|
} from "ext:deno_web/06_streams.js";
|
|
import { getLocationHref } from "ext:deno_web/12_location.js";
|
|
|
|
const {
|
|
ArrayBuffer,
|
|
ArrayBufferPrototype,
|
|
ArrayBufferIsView,
|
|
ArrayPrototypeConcat,
|
|
ArrayPrototypeShift,
|
|
ArrayPrototypePush,
|
|
DataView,
|
|
DataViewPrototype,
|
|
DataViewPrototypeGetUint16,
|
|
DataViewPrototypeGetUint32,
|
|
DataViewPrototypeGetBigUint64,
|
|
DataViewPrototypeSetUint16,
|
|
DataViewPrototypeSetUint32,
|
|
DataViewPrototypeSetBigUint64,
|
|
DateNow,
|
|
BigInt,
|
|
Number,
|
|
ObjectPrototypeIsPrototypeOf,
|
|
Promise,
|
|
PromiseReject,
|
|
PromiseResolve,
|
|
PromisePrototypeThen,
|
|
PromisePrototypeCatch,
|
|
RangeError,
|
|
ReflectConstruct,
|
|
Symbol,
|
|
TypedArrayPrototypeGetBuffer,
|
|
TypedArrayPrototypeGetByteLength,
|
|
TypedArrayPrototypeGetByteOffset,
|
|
TypeError,
|
|
Uint8Array,
|
|
} = primordials;
|
|
|
|
const MAX_PRIORITY = 2_147_483_647;
|
|
const BI_WEBTRANSPORT = 0x41n;
|
|
const UNI_WEBTRANSPORT = 0x54n;
|
|
const UNI_QPACK_ENCODER = 0x02n;
|
|
const UNI_QPACK_DECODER = 0x03n;
|
|
|
|
function encodeVarint(x) {
|
|
x = BigInt(x);
|
|
if (x < 2n ** 6n) {
|
|
return new Uint8Array([Number(x)]);
|
|
}
|
|
if (x < 2n ** 14n) {
|
|
const s = Number(0b01n << 14n | x);
|
|
const a = new Uint8Array(2);
|
|
const v = new DataView(TypedArrayPrototypeGetBuffer(a));
|
|
DataViewPrototypeSetUint16(v, 0, s, false);
|
|
return a;
|
|
}
|
|
if (x < 2n ** 30n) {
|
|
const s = Number(0b10n << 30n | x);
|
|
const a = new Uint8Array(4);
|
|
const v = new DataView(TypedArrayPrototypeGetBuffer(a));
|
|
DataViewPrototypeSetUint32(v, 0, s, false);
|
|
return a;
|
|
}
|
|
if (x < 2n ** 62n) {
|
|
const s = 0b11n << 62n | x;
|
|
const a = new Uint8Array(8);
|
|
const v = new DataView(TypedArrayPrototypeGetBuffer(a));
|
|
DataViewPrototypeSetBigUint64(v, 0, s, false);
|
|
return a;
|
|
}
|
|
throw new RangeError("invalid varint");
|
|
}
|
|
|
|
function decodeVarint(data) {
|
|
const view = new DataView(
|
|
TypedArrayPrototypeGetBuffer(data),
|
|
TypedArrayPrototypeGetByteOffset(data),
|
|
TypedArrayPrototypeGetByteLength(data),
|
|
);
|
|
const tag = data[0] >> 6;
|
|
switch (tag) {
|
|
case 0b00:
|
|
return BigInt(data[0]);
|
|
case 0b01: {
|
|
const v = DataViewPrototypeGetUint16(view, 0, false);
|
|
return BigInt(v) & ((2n ** 12n) - 1n);
|
|
}
|
|
case 0b10: {
|
|
const v = DataViewPrototypeGetUint32(view, 0, false);
|
|
return BigInt(v) & ((2n ** 28n) - 1n);
|
|
}
|
|
case 0b11: {
|
|
const v = DataViewPrototypeGetBigUint64(view, 0, false);
|
|
return v & ((2n ** 60n) - 1n);
|
|
}
|
|
default:
|
|
throw new RangeError();
|
|
}
|
|
}
|
|
|
|
async function readVarint(reader) {
|
|
const buf = new ArrayBuffer(8);
|
|
const { value: b1 } = await reader.read(new Uint8Array(buf, 0, 1));
|
|
const size = 1 << (b1[0] >> 6);
|
|
if (size === 1) {
|
|
return decodeVarint(b1);
|
|
}
|
|
const { value: bn } = await reader.read(
|
|
new Uint8Array(TypedArrayPrototypeGetBuffer(b1), 1, size - 1),
|
|
);
|
|
return decodeVarint(
|
|
new Uint8Array(TypedArrayPrototypeGetBuffer(bn, 0, size)),
|
|
);
|
|
}
|
|
|
|
function equal(a, b) {
|
|
if (a.length !== b.length) return false;
|
|
for (let i = 0; i < a.length; i += 1) {
|
|
if (a[i] !== b[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function concat(a, b) {
|
|
const c = new Uint8Array(a.length + b.length);
|
|
c.set(a, 0);
|
|
c.set(b, a.length);
|
|
return c;
|
|
}
|
|
|
|
const illegalConstructorKey = Symbol("illegalConstructorKey");
|
|
|
|
class WebTransport {
|
|
[webidl.brand] = webidl.brand;
|
|
#conn;
|
|
#promise;
|
|
#ready;
|
|
// deno-lint-ignore prefer-primordials
|
|
#closed = Promise.withResolvers();
|
|
#settingsTx;
|
|
#settingsRx;
|
|
#connect;
|
|
#headerUni;
|
|
#headerBi;
|
|
#reliability = "pending";
|
|
#congestionControl = "default";
|
|
#anticipatedConcurrentIncomingBidirectionalStreams = null;
|
|
#anticipatedConcurrentIncomingUnidirectionalStreams = null;
|
|
#incomingBidirectionalStreams;
|
|
#incomingUnidirectionalStreams;
|
|
#datagrams;
|
|
|
|
// endpoint may incorrectly open qpack streams.
|
|
// if so, we need to hold on to them so they
|
|
// aren't closed, even though we will never do
|
|
// anything with them :(
|
|
#qpackEncoder;
|
|
#qpackDecoder;
|
|
|
|
constructor(url, options) {
|
|
let promise;
|
|
|
|
if (url === illegalConstructorKey) {
|
|
promise = PromiseResolve(options);
|
|
} else {
|
|
const prefix = "Failed to construct 'WebTransport'";
|
|
webidl.requiredArguments(arguments.length, 1, prefix);
|
|
url = webidl.converters.USVString(url, prefix, "Argument 1");
|
|
options = webidl.converters.WebTransportOptions(
|
|
options,
|
|
prefix,
|
|
"Argument 2",
|
|
);
|
|
|
|
let parsedURL;
|
|
try {
|
|
parsedURL = new URL(url, getLocationHref());
|
|
} catch (e) {
|
|
throw new DOMException(e.message, "SyntaxError");
|
|
}
|
|
|
|
switch (options.congestionControl) {
|
|
case "throughput":
|
|
this.#congestionControl = "throughput";
|
|
break;
|
|
case "low-latency":
|
|
this.#congestionControl = "low-latency";
|
|
break;
|
|
default:
|
|
this.#congestionControl = "default";
|
|
}
|
|
this.#anticipatedConcurrentIncomingBidirectionalStreams =
|
|
options.anticipatedConcurrentIncomingBidirectionalStreams;
|
|
this.#anticipatedConcurrentIncomingUnidirectionalStreams =
|
|
options.anticipatedConcurrentIncomingUnidirectionalStreams;
|
|
|
|
promise = PromisePrototypeThen(
|
|
connectQuic({
|
|
hostname: parsedURL.hostname,
|
|
port: Number(parsedURL.port) || 443,
|
|
keepAliveInterval: 4e3,
|
|
maxIdleTimeout: 10e3,
|
|
congestionControl: options.congestionControl,
|
|
alpnProtocols: ["h3"],
|
|
serverCertificateHashes: options.serverCertificateHashes,
|
|
}),
|
|
async (conn) => {
|
|
const { connect, settingsTx, settingsRx } = await webtransportConnect(
|
|
conn,
|
|
// deno-lint-ignore prefer-primordials
|
|
parsedURL.toString(),
|
|
);
|
|
|
|
return {
|
|
conn,
|
|
connect,
|
|
settingsTx,
|
|
settingsRx,
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
PromisePrototypeCatch(promise, () => this.#closed.resolve());
|
|
|
|
promise = PromisePrototypeThen(
|
|
promise,
|
|
({ conn, connect, settingsTx, settingsRx }) => {
|
|
this.#conn = conn;
|
|
this.#closed.resolve(conn.closed);
|
|
|
|
const sessionId = connect.writable.id;
|
|
const sessionIdBuf = encodeVarint(sessionId);
|
|
this.#headerBi = concat(encodeVarint(BI_WEBTRANSPORT), sessionIdBuf);
|
|
this.#headerUni = concat(encodeVarint(UNI_WEBTRANSPORT), sessionIdBuf);
|
|
|
|
this.#settingsTx = settingsTx;
|
|
this.#settingsRx = settingsRx;
|
|
this.#connect = connect;
|
|
|
|
this.#reliability = "supports-unreliable";
|
|
|
|
return { conn, sessionId, sessionIdBuf };
|
|
},
|
|
);
|
|
|
|
this.#promise = promise;
|
|
this.#datagrams = new WebTransportDatagramDuplexStream(
|
|
illegalConstructorKey,
|
|
promise,
|
|
);
|
|
this.#ready = PromisePrototypeThen(promise, () => undefined, (e) => {
|
|
throw new WebTransportError(e.message);
|
|
});
|
|
}
|
|
|
|
getStats() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
return PromiseResolve({
|
|
bytesSent: 0,
|
|
packetsSent: 0,
|
|
bytesLost: 0,
|
|
packetsLost: 0,
|
|
bytesReceived: 0,
|
|
packetsReceived: 0,
|
|
smoothedRtt: 0,
|
|
rttVariation: 0,
|
|
minRtt: 0,
|
|
estimatedSendRate: null,
|
|
atSendCapacity: false,
|
|
});
|
|
}
|
|
|
|
get ready() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
return this.#ready;
|
|
}
|
|
|
|
get reliability() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
return this.#reliability;
|
|
}
|
|
|
|
get congestionControl() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
return this.#congestionControl;
|
|
}
|
|
|
|
get anticipatedConcurrentIncomingUnidirectionalStreams() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
return this.#anticipatedConcurrentIncomingUnidirectionalStreams;
|
|
}
|
|
|
|
get anticipatedConcurrentIncomingBidirectionalStreams() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
return this.#anticipatedConcurrentIncomingBidirectionalStreams;
|
|
}
|
|
|
|
get closed() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
return this.#closed.promise;
|
|
}
|
|
|
|
close(closeInfo) {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
closeInfo = webidl.converters.WebTransportCloseInfo(
|
|
closeInfo,
|
|
"Failed to execute 'close' on 'WebTransport'",
|
|
"Argument 1",
|
|
);
|
|
if (!this.#conn) {
|
|
throw new WebTransportError("WebTransport is not connected", {
|
|
source: "session",
|
|
});
|
|
}
|
|
this.#conn.close({
|
|
closeCode: closeInfo.closeCode,
|
|
reason: closeInfo.reason,
|
|
});
|
|
}
|
|
|
|
get datagrams() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
return this.#datagrams;
|
|
}
|
|
|
|
async createBidirectionalStream(options) {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
options = webidl.converters.WebTransportSendStreamOptions(
|
|
options,
|
|
"Failed to execute 'createBidirectionalStream' on 'WebTransport'",
|
|
"Argument 1",
|
|
);
|
|
|
|
const { conn } = await this.#promise;
|
|
const bidi = await conn.createBidirectionalStream({
|
|
waitUntilAvailable: options.waitUntilAvailable,
|
|
});
|
|
|
|
bidi.writable.sendOrder = MAX_PRIORITY;
|
|
const writer = bidi.writable.getWriter();
|
|
await writer.write(this.#headerBi);
|
|
writer.releaseLock();
|
|
bidi.writable.sendOrder = options.sendOrder || 0;
|
|
|
|
const wrapper = new WebTransportBidirectionalStream(
|
|
illegalConstructorKey,
|
|
bidi,
|
|
);
|
|
if (options.sendGroup) {
|
|
wrapper.writable.sendGroup = options.sendGroup;
|
|
}
|
|
|
|
return wrapper;
|
|
}
|
|
|
|
get incomingBidirectionalStreams() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
if (!this.#incomingBidirectionalStreams) {
|
|
const readerPromise = PromisePrototypeThen(
|
|
this.#promise,
|
|
({ conn, sessionId }) => ({
|
|
sessionId,
|
|
reader: conn.incomingBidirectionalStreams.getReader(),
|
|
}),
|
|
);
|
|
this.#incomingBidirectionalStreams = new ReadableStream({
|
|
pull: async (controller) => {
|
|
const { sessionId, reader } = await readerPromise;
|
|
const { value: bidi, done } = await reader.read();
|
|
if (done) {
|
|
controller.close();
|
|
} else {
|
|
const reader = bidi.readable.getReader({ mode: "byob" });
|
|
try {
|
|
const type = await readVarint(reader);
|
|
if (type !== BI_WEBTRANSPORT) return;
|
|
const sid = await readVarint(reader);
|
|
if (sid !== sessionId) return;
|
|
controller.enqueue(
|
|
new WebTransportBidirectionalStream(
|
|
illegalConstructorKey,
|
|
bidi,
|
|
),
|
|
);
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
}
|
|
},
|
|
});
|
|
}
|
|
return this.#incomingBidirectionalStreams;
|
|
}
|
|
|
|
async createUnidirectionalStream(options) {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
options = webidl.converters.WebTransportSendStreamOptions(
|
|
options,
|
|
"Failed to execute 'createUnidirectionalStream' on 'WebTransport'",
|
|
"Argument 1",
|
|
);
|
|
|
|
const { conn } = await this.#promise;
|
|
const stream = await conn.createUnidirectionalStream({
|
|
waitUntilAvailable: options.waitUntilAvailable,
|
|
});
|
|
|
|
stream.sendOrder = MAX_PRIORITY;
|
|
const writer = stream.getWriter();
|
|
await writer.write(this.#headerUni);
|
|
writer.releaseLock();
|
|
stream.sendOrder = options.sendOrder || 0;
|
|
|
|
const wrapper = writableStream(stream);
|
|
if (options.sendGroup) {
|
|
wrapper.sendGroup = options.sendGroup;
|
|
}
|
|
|
|
return wrapper;
|
|
}
|
|
|
|
get incomingUnidirectionalStreams() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
|
|
if (!this.#incomingUnidirectionalStreams) {
|
|
const readerPromise = PromisePrototypeThen(
|
|
this.#promise,
|
|
({ conn, sessionId }) => ({
|
|
sessionId,
|
|
reader: conn.incomingUnidirectionalStreams.getReader(),
|
|
}),
|
|
);
|
|
this.#incomingUnidirectionalStreams = new ReadableStream({
|
|
pull: async (controller) => {
|
|
const { reader, sessionId } = await readerPromise;
|
|
const { value: stream, done } = await reader.read();
|
|
if (done) {
|
|
controller.close();
|
|
} else {
|
|
const reader = stream.getReader({ mode: "byob" });
|
|
try {
|
|
const type = await readVarint(reader);
|
|
switch (type) {
|
|
case UNI_QPACK_ENCODER:
|
|
this.#qpackEncoder = stream;
|
|
return;
|
|
case UNI_QPACK_DECODER:
|
|
this.#qpackDecoder = stream;
|
|
return;
|
|
case UNI_WEBTRANSPORT: {
|
|
const sid = await readVarint(reader);
|
|
if (sid === sessionId) {
|
|
controller.enqueue(
|
|
readableStream(stream),
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
return this.#incomingUnidirectionalStreams;
|
|
}
|
|
|
|
createSendGroup() {
|
|
webidl.assertBranded(this, WebTransportPrototype);
|
|
|
|
return new WebTransportSendGroup(illegalConstructorKey);
|
|
}
|
|
|
|
static get supportsReliableOnly() {
|
|
return false;
|
|
}
|
|
}
|
|
webidl.configureInterface(WebTransport);
|
|
const WebTransportPrototype = WebTransport.prototype;
|
|
|
|
async function upgradeWebTransport(conn) {
|
|
const { url, connect, settingsTx, settingsRx } = await webtransportAccept(
|
|
conn,
|
|
);
|
|
const wt = new WebTransport(illegalConstructorKey, {
|
|
conn,
|
|
connect,
|
|
settingsTx,
|
|
settingsRx,
|
|
});
|
|
wt.url = url;
|
|
return wt;
|
|
}
|
|
|
|
function readableStream(stream) {
|
|
return readableStreamForRid(
|
|
getReadableStreamResourceBacking(stream).rid,
|
|
false, // input stream already has cleanup
|
|
(...args) =>
|
|
ReflectConstruct(
|
|
WebTransportReceiveStream,
|
|
ArrayPrototypeConcat(args, [illegalConstructorKey, stream]),
|
|
),
|
|
);
|
|
}
|
|
|
|
function writableStream(stream) {
|
|
return writableStreamForRid(
|
|
getWritableStreamResourceBacking(stream).rid,
|
|
false, // input stream already has cleanup
|
|
(...args) =>
|
|
ReflectConstruct(
|
|
WebTransportSendStream,
|
|
ArrayPrototypeConcat(args, [illegalConstructorKey, stream]),
|
|
),
|
|
);
|
|
}
|
|
|
|
class WebTransportBidirectionalStream {
|
|
[webidl.brand] = webidl.brand;
|
|
#inner;
|
|
#readable;
|
|
#writable;
|
|
|
|
constructor(key, inner) {
|
|
if (key !== illegalConstructorKey) {
|
|
webidl.illegalConstructor();
|
|
}
|
|
this.#inner = inner;
|
|
}
|
|
|
|
get readable() {
|
|
webidl.assertBranded(this, WebTransportBidirectionalStreamPrototype);
|
|
if (!this.#readable) {
|
|
this.#readable = readableStream(this.#inner.readable);
|
|
}
|
|
return this.#readable;
|
|
}
|
|
|
|
get writable() {
|
|
webidl.assertBranded(this, WebTransportBidirectionalStreamPrototype);
|
|
if (!this.#writable) {
|
|
this.#writable = writableStream(this.#inner.writable);
|
|
}
|
|
return this.#writable;
|
|
}
|
|
}
|
|
webidl.configureInterface(WebTransportBidirectionalStream);
|
|
const WebTransportBidirectionalStreamPrototype =
|
|
WebTransportBidirectionalStream.prototype;
|
|
|
|
class WebTransportSendStream extends WritableStream {
|
|
[webidl.brand] = webidl.brand;
|
|
#inner;
|
|
#sendGroup = null;
|
|
|
|
constructor(brand, key, inner) {
|
|
if (key !== illegalConstructorKey) {
|
|
webidl.illegalConstructor();
|
|
}
|
|
super(brand);
|
|
this.#inner = inner;
|
|
}
|
|
|
|
get sendGroup() {
|
|
webidl.assertBranded(this, WebTransportSendStreamPrototype);
|
|
return this.#sendGroup;
|
|
}
|
|
|
|
set sendGroup(value) {
|
|
webidl.assertBranded(this, WebTransportSendStreamPrototype);
|
|
value = webidl.converters.WebTransportSendGroup(
|
|
value,
|
|
"Failed to execute 'sendGroup' on 'WebTransportSendStream'",
|
|
);
|
|
this.#sendGroup = value;
|
|
}
|
|
|
|
get sendOrder() {
|
|
webidl.assertBranded(this, WebTransportSendStreamPrototype);
|
|
return this.#inner.sendOrder;
|
|
}
|
|
|
|
set sendOrder(sendOrder) {
|
|
webidl.assertBranded(this, WebTransportSendStreamPrototype);
|
|
sendOrder = webidl.converters["long long"](
|
|
sendOrder,
|
|
"Failed to execute 'sendOrder' on 'WebTransportSendStream'",
|
|
);
|
|
this.#inner.sendOrder = sendOrder;
|
|
}
|
|
|
|
getStats() {
|
|
webidl.assertBranded(this, WebTransportSendStreamPrototype);
|
|
return PromiseResolve({
|
|
bytesWritten: 0,
|
|
bytesSent: 0,
|
|
bytesAcknowledged: 0,
|
|
});
|
|
}
|
|
|
|
getWriter() {
|
|
webidl.assertBranded(this, WebTransportSendStreamPrototype);
|
|
return new WebTransportWriter(this);
|
|
}
|
|
}
|
|
webidl.configureInterface(WebTransportSendStream);
|
|
const WebTransportSendStreamPrototype = WebTransportSendStream.prototype;
|
|
|
|
class WebTransportReceiveStream extends ReadableStream {
|
|
[webidl.brand] = webidl.brand;
|
|
#inner;
|
|
|
|
constructor(brand, key, inner) {
|
|
if (key !== illegalConstructorKey) {
|
|
webidl.illegalConstructor();
|
|
}
|
|
super(brand);
|
|
this.#inner = inner;
|
|
}
|
|
|
|
getStats() {
|
|
webidl.assertBranded(this, WebTransportReceiveStreamPrototype);
|
|
return PromiseResolve({
|
|
bytesReceived: 0,
|
|
bytesRead: 0,
|
|
});
|
|
}
|
|
}
|
|
webidl.configureInterface(WebTransportReceiveStream);
|
|
const WebTransportReceiveStreamPrototype = WebTransportReceiveStream.prototype;
|
|
|
|
class WebTransportWriter extends WritableStreamDefaultWriter {
|
|
[webidl.brand] = webidl.brand;
|
|
}
|
|
webidl.configureInterface(WebTransportWriter);
|
|
|
|
class WebTransportDatagramDuplexStream {
|
|
[webidl.brand] = webidl.brand;
|
|
#promise;
|
|
#conn;
|
|
#sessionIdBuf;
|
|
#readable;
|
|
#readableController;
|
|
#writable;
|
|
#incomingMaxAge = Infinity;
|
|
#outgoingMaxAge = Infinity;
|
|
#incomingHighWaterMark = 1;
|
|
#outgoingHighWaterMark = 5;
|
|
#incomingDatagramsPullPromise = null;
|
|
#incomingDatagramsQueue = [];
|
|
#outgoingDatagramsQueue = [];
|
|
#sending = false;
|
|
|
|
constructor(key, promise) {
|
|
if (key !== illegalConstructorKey) {
|
|
webidl.illegalConstructor();
|
|
}
|
|
|
|
this.#promise = promise;
|
|
PromisePrototypeThen(promise, ({ conn, sessionIdBuf }) => {
|
|
this.#conn = conn;
|
|
this.#sessionIdBuf = sessionIdBuf;
|
|
});
|
|
|
|
this.#receiveDatagrams();
|
|
}
|
|
|
|
async #receiveDatagrams() {
|
|
const { conn, sessionIdBuf } = await this.#promise;
|
|
while (true) {
|
|
const queue = this.#incomingDatagramsQueue;
|
|
const duration = this.#incomingMaxAge ?? Infinity;
|
|
|
|
let datagram;
|
|
try {
|
|
datagram = await conn.readDatagram();
|
|
} catch {
|
|
break;
|
|
}
|
|
if (!equal(datagram.subarray(0, sessionIdBuf.length), sessionIdBuf)) {
|
|
continue;
|
|
}
|
|
datagram = datagram.subarray(sessionIdBuf.length);
|
|
|
|
ArrayPrototypePush(queue, { datagram, timestamp: DateNow() });
|
|
|
|
const toBeRemoved = queue.length - this.#incomingHighWaterMark;
|
|
while (toBeRemoved > 0) {
|
|
ArrayPrototypeShift(queue);
|
|
}
|
|
|
|
while (queue.length > 0) {
|
|
const { timestamp } = queue[0];
|
|
if (DateNow() - timestamp > duration) {
|
|
ArrayPrototypeShift(queue);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (queue.length > 0 && this.#incomingDatagramsPullPromise) {
|
|
const { datagram } = ArrayPrototypeShift(queue);
|
|
const promise = this.#incomingDatagramsPullPromise;
|
|
this.#incomingDatagramsPullPromise = null;
|
|
this.#readableController.enqueue(datagram);
|
|
promise.resolve(undefined);
|
|
}
|
|
}
|
|
}
|
|
|
|
async #sendDatagrams() {
|
|
if (this.#sending) return;
|
|
this.#sending = true;
|
|
const { conn, sessionIdBuf } = await this.#promise;
|
|
|
|
const queue = this.#outgoingDatagramsQueue;
|
|
const duration = this.#outgoingMaxAge ?? Infinity;
|
|
while (queue.length > 0) {
|
|
const { bytes, timestamp, promise } = ArrayPrototypeShift(queue);
|
|
|
|
if (DateNow() - timestamp > duration) {
|
|
promise.resolve(undefined);
|
|
continue;
|
|
}
|
|
|
|
if (bytes.length <= this.maxDatagramSize) {
|
|
const datagram = concat(sessionIdBuf, bytes);
|
|
try {
|
|
await conn.sendDatagram(datagram);
|
|
} catch {
|
|
break;
|
|
}
|
|
}
|
|
|
|
promise.resolve(undefined);
|
|
}
|
|
|
|
this.#sending = false;
|
|
}
|
|
|
|
get incomingMaxAge() {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
return this.#incomingMaxAge;
|
|
}
|
|
|
|
set incomingMaxAge(value) {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
value = webidl.converters["unrestricted double?"](
|
|
value,
|
|
"Failed to execute 'incomingMaxAge' on 'WebTransportDatagramDuplexStream'",
|
|
);
|
|
if (value < 0 || NumberIsNaN(value)) {
|
|
throw new RangeError("incomingMaxAge cannot be negative");
|
|
}
|
|
if (value === 0) value = null;
|
|
this.#incomingMaxAge = value;
|
|
}
|
|
|
|
get outgoingMaxAge() {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
return this.#outgoingMaxAge;
|
|
}
|
|
|
|
set outgoingMaxAge(value) {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
value = webidl.converters["unrestricted double?"](
|
|
value,
|
|
"Failed to execute 'outgoingMaxAge' on 'WebTransportDatagramDuplexStream'",
|
|
);
|
|
if (value < 0 || NumberIsNaN(value)) {
|
|
throw new RangeError("outgoingMaxAge cannot be negative");
|
|
}
|
|
if (value === 0) value = null;
|
|
this.#outgoingMaxAge = value;
|
|
}
|
|
|
|
get incomingHighWaterMark() {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
return this.#incomingHighWaterMark;
|
|
}
|
|
|
|
set incomingHighWaterMark(value) {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
value = webidl.converters["unrestricted double"](
|
|
value,
|
|
"Failed to execute 'incomingHighWaterMark' on 'WebTransportDatagramDuplexStream'",
|
|
);
|
|
if (value < 0 || NumberIsNaN(value)) {
|
|
throw new RangeError("incomingHighWaterMark cannot be negative");
|
|
}
|
|
if (value < 1) value = 1;
|
|
this.#incomingHighWaterMark = value;
|
|
}
|
|
|
|
get outgoingHighWaterMark() {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
return this.#outgoingHighWaterMark;
|
|
}
|
|
|
|
set outgoingHighWaterMark(value) {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
value = webidl.converters["unrestricted double"](
|
|
value,
|
|
"Failed to execute 'outgoingHighWaterMark' on 'WebTransportDatagramDuplexStream'",
|
|
);
|
|
if (value < 0 || NumberIsNaN(value)) {
|
|
throw new RangeError("outgoingHighWaterMark cannot be negative");
|
|
}
|
|
if (value < 1) value = 1;
|
|
this.#outgoingHighWaterMark = value;
|
|
}
|
|
|
|
get maxDatagramSize() {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
if (this.#conn) {
|
|
return this.#conn.maxDatagramSize - this.#sessionIdBuf.length;
|
|
}
|
|
return 1024;
|
|
}
|
|
|
|
get readable() {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
if (!this.#readable) {
|
|
this.#readable = new ReadableStream({
|
|
type: "bytes",
|
|
start: (controller) => {
|
|
PromisePrototypeThen(
|
|
PromisePrototypeThen(this.#promise, ({ conn }) => conn.closed),
|
|
() => {
|
|
try {
|
|
controller.close();
|
|
} catch {
|
|
// nothing
|
|
}
|
|
},
|
|
);
|
|
this.#readableController = controller;
|
|
},
|
|
pull: (controller) => {
|
|
assert(this.#incomingDatagramsPullPromise === null);
|
|
const queue = this.#incomingDatagramsQueue;
|
|
if (queue.length === 0) {
|
|
// deno-lint-ignore prefer-primordials
|
|
this.#incomingDatagramsPullPromise = Promise.withResolvers();
|
|
return this.#incomingDatagramsPullPromise.promise;
|
|
}
|
|
const { datagram } = ArrayPrototypeShift(queue);
|
|
if (controller.byobRequest) {
|
|
const view = controller.byobRequest.view;
|
|
if (
|
|
ObjectPrototypeIsPrototypeOf(DataViewPrototype, view) ||
|
|
TypedArrayPrototypeGetLength(view) < datagram.length
|
|
) {
|
|
return PromiseReject(
|
|
new RangeError("BYOB view is not large enough for datagram"),
|
|
);
|
|
}
|
|
if (view.constructor.BYTES_PER_ELEMENT !== 1) {
|
|
return PromiseReject(
|
|
new TypeError("BYOB view must be Uint8Array"),
|
|
);
|
|
}
|
|
view.set(datagram);
|
|
controller.byobRequest.respond(datagram.length);
|
|
} else {
|
|
controller.enqueue(datagram);
|
|
}
|
|
return PromiseResolve(undefined);
|
|
},
|
|
}, { highWaterMark: 0 });
|
|
}
|
|
return this.#readable;
|
|
}
|
|
|
|
get writable() {
|
|
webidl.assertBranded(this, WebTransportDatagramDuplexStreamPrototype);
|
|
if (!this.#writable) {
|
|
this.#writable = new WritableStream({
|
|
write: (data) => {
|
|
if (
|
|
!(ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, data) ||
|
|
ArrayBufferIsView(data))
|
|
) {
|
|
return PromiseReject(new TypeError("Invalid BYOB view"));
|
|
}
|
|
if (data.length > this.maxDatagramSize) {
|
|
return PromiseResolve(undefined);
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const bytes = new Uint8Array(data.length);
|
|
bytes.set(data);
|
|
const chunk = {
|
|
bytes,
|
|
timestamp: DateNow(),
|
|
promise: { resolve, reject },
|
|
};
|
|
ArrayPrototypePush(this.#outgoingDatagramsQueue, chunk);
|
|
if (
|
|
this.#outgoingDatagramsQueue.length < this.#outgoingHighWaterMark
|
|
) {
|
|
resolve(undefined);
|
|
}
|
|
this.#sendDatagrams();
|
|
});
|
|
},
|
|
});
|
|
}
|
|
return this.#writable;
|
|
}
|
|
}
|
|
webidl.configureInterface(WebTransportDatagramDuplexStream);
|
|
const WebTransportDatagramDuplexStreamPrototype =
|
|
WebTransportDatagramDuplexStream.prototype;
|
|
|
|
class WebTransportSendGroup {
|
|
constructor(key) {
|
|
if (key !== illegalConstructorKey) {
|
|
webidl.illegalConstructor();
|
|
}
|
|
}
|
|
|
|
getStats() {
|
|
webidl.assertBranded(this, WebTransportSendGroupPrototype);
|
|
return PromiseResolve({
|
|
bytesWritten: 0,
|
|
bytesSent: 0,
|
|
bytesAcknowledged: 0,
|
|
});
|
|
}
|
|
}
|
|
webidl.configureInterface(WebTransportSendGroup);
|
|
const WebTransportSendGroupPrototype = WebTransportSendGroup.prototype;
|
|
|
|
class WebTransportError extends DOMException {
|
|
#source;
|
|
#streamErrorCode;
|
|
|
|
constructor(message = "", init = { __proto__: null }) {
|
|
super(message, "WebTransportError");
|
|
this[webidl.brand] = webidl.brand;
|
|
|
|
init = webidl.converters["WebTransportErrorOptions"](
|
|
init,
|
|
"Failed to construct 'WebTransportError'",
|
|
"Argument 2",
|
|
);
|
|
|
|
this.#source = init.source;
|
|
this.#streamErrorCode = init.streamErrorCode;
|
|
}
|
|
|
|
get source() {
|
|
webidl.assertBranded(this, WebTransportErrorPrototype);
|
|
return this.#source;
|
|
}
|
|
|
|
get streamErrorCode() {
|
|
webidl.assertBranded(this, WebTransportErrorPrototype);
|
|
return this.#streamErrorCode;
|
|
}
|
|
}
|
|
webidl.configureInterface(WebTransportError);
|
|
const WebTransportErrorPrototype = WebTransportError.prototype;
|
|
|
|
webidl.converters.WebTransportSendGroup = webidl.createInterfaceConverter(
|
|
"WebTransportSendGroup",
|
|
WebTransportSendGroupPrototype,
|
|
);
|
|
|
|
webidl.converters.WebTransportSendStreamOptions = webidl
|
|
.createDictionaryConverter("WebTransportSendStreamOptions", [
|
|
{
|
|
key: "sendGroup",
|
|
converter: webidl.converters.WebTransportSendGroup,
|
|
},
|
|
{
|
|
key: "sendOrder",
|
|
converter: webidl.converters["long long"],
|
|
defaultValue: 0,
|
|
},
|
|
{
|
|
key: "waitUntilAvailable",
|
|
converter: webidl.converters.boolean,
|
|
defaultValue: false,
|
|
},
|
|
]);
|
|
|
|
webidl.converters.WebTransportCloseInfo = webidl.createDictionaryConverter(
|
|
"WebTransportCloseInfo",
|
|
[
|
|
{
|
|
key: "closeCode",
|
|
converter: webidl.converters["unsigned long"],
|
|
defaultValue: 0,
|
|
},
|
|
{
|
|
key: "reason",
|
|
converter: webidl.converters.USVString,
|
|
defaultValue: "",
|
|
},
|
|
],
|
|
);
|
|
|
|
webidl.converters.WebTransportHash = webidl.createDictionaryConverter(
|
|
"WebTransportHash",
|
|
[
|
|
{
|
|
key: "algorithm",
|
|
converter: webidl.converters.DOMString,
|
|
},
|
|
{
|
|
key: "value",
|
|
converter: webidl.converters.BufferSource,
|
|
},
|
|
],
|
|
);
|
|
|
|
webidl.converters["sequence<WebTransportHash>"] = webidl
|
|
.createSequenceConverter(webidl.converters.WebTransportHash);
|
|
|
|
webidl.converters.WebTransportCongestionControl = webidl.createEnumConverter(
|
|
"WebTransportCongestionControl",
|
|
[
|
|
"default",
|
|
"throughput",
|
|
"low-latency",
|
|
],
|
|
);
|
|
|
|
webidl.converters.WebTransportOptions = webidl
|
|
.createDictionaryConverter("WebTransportOptions", [
|
|
{
|
|
key: "allowPooling",
|
|
converter: webidl.converters.boolean,
|
|
defaultValue: false,
|
|
},
|
|
{
|
|
key: "requireUnreliable",
|
|
converter: webidl.converters.boolean,
|
|
defaultValue: false,
|
|
},
|
|
{
|
|
key: "serverCertificateHashes",
|
|
converter: webidl.converters["sequence<WebTransportHash>"],
|
|
},
|
|
{
|
|
key: "congestionControl",
|
|
converter: webidl.converters.WebTransportCongestionControl,
|
|
defaultValue: "default",
|
|
},
|
|
{
|
|
key: "anticipatedConcurrentIncomingUnidirectionalStreams",
|
|
converter: webidl.converters["unsigned short?"],
|
|
defaultValue: null,
|
|
},
|
|
{
|
|
key: "anticipatedConcurrentIncomingBidirectionalStreams",
|
|
converter: webidl.converters["unsigned short?"],
|
|
defaultValue: null,
|
|
},
|
|
{
|
|
key: "protocols",
|
|
converter: webidl.converters["sequence<DOMString>"],
|
|
defaultValue: [],
|
|
},
|
|
]);
|
|
|
|
webidl.converters.WebTransportErrorSource = webidl.createEnumConverter(
|
|
"WebTransportErrorSource",
|
|
["stream", "session"],
|
|
);
|
|
|
|
webidl.converters.WebTransportErrorOptions = webidl.createDictionaryConverter(
|
|
"WebTransportErrorOptions",
|
|
[
|
|
{
|
|
key: "source",
|
|
converter: webidl.converters.WebTransportErrorSource,
|
|
defaultValue: "stream",
|
|
},
|
|
{
|
|
key: "streamErrorCode",
|
|
converter: webidl.converters["unsigned long?"],
|
|
defaultValue: null,
|
|
},
|
|
],
|
|
);
|
|
|
|
export {
|
|
upgradeWebTransport,
|
|
WebTransport,
|
|
WebTransportBidirectionalStream,
|
|
WebTransportDatagramDuplexStream,
|
|
WebTransportError,
|
|
WebTransportReceiveStream,
|
|
WebTransportSendGroup,
|
|
WebTransportSendStream,
|
|
};
|