diff --git a/doc/api/errors.md b/doc/api/errors.md
index dde490c29b477cc..75257f6a054efcf 100644
--- a/doc/api/errors.md
+++ b/doc/api/errors.md
@@ -3571,6 +3571,42 @@ removed: v10.0.0
The `node:repl` module was unable to parse data from the REPL history file.
+
+
+### `ERR_QUIC_CONNECTION_FAILED`
+
+
+
+> Stability: 1 - Experimental
+
+Establishing a QUIC connection failed.
+
+
+
+### `ERR_QUIC_ENDPOINT_CLOSED`
+
+
+
+> Stability: 1 - Experimental
+
+A QUIC Endpoint closed with an error.
+
+
+
+### `ERR_QUIC_OPEN_STREAM_FAILED`
+
+
+
+> Stability: 1 - Experimental
+
+Opening a QUIC stream failed.
+
### `ERR_SOCKET_CANNOT_SEND`
diff --git a/lib/internal/errors.js b/lib/internal/errors.js
index 8ab4a63a0f7110e..75643133b9e7114 100644
--- a/lib/internal/errors.js
+++ b/lib/internal/errors.js
@@ -1645,6 +1645,9 @@ E('ERR_PARSE_ARGS_UNKNOWN_OPTION', (option, allowPositionals) => {
E('ERR_PERFORMANCE_INVALID_TIMESTAMP',
'%d is not a valid timestamp', TypeError);
E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError);
+E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error);
+E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error);
+E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error);
E('ERR_REQUIRE_CYCLE_MODULE', '%s', Error);
E('ERR_REQUIRE_ESM',
function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) {
diff --git a/lib/internal/quic/datagrams.js b/lib/internal/quic/datagrams.js
new file mode 100644
index 000000000000000..d3f39d9a72b33e7
--- /dev/null
+++ b/lib/internal/quic/datagrams.js
@@ -0,0 +1,142 @@
+'use strict';
+
+const {
+ Uint8Array,
+} = primordials;
+
+const {
+ ReadableStream,
+} = require('internal/webstreams/readablestream');
+
+const {
+ WritableStream,
+} = require('internal/webstreams/writablestream');
+
+const {
+ isArrayBufferView,
+} = require('util/types');
+
+const {
+ codes: {
+ ERR_INVALID_ARG_TYPE,
+ },
+} = require('internal/errors');
+
+// QUIC datagrams are unordered, unreliable packets of data exchanged over
+// a session. They are unrelated to any specific QUIC stream. Our implementation
+// uses a ReadableStream to receive datagrams and a WritableStream to send them.
+// Any ArrayBufferView can be used when sending a datagram. Received datagrams
+// will always be Uint8Array.
+// The DatagramReadableStream and DatagramWritableStream instances are created
+// and held internally by the QUIC Session object. Only the readable and writable
+// properties will be exposed.
+
+class DatagramReadableStream {
+ #readable = undefined;
+
+ /** @type {ReadableStreamDefaultController} */
+ #controller = undefined;
+
+ constructor() {
+ let controller;
+ this.#readable = new ReadableStream({
+ start(c) { controller = c; },
+ });
+ this.#controller = controller;
+ }
+
+ get readable() { return this.#readable; }
+
+ // Close the ReadableStream. The underlying source will be closed. Any
+ // datagrams already in the queue will be preserved and will be read.
+ close() {
+ this.#controller.close();
+ }
+
+ // Errors the readable stream
+ error(reason) {
+ this.#controller.error(reason);
+ }
+
+ // Enqueue a datagram to be read by the stream. This will always be
+ // a Uint8Array.
+ enqueue(datagram) {
+ this.#controller.enqueue(datagram);
+ }
+}
+
+class DatagramWritableStream {
+ #writable = undefined;
+ /** @type {WritableStreamDefaultController} */
+ #controller = undefined;
+
+ /**
+ * @callback DatagramWrittenCallback
+ * @param {Uint8Array} chunk
+ * @returns {Promise}
+ */
+
+ /**
+ * @callback DatagramClosedCallback
+ * @returns {Promise}
+ */
+
+ /**
+ * @callback DatagramAbortedCallback
+ * @param {any} reason
+ * @returns {Promise}
+ */
+
+ /**
+ * @param {DatagramWrittenCallback} written
+ * @param {DatagramClosedCallback} closed
+ * @param {DatagramAbortedCallback} aborted
+ */
+ constructor(written, closed, aborted) {
+ let controller;
+ this.#writable = new WritableStream({
+ start(c) { controller = c; },
+ async close(controller) {
+ try {
+ await closed(undefined);
+ } catch (err) {
+ controller.error(err);
+ }
+ },
+ async abort(reason) {
+ try {
+ await aborted(reason);
+ } catch {
+ // There's nothing to do in this case
+ }
+ },
+ async write(chunk, controller) {
+ if (!isArrayBufferView(chunk)) {
+ throw new ERR_INVALID_ARG_TYPE('chunk', ['ArrayBufferView'], chunk);
+ }
+ const {
+ byteOffset,
+ byteLength,
+ } = chunk;
+ chunk = new Uint8Array(chunk.buffer.transfer(), byteOffset, byteLength);
+ try {
+ await written(chunk);
+ } catch (err) {
+ controller.error(err);
+ }
+ },
+ });
+ this.#controller = controller;
+ }
+
+ get writable() { return this.#writable; }
+
+ error(reason) {
+ this.#controller.error(reason);
+ }
+}
+
+module.exports = {
+ DatagramReadableStream,
+ DatagramWritableStream,
+};
diff --git a/lib/internal/quic/quic.js b/lib/internal/quic/quic.js
new file mode 100644
index 000000000000000..708f33a11f5603c
--- /dev/null
+++ b/lib/internal/quic/quic.js
@@ -0,0 +1,2103 @@
+'use strict';
+
+const {
+ ArrayIsArray,
+ BigUint64Array,
+ DataView,
+ ObjectDefineProperties,
+ PromiseWithResolvers,
+ ReflectConstruct,
+ Symbol,
+ SymbolAsyncDispose,
+ SymbolDispose,
+} = primordials;
+
+const {
+ Endpoint: Endpoint_,
+ setCallbacks,
+
+ // The constants to be exposed to end users for various options.
+ CC_ALGO_RENO,
+ CC_ALGO_CUBIC,
+ CC_ALGO_BBR,
+ CC_ALGO_RENO_STR,
+ CC_ALGO_CUBIC_STR,
+ CC_ALGO_BBR_STR,
+ PREFERRED_ADDRESS_IGNORE,
+ PREFERRED_ADDRESS_USE,
+ DEFAULT_PREFERRED_ADDRESS_POLICY,
+ DEFAULT_CIPHERS,
+ DEFAULT_GROUPS,
+ STREAM_DIRECTION_BIDIRECTIONAL,
+ STREAM_DIRECTION_UNIDIRECTIONAL,
+
+ // Internal constants for use by the implementation.
+ // These are not exposed to end users.
+ CLOSECONTEXT_CLOSE: kCloseContextClose,
+ CLOSECONTEXT_BIND_FAILURE: kCloseContextBindFailure,
+ CLOSECONTEXT_LISTEN_FAILURE: kCloseContextListenFailure,
+ CLOSECONTEXT_RECEIVE_FAILURE: kCloseContextReceiveFailure,
+ CLOSECONTEXT_SEND_FAILURE: kCloseContextSendFailure,
+ CLOSECONTEXT_START_FAILURE: kCloseContextStartFailure,
+
+ // All of the IDX_STATS_* constants are the index positions of the stats
+ // fields in the relevant BigUint64Array's that underly the *Stats objects.
+ // These are not exposed to end users.
+ IDX_STATS_ENDPOINT_CREATED_AT,
+ IDX_STATS_ENDPOINT_DESTROYED_AT,
+ IDX_STATS_ENDPOINT_BYTES_RECEIVED,
+ IDX_STATS_ENDPOINT_BYTES_SENT,
+ IDX_STATS_ENDPOINT_PACKETS_RECEIVED,
+ IDX_STATS_ENDPOINT_PACKETS_SENT,
+ IDX_STATS_ENDPOINT_SERVER_SESSIONS,
+ IDX_STATS_ENDPOINT_CLIENT_SESSIONS,
+ IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT,
+ IDX_STATS_ENDPOINT_RETRY_COUNT,
+ IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT,
+ IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT,
+ IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT,
+
+ IDX_STATS_SESSION_CREATED_AT,
+ IDX_STATS_SESSION_CLOSING_AT,
+ IDX_STATS_SESSION_DESTROYED_AT,
+ IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT,
+ IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT,
+ IDX_STATS_SESSION_GRACEFUL_CLOSING_AT,
+ IDX_STATS_SESSION_BYTES_RECEIVED,
+ IDX_STATS_SESSION_BYTES_SENT,
+ IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT,
+ IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT,
+ IDX_STATS_SESSION_UNI_IN_STREAM_COUNT,
+ IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT,
+ IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT,
+ IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT,
+ IDX_STATS_SESSION_BYTES_IN_FLIGHT,
+ IDX_STATS_SESSION_BLOCK_COUNT,
+ IDX_STATS_SESSION_CWND,
+ IDX_STATS_SESSION_LATEST_RTT,
+ IDX_STATS_SESSION_MIN_RTT,
+ IDX_STATS_SESSION_RTTVAR,
+ IDX_STATS_SESSION_SMOOTHED_RTT,
+ IDX_STATS_SESSION_SSTHRESH,
+ IDX_STATS_SESSION_DATAGRAMS_RECEIVED,
+ IDX_STATS_SESSION_DATAGRAMS_SENT,
+ IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED,
+ IDX_STATS_SESSION_DATAGRAMS_LOST,
+
+ IDX_STATS_STREAM_CREATED_AT,
+ IDX_STATS_STREAM_RECEIVED_AT,
+ IDX_STATS_STREAM_ACKED_AT,
+ IDX_STATS_STREAM_CLOSING_AT,
+ IDX_STATS_STREAM_DESTROYED_AT,
+ IDX_STATS_STREAM_BYTES_RECEIVED,
+ IDX_STATS_STREAM_BYTES_SENT,
+ IDX_STATS_STREAM_MAX_OFFSET,
+ IDX_STATS_STREAM_MAX_OFFSET_ACK,
+ IDX_STATS_STREAM_MAX_OFFSET_RECV,
+ IDX_STATS_STREAM_FINAL_SIZE,
+
+ IDX_STATE_SESSION_PATH_VALIDATION,
+ IDX_STATE_SESSION_VERSION_NEGOTIATION,
+ IDX_STATE_SESSION_DATAGRAM,
+ IDX_STATE_SESSION_SESSION_TICKET,
+ IDX_STATE_SESSION_CLOSING,
+ IDX_STATE_SESSION_GRACEFUL_CLOSE,
+ IDX_STATE_SESSION_SILENT_CLOSE,
+ IDX_STATE_SESSION_STATELESS_RESET,
+ IDX_STATE_SESSION_DESTROYED,
+ IDX_STATE_SESSION_HANDSHAKE_COMPLETED,
+ IDX_STATE_SESSION_HANDSHAKE_CONFIRMED,
+ IDX_STATE_SESSION_STREAM_OPEN_ALLOWED,
+ IDX_STATE_SESSION_PRIORITY_SUPPORTED,
+ IDX_STATE_SESSION_WRAPPED,
+ IDX_STATE_SESSION_LAST_DATAGRAM_ID,
+
+ IDX_STATE_ENDPOINT_BOUND,
+ IDX_STATE_ENDPOINT_RECEIVING,
+ IDX_STATE_ENDPOINT_LISTENING,
+ IDX_STATE_ENDPOINT_CLOSING,
+ IDX_STATE_ENDPOINT_BUSY,
+ IDX_STATE_ENDPOINT_PENDING_CALLBACKS,
+
+ IDX_STATE_STREAM_ID,
+ IDX_STATE_STREAM_FIN_SENT,
+ IDX_STATE_STREAM_FIN_RECEIVED,
+ IDX_STATE_STREAM_READ_ENDED,
+ IDX_STATE_STREAM_WRITE_ENDED,
+ IDX_STATE_STREAM_DESTROYED,
+ IDX_STATE_STREAM_PAUSED,
+ IDX_STATE_STREAM_RESET,
+ IDX_STATE_STREAM_HAS_READER,
+ IDX_STATE_STREAM_WANTS_BLOCK,
+ IDX_STATE_STREAM_WANTS_HEADERS,
+ IDX_STATE_STREAM_WANTS_RESET,
+ IDX_STATE_STREAM_WANTS_TRAILERS,
+} = internalBinding('quic');
+
+const {
+ codes: {
+ ERR_ILLEGAL_CONSTRUCTOR,
+ ERR_INVALID_ARG_TYPE,
+ ERR_INVALID_ARG_VALUE,
+ ERR_INVALID_STATE,
+ ERR_QUIC_CONNECTION_FAILED,
+ ERR_QUIC_ENDPOINT_CLOSED,
+ ERR_QUIC_OPEN_STREAM_FAILED,
+ },
+} = require('internal/errors');
+
+const {
+ InternalSocketAddress,
+ SocketAddress,
+ kHandle: kSocketAddressHandle,
+} = require('internal/socketaddress');
+
+const {
+ isKeyObject,
+ isCryptoKey,
+} = require('internal/crypto/keys');
+
+const {
+ EventTarget,
+ Event,
+ kNewListener,
+ kRemoveListener,
+} = require('internal/event_target');
+
+const {
+ ReadableStream,
+} = require('internal/webstreams/readablestream');
+
+const {
+ WritableStream,
+} = require('internal/webstreams/writablestream');
+
+const {
+ customInspectSymbol: kInspect,
+} = require('internal/util');
+const { inspect } = require('internal/util/inspect');
+
+const {
+ kHandle: kKeyObjectHandle,
+ kKeyObject: kKeyObjectInner,
+} = require('internal/crypto/util');
+
+const {
+ validateObject,
+} = require('internal/validators');
+
+const {
+ DatagramReadableStream,
+ DatagramWritableStream,
+} = require('internal/quic/datagrams');
+
+const kOwner = Symbol('kOwner');
+const kHandle = Symbol('kHandle');
+const kFinishClose = Symbol('kFinishClose');
+const kStats = Symbol('kStats');
+const kState = Symbol('kState');
+const kReadable = Symbol('kReadable');
+const kWritable = Symbol('kWritable');
+const kEndpoint = Symbol('kEndpoint');
+const kSession = Symbol('kSession');
+const kStreams = Symbol('kStreams');
+const kNewSession = Symbol('kNewSession');
+const kNewStream = Symbol('kNewStream');
+const kRemoteAddress = Symbol('kRemoteAddress');
+const kDatagramsReadable = Symbol('kDatagramsReadable');
+const kDatagramsWritable = Symbol('kDatagramsWritable');
+const kIsPendingClose = Symbol('kIsPendingClose');
+const kPendingClose = Symbol('kPendingClose');
+const kHandshake = Symbol('kHandshake');
+const kPathValidation = Symbol('kPathValidation');
+const kTicket = Symbol('kTicket');
+const kVersionNegotiation = Symbol('kVersionNegotiation');
+
+/**
+ * @typedef {import('../socketaddress.js').SocketAddress} SocketAddress
+ * @typedef {import('../crypto/keys.js').KeyObject} KeyObject
+ * @typedef {import('../crypto/keys.js').CryptoKey} CryptoKey
+ */
+
+/**
+ * @typedef {object} EndpointOptions
+ * @property {SocketAddress} [address] The local address to bind to.
+ * @property {bigint|number} [retryTokenExpiration] The retry token expiration.
+ * @property {bigint|number} [tokenExpiration] The token expiration.
+ * @property {bigint|number} [maxConnectionsPerHost] The maximum number of connections per host
+ * @property {bigint|number} [maxConnectionsTotal] The maximum number of total connections
+ * @property {bigint|number} [maxStatelessResetsPerHost] The maximum number of stateless resets per host
+ * @property {bigint|number} [addressLRUSize] The size of the address LRU cache
+ * @property {bigint|number} [maxRetries] The maximum number of retries
+ * @property {bigint|number} [maxPayloadSize] The maximum payload size
+ * @property {bigint|number} [unacknowledgedPacketThreshold] The unacknowledged packet threshold
+ * @property {bigint|number} [handshakeTimeout] The handshake timeout
+ * @property {bigint|number} [maxStreamWindow] The maximum stream window
+ * @property {bigint|number} [maxWindow] The maximum window
+ * @property {number} [rxDiagnosticLoss] The receive diagnostic loss
+ * @property {number} [txDiagnosticLoss] The transmit diagnostic loss
+ * @property {number} [udpReceiveBufferSize] The UDP receive buffer size
+ * @property {number} [udpSendBufferSize] The UDP send buffer size
+ * @property {number} [udpTTL] The UDP TTL
+ * @property {boolean} [noUdpPayloadSizeShaping] Disable UDP payload size shaping
+ * @property {boolean} [validateAddress] Validate the address
+ * @property {boolean} [disableActiveMigration] Disable active migration
+ * @property {boolean} [ipv6Only] Use IPv6 only
+ * @property {'reno'|'cubic'|'bbr'|number} [cc] The congestion control algorithm
+ * @property {ArrayBufferView} [resetTokenSecret] The reset token secret
+ * @property {ArrayBufferView} [tokenSecret] The token secret
+ */
+
+/**
+ * @typedef {object} TlsOptions
+ * @property {string} [sni] The server name indication
+ * @property {string} [alpn] The application layer protocol negotiation
+ * @property {string} [ciphers] The ciphers
+ * @property {string} [groups] The groups
+ * @property {boolean} [keylog] Enable key logging
+ * @property {boolean} [verifyClient] Verify the client
+ * @property {boolean} [tlsTrace] Enable TLS tracing
+ * @property {boolean} [verifyPrivateKey] Verify the private key
+ * @property {KeyObject|CryptoKey|Array} [keys] The keys
+ * @property {ArrayBuffer|ArrayBufferView|Array} [certs] The certificates
+ * @property {ArrayBuffer|ArrayBufferView|Array} [ca] The certificate authority
+ * @property {ArrayBuffer|ArrayBufferView|Array} [crl] The certificate revocation list
+ */
+
+/**
+ * @typedef {object} TransportParams
+ * @property {SocketAddress} [preferredAddressIpv4] The preferred IPv4 address
+ * @property {SocketAddress} [preferredAddressIpv6] The preferred IPv6 address
+ * @property {bigint|number} [initialMaxStreamDataBidiLocal] The initial maximum stream data bidirectional local
+ * @property {bigint|number} [initialMaxStreamDataBidiRemote] The initial maximum stream data bidirectional remote
+ * @property {bigint|number} [initialMaxStreamDataUni] The initial maximum stream data unidirectional
+ * @property {bigint|number} [initialMaxData] The initial maximum data
+ * @property {bigint|number} [initialMaxStreamsBidi] The initial maximum streams bidirectional
+ * @property {bigint|number} [initialMaxStreamsUni] The initial maximum streams unidirectional
+ * @property {bigint|number} [maxIdleTimeout] The maximum idle timeout
+ * @property {bigint|number} [activeConnectionIDLimit] The active connection ID limit
+ * @property {bigint|number} [ackDelayExponent] The acknowledgment delay exponent
+ * @property {bigint|number} [maxAckDelay] The maximum acknowledgment delay
+ * @property {bigint|number} [maxDatagramFrameSize] The maximum datagram frame size
+ * @property {boolean} [disableActiveMigration] Disable active migration
+ */
+
+/**
+ * @typedef {object} ApplicationOptions
+ * @property {bigint|number} [maxHeaderPairs] The maximum header pairs
+ * @property {bigint|number} [maxHeaderLength] The maximum header length
+ * @property {bigint|number} [maxFieldSectionSize] The maximum field section size
+ * @property {bigint|number} [qpackMaxDTableCapacity] The qpack maximum dynamic table capacity
+ * @property {bigint|number} [qpackEncoderMaxDTableCapacity] The qpack encoder maximum dynamic table capacity
+ * @property {bigint|number} [qpackBlockedStreams] The qpack blocked streams
+ * @property {boolean} [enableConnectPrototcol] Enable the connect protocol
+ * @property {boolean} [enableDatagrams] Enable datagrams
+ */
+
+/**
+ * @typedef {object} SessionOptions
+ * @property {number} [version] The version
+ * @property {number} [minVersion] The minimum version
+ * @property {'use'|'ignore'|'default'} [preferredAddressPolicy] The preferred address policy
+ * @property {ApplicationOptions} [application] The application options
+ * @property {TransportParams} [transportParams] The transport parameters
+ * @property {TlsOptions} [tls] The TLS options
+ * @property {boolean} [qlog] Enable qlog
+ * @property {ArrayBufferView} [sessionTicket] The session ticket
+ */
+
+/**
+ * @typedef {object} Datagrams
+ * @property {ReadableStream} readable The readable stream
+ * @property {WritableStream} writable The writable stream
+ */
+
+setCallbacks({
+ onEndpointClose(context, status) {
+ this[kOwner][kFinishClose](context, status);
+ },
+ onSessionNew(session) {
+ this[kOwner][kNewSession](session);
+ },
+ onSessionClose(errorType, code, reason) {
+ this[kOwner][kFinishClose](errorType, code, reason);
+ },
+ onSessionDatagram(uint8Array, early) {
+ const session = this[kOwner];
+ // Make sure the datagram streams are created.
+ session.datagrams; // eslint-disable-line no-unused-expressions
+ // Enqueue the datagram that was received.
+ session[kDatagramsReadable].enqueue(uint8Array);
+ },
+ onSessionDatagramStatus(id, status) {
+ // TODO(@jasnell): Called when a datagram has been acknowledged
+ // or lost. Currently there's nothing to do here. We need to
+ // decide if/how we want to expose this information.
+ },
+ onSessionHandshake(sni, alpn, cipher, cipherVersion,
+ validationErrorReason,
+ validationErrorCode,
+ earlyDataAccepted) {
+ this[kOwner][kHandshake](sni, alpn, cipher, cipherVersion,
+ validationErrorReason,
+ validationErrorCode,
+ earlyDataAccepted);
+ },
+ onSessionPathValidation(result,
+ newLocalAddress,
+ newRemoteAddress,
+ oldLocalAddress,
+ oldRemoteAddress,
+ preferredAddress) {
+ this[kOwner][kPathValidation](result, newLocalAddress,
+ newRemoteAddress, oldLocalAddress,
+ oldRemoteAddress, preferredAddress);
+ },
+ onSessionTicket(ticket) {
+ this[kOwner][kTicket](ticket);
+ },
+ onSessionVersionNegotiation(version,
+ requestedVersions,
+ supportedVersions) {
+ this[kOwner][kVersionNegotiation](version, requestedVersions,
+ supportedVersions);
+ },
+ onStreamCreated(stream) {},
+ onStreamBlocked() {},
+ onStreamClose(error) {},
+ onStreamReset(error) {},
+ onStreamHeaders(headers, kind) {},
+ onStreamTrailers() {},
+});
+
+/**
+ * Emitted when a new server-side session has been initiated.
+ */
+class SessionEvent extends Event {
+ #session = undefined;
+ #endpoint = undefined;
+
+ /**
+ * @param {Session} session
+ * @param {Endpoint} endpoint
+ */
+ constructor(session, endpoint) {
+ super('session');
+ this.#session = session;
+ this.#endpoint = endpoint;
+ }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ /** @type {Endpoint} */
+ get endpoint() { return this.#endpoint; }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `SessionEvent ${inspect({
+ session: this.session,
+ endpoint: this.endpoint,
+ }, opts)}`;
+ }
+}
+
+class StreamEvent extends Event {
+ #stream = undefined;
+ #session = undefined;
+
+ /**
+ * @param {Stream} stream
+ * @param {Session} session
+ */
+ constructor(stream, session) {
+ super('stream');
+ this.#stream = stream;
+ this.#session = session;
+ }
+
+ /** @type {Stream} */
+ get stream() { return this.#stream; }
+
+ /** @type {Session} */
+ get session() { return this.#session; }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `StreamEvent ${inspect({
+ stream: this.stream,
+ }, opts)}`;
+ }
+}
+
+/**
+ * @param {string} policy
+ * @returns {number}
+ */
+function getPreferredAddressPolicy(policy) {
+ if (policy === 'use') return PREFERRED_ADDRESS_USE;
+ if (policy === 'ignore') return PREFERRED_ADDRESS_IGNORE;
+ if (policy === 'default' || policy === undefined)
+ return DEFAULT_PREFERRED_ADDRESS_POLICY;
+ throw new ERR_INVALID_ARG_VALUE('options.preferredAddressPolicy', policy);
+}
+
+/**
+ * @param {TlsOptions} tls
+ */
+function processTlsOptions(tls) {
+ validateObject(tls, 'options.tls');
+ const {
+ sni,
+ alpn,
+ ciphers,
+ groups,
+ keylog,
+ verifyClient,
+ tlsTrace,
+ verifyPrivateKey,
+ keys,
+ certs,
+ ca,
+ crl,
+ } = tls;
+
+ const keyHandles = [];
+ if (keys !== undefined) {
+ const keyInputs = ArrayIsArray(keys) ? keys : [keys];
+ for (const key of keyInputs) {
+ if (isKeyObject(key)) {
+ if (key.type !== 'private') {
+ throw new ERR_INVALID_ARG_VALUE('options.tls.keys', key, 'must be a private key');
+ }
+ keyHandles.push(key[kKeyObjectHandle]);
+ continue;
+ } else if (isCryptoKey(key)) {
+ if (key.type !== 'private') {
+ throw new ERR_INVALID_ARG_VALUE('options.tls.keys', key, 'must be a private key');
+ }
+ keyHandles.push(key[kKeyObjectInner][kKeyObjectHandle]);
+ continue;
+ } else {
+ throw new ERR_INVALID_ARG_TYPE('options.tls.keys', ['KeyObject', 'CryptoKey'], key);
+ }
+ }
+ }
+
+ return {
+ sni,
+ alpn,
+ ciphers,
+ groups,
+ keylog,
+ verifyClient,
+ tlsTrace,
+ verifyPrivateKey,
+ keys: keyHandles,
+ certs,
+ ca,
+ crl,
+ };
+}
+
+class StreamStats {
+ constructor() {
+ throw new ERR_INVALID_STATE('StreamStats');
+ }
+
+ get createdAt() {
+ return this[kHandle][IDX_STATS_STREAM_CREATED_AT];
+ }
+
+ get receivedAt() {
+ return this[kHandle][IDX_STATS_STREAM_RECEIVED_AT];
+ }
+
+ get ackedAt() {
+ return this[kHandle][IDX_STATS_STREAM_ACKED_AT];
+ }
+
+ get closingAt() {
+ return this[kHandle][IDX_STATS_STREAM_CLOSING_AT];
+ }
+
+ get destroyedAt() {
+ return this[kHandle][IDX_STATS_STREAM_DESTROYED_AT];
+ }
+
+ get bytesReceived() {
+ return this[kHandle][IDX_STATS_STREAM_BYTES_RECEIVED];
+ }
+
+ get bytesSent() {
+ return this[kHandle][IDX_STATS_STREAM_BYTES_SENT];
+ }
+
+ get maxOffset() {
+ return this[kHandle][IDX_STATS_STREAM_MAX_OFFSET];
+ }
+
+ get maxOffsetAcknowledged() {
+ return this[kHandle][IDX_STATS_STREAM_MAX_OFFSET_ACK];
+ }
+
+ get maxOffsetReceived() {
+ return this[kHandle][IDX_STATS_STREAM_MAX_OFFSET_RECV];
+ }
+
+ get finalSize() {
+ return this[kHandle][IDX_STATS_STREAM_FINAL_SIZE];
+ }
+
+ toJSON() {
+ return {
+ createdAt: `${this.createdAt}`,
+ receivedAt: `${this.receivedAt}`,
+ ackedAt: `${this.ackedAt}`,
+ closingAt: `${this.closingAt}`,
+ destroyedAt: `${this.destroyedAt}`,
+ bytesReceived: `${this.bytesReceived}`,
+ bytesSent: `${this.bytesSent}`,
+ maxOffset: `${this.maxOffset}`,
+ maxOffsetAcknowledged: `${this.maxOffsetAcknowledged}`,
+ maxOffsetReceived: `${this.maxOffsetReceived}`,
+ finalSize: `${this.finalSize}`,
+ };
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `StreamStats ${inspect({
+ createdAt: this.createdAt,
+ receivedAt: this.receivedAt,
+ ackedAt: this.ackedAt,
+ closingAt: this.closingAt,
+ destroyedAt: this.destroyedAt,
+ bytesReceived: this.bytesReceived,
+ bytesSent: this.bytesSent,
+ maxOffset: this.maxOffset,
+ maxOffsetAcknowledged: this.maxOffsetAcknowledged,
+ maxOffsetReceived: this.maxOffsetReceived,
+ finalSize: this.finalSize,
+ }, opts)}`;
+ }
+
+ [kFinishClose]() {
+ // Snapshot the stats into a new BigUint64Array since the underlying
+ // buffer will be destroyed.
+ this[kHandle] = new BigUint64Array(this[kHandle]);
+ }
+}
+
+/**
+ * @param {ArrayBuffer} buffer
+ * @returns {StreamStats}
+ */
+function createStreamStats(buffer) {
+ return ReflectConstruct(function() {
+ this[kHandle] = new BigUint64Array(buffer);
+ }, [], StreamStats);
+}
+
+class StreamState {
+ constructor() {
+ throw new ERR_INVALID_STATE('StreamState');
+ }
+
+ /** @type {bigint} */
+ get id() {
+ return this[kHandle].getBigInt64(IDX_STATE_STREAM_ID);
+ }
+
+ /** @type {boolean} */
+ get finSent() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_FIN_SENT);
+ }
+
+ /** @type {boolean} */
+ get finReceived() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_FIN_RECEIVED);
+ }
+
+ /** @type {boolean} */
+ get readEnded() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_READ_ENDED);
+ }
+
+ /** @type {boolean} */
+ get writeEnded() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_WRITE_ENDED);
+ }
+
+ /** @type {boolean} */
+ get destroyed() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_DESTROYED);
+ }
+
+ /** @type {boolean} */
+ get paused() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_PAUSED);
+ }
+
+ /** @type {boolean} */
+ get reset() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_RESET);
+ }
+
+ /** @type {boolean} */
+ get hasReader() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_HAS_READER);
+ }
+
+ /** @type {boolean} */
+ get wantsBlock() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_WANTS_BLOCK);
+ }
+
+ /** @type {boolean} */
+ set wantsBlock(val) {
+ this[kHandle].setUint8(IDX_STATE_STREAM_WANTS_BLOCK, val ? 1 : 0);
+ }
+
+ /** @type {boolean} */
+ get wantsHeaders() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_WANTS_HEADERS);
+ }
+
+ /** @type {boolean} */
+ set wantsHeaders(val) {
+ this[kHandle].setUint8(IDX_STATE_STREAM_WANTS_HEADERS, val ? 1 : 0);
+ }
+
+ /** @type {boolean} */
+ get wantsReset() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_WANTS_RESET);
+ }
+
+ /** @type {boolean} */
+ set wantsReset(val) {
+ this[kHandle].setUint8(IDX_STATE_STREAM_WANTS_RESET, val ? 1 : 0);
+ }
+
+ /** @type {boolean} */
+ get wantsTrailers() {
+ return !!this[kHandle].getUint8(IDX_STATE_STREAM_WANTS_TRAILERS);
+ }
+
+ /** @type {boolean} */
+ set wantsTrailers(val) {
+ this[kHandle].setUint8(IDX_STATE_STREAM_WANTS_TRAILERS, val ? 1 : 0);
+ }
+
+ toJSON() {
+ return {
+ id: `${this.id}`,
+ finSent: this.finSent,
+ finReceived: this.finReceived,
+ readEnded: this.readEnded,
+ writeEnded: this.writeEnded,
+ destroyed: this.destroyed,
+ paused: this.paused,
+ reset: this.reset,
+ hasReader: this.hasReader,
+ wantsBlock: this.wantsBlock,
+ wantsHeaders: this.wantsHeaders,
+ wantsReset: this.wantsReset,
+ wantsTrailers: this.wantsTrailers,
+ };
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `StreamState ${inspect({
+ id: this.id,
+ finSent: this.finSent,
+ finReceived: this.finReceived,
+ readEnded: this.readEnded,
+ writeEnded: this.writeEnded,
+ destroyed: this.destroyed,
+ paused: this.paused,
+ reset: this.reset,
+ hasReader: this.hasReader,
+ wantsBlock: this.wantsBlock,
+ wantsHeaders: this.wantsHeaders,
+ wantsReset: this.wantsReset,
+ wantsTrailers: this.wantsTrailers,
+ }, opts)}`;
+ }
+}
+
+/**
+ * @param {ArrayBuffer} buffer
+ * @returns {StreamState}
+ */
+function createStreamState(buffer) {
+ return ReflectConstruct(function() {
+ this[kHandle] = new DataView(buffer);
+ }, [], StreamState);
+}
+
+class Stream extends EventTarget {
+ constructor() {
+ throw new ERR_ILLEGAL_CONSTRUCTOR('Stream');
+ }
+
+ /** @type {StreamStats} */
+ get stats() { return this[kStats]; }
+
+ /** @type {StreamState} */
+ get state() { return this[kState]; }
+
+ /** @type {Session} */
+ get session() { return this[kSession]; }
+
+ /** @type {bigint} */
+ get id() { return this[kState].id; }
+}
+
+class BidirectionalStream extends Stream {
+ constructor() {
+ throw new ERR_ILLEGAL_CONSTRUCTOR('BidirectionalStream');
+ }
+
+ /** @type {ReadableStream} */
+ get readable() { return this[kReadable]; }
+
+ /** @type {WritableStream} */
+ get writable() { return this[kWritable]; }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `BidirectionalStream ${inspect({
+ stats: this.stats,
+ state: this.state,
+ session: this.session,
+ readable: this.readable,
+ writable: this.writable,
+ }, opts)}`;
+ }
+}
+
+class UnidirectionalInboundStream extends Stream {
+ constructor() {
+ throw new ERR_ILLEGAL_CONSTRUCTOR('UnidirectionalInboundStream');
+ }
+
+ /** @type {ReadableStream} */
+ get readable() { return this[kReadable]; }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `UnidirectionalInboundStream ${inspect({
+ stats: this.stats,
+ state: this.state,
+ session: this.session,
+ readable: this.readable,
+ }, opts)}`;
+ }
+}
+
+class UnidirectionalOutboundStream extends Stream {
+ constructor() {
+ throw new ERR_ILLEGAL_CONSTRUCTOR('UnidirectionalOutboundStream');
+ }
+
+ /** @type {WritableStream} */
+ get writable() { return this[kWritable]; }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `UnidirectionalOutboundStream ${inspect({
+ stats: this.stats,
+ state: this.state,
+ session: this.session,
+ writable: this.writable,
+ }, opts)}`;
+ }
+}
+
+/**
+ * @param {object} handle
+ * @returns {BidirectionalStream}
+ */
+function createBidirectionalStream(handle, session) {
+ const instance = ReflectConstruct(EventTarget, [], BidirectionalStream);
+ instance[kHandle] = handle;
+ instance[kStats] = createStreamStats(handle.stats);
+ instance[kState] = createStreamState(handle.state);
+ instance[kSession] = session;
+ instance[kReadable] = new ReadableStream();
+ instance[kWritable] = new WritableStream();
+ handle[kOwner] = instance;
+ return instance;
+}
+
+/**
+ * @param {object} handle
+ * @returns {UnidirectionalInboundStream}
+ */
+function createUnidirectionalInboundStream(handle, session) { // eslint-disable-line no-unused-vars
+ const instance = ReflectConstruct(EventTarget, [], UnidirectionalInboundStream);
+ instance[kHandle] = handle;
+ instance[kStats] = createStreamStats(handle.stats);
+ instance[kState] = createStreamState(handle.state);
+ instance[kSession] = session;
+ instance[kReadable] = new ReadableStream();
+ handle[kOwner] = instance;
+ return instance;
+}
+
+/**
+ * @param {object} handle
+ * @returns {UnidirectionalOutboundStream}
+ */
+function createUnidirectionalOutboundStream(handle, session) {
+ const instance = ReflectConstruct(EventTarget, [], UnidirectionalOutboundStream);
+ instance[kHandle] = handle;
+ instance[kStats] = createStreamStats(handle.stats);
+ instance[kState] = createStreamState(handle.state);
+ instance[kSession] = session;
+ instance[kWritable] = new WritableStream();
+ handle[kOwner] = instance;
+ return instance;
+}
+
+class SessionStats {
+ constructor() {
+ throw new ERR_INVALID_STATE('SessionStats');
+ }
+
+ /** @type {bigint} */
+ get createdAt() {
+ return this[kHandle][IDX_STATS_SESSION_CREATED_AT];
+ }
+
+ /** @type {bigint} */
+ get closingAt() {
+ return this[kHandle][IDX_STATS_SESSION_CLOSING_AT];
+ }
+
+ /** @type {bigint} */
+ get destroyedAt() {
+ return this[kHandle][IDX_STATS_SESSION_DESTROYED_AT];
+ }
+
+ /** @type {bigint} */
+ get handshakeCompletedAt() {
+ return this[kHandle][IDX_STATS_SESSION_HANDSHAKE_COMPLETED_AT];
+ }
+
+ /** @type {bigint} */
+ get handshakeConfirmedAt() {
+ return this[kHandle][IDX_STATS_SESSION_HANDSHAKE_CONFIRMED_AT];
+ }
+
+ /** @type {bigint} */
+ get gracefulClosingAt() {
+ return this[kHandle][IDX_STATS_SESSION_GRACEFUL_CLOSING_AT];
+ }
+
+ /** @type {bigint} */
+ get bytesReceived() {
+ return this[kHandle][IDX_STATS_SESSION_BYTES_RECEIVED];
+ }
+
+ /** @type {bigint} */
+ get bytesSent() {
+ return this[kHandle][IDX_STATS_SESSION_BYTES_SENT];
+ }
+
+ /** @type {bigint} */
+ get bidiInStreamCount() {
+ return this[kHandle][IDX_STATS_SESSION_BIDI_IN_STREAM_COUNT];
+ }
+
+ /** @type {bigint} */
+ get bidiOutStreamCount() {
+ return this[kHandle][IDX_STATS_SESSION_BIDI_OUT_STREAM_COUNT];
+ }
+
+ /** @type {bigint} */
+ get uniInStreamCount() {
+ return this[kHandle][IDX_STATS_SESSION_UNI_IN_STREAM_COUNT];
+ }
+
+ /** @type {bigint} */
+ get uniOutStreamCount() {
+ return this[kHandle][IDX_STATS_SESSION_UNI_OUT_STREAM_COUNT];
+ }
+
+ /** @type {bigint} */
+ get lossRetransmitCount() {
+ return this[kHandle][IDX_STATS_SESSION_LOSS_RETRANSMIT_COUNT];
+ }
+
+ /** @type {bigint} */
+ get maxBytesInFlights() {
+ return this[kHandle][IDX_STATS_SESSION_MAX_BYTES_IN_FLIGHT];
+ }
+
+ /** @type {bigint} */
+ get bytesInFlight() {
+ return this[kHandle][IDX_STATS_SESSION_BYTES_IN_FLIGHT];
+ }
+
+ /** @type {bigint} */
+ get blockCount() {
+ return this[kHandle][IDX_STATS_SESSION_BLOCK_COUNT];
+ }
+
+ /** @type {bigint} */
+ get cwnd() {
+ return this[kHandle][IDX_STATS_SESSION_CWND];
+ }
+
+ /** @type {bigint} */
+ get latestRtt() {
+ return this[kHandle][IDX_STATS_SESSION_LATEST_RTT];
+ }
+
+ /** @type {bigint} */
+ get minRtt() {
+ return this[kHandle][IDX_STATS_SESSION_MIN_RTT];
+ }
+
+ /** @type {bigint} */
+ get rttVar() {
+ return this[kHandle][IDX_STATS_SESSION_RTTVAR];
+ }
+
+ /** @type {bigint} */
+ get smoothedRtt() {
+ return this[kHandle][IDX_STATS_SESSION_SMOOTHED_RTT];
+ }
+
+ /** @type {bigint} */
+ get ssthresh() {
+ return this[kHandle][IDX_STATS_SESSION_SSTHRESH];
+ }
+
+ /** @type {bigint} */
+ get datagramsReceived() {
+ return this[kHandle][IDX_STATS_SESSION_DATAGRAMS_RECEIVED];
+ }
+
+ /** @type {bigint} */
+ get datagramsSent() {
+ return this[kHandle][IDX_STATS_SESSION_DATAGRAMS_SENT];
+ }
+
+ /** @type {bigint} */
+ get datagramsAcknowledged() {
+ return this[kHandle][IDX_STATS_SESSION_DATAGRAMS_ACKNOWLEDGED];
+ }
+
+ /** @type {bigint} */
+ get datagramsLost() {
+ return this[kHandle][IDX_STATS_SESSION_DATAGRAMS_LOST];
+ }
+
+ toJSON() {
+ return {
+ createdAt: `${this.createdAt}`,
+ closingAt: `${this.closingAt}`,
+ destroyedAt: `${this.destroyedAt}`,
+ handshakeCompletedAt: `${this.handshakeCompletedAt}`,
+ handshakeConfirmedAt: `${this.handshakeConfirmedAt}`,
+ gracefulClosingAt: `${this.gracefulClosingAt}`,
+ bytesReceived: `${this.bytesReceived}`,
+ bytesSent: `${this.bytesSent}`,
+ bidiInStreamCount: `${this.bidiInStreamCount}`,
+ bidiOutStreamCount: `${this.bidiOutStreamCount}`,
+ uniInStreamCount: `${this.uniInStreamCount}`,
+ uniOutStreamCount: `${this.uniOutStreamCount}`,
+ lossRetransmitCount: `${this.lossRetransmitCount}`,
+ maxBytesInFlights: `${this.maxBytesInFlights}`,
+ bytesInFlight: `${this.bytesInFlight}`,
+ blockCount: `${this.blockCount}`,
+ cwnd: `${this.cwnd}`,
+ latestRtt: `${this.latestRtt}`,
+ minRtt: `${this.minRtt}`,
+ rttVar: `${this.rttVar}`,
+ smoothedRtt: `${this.smoothedRtt}`,
+ ssthresh: `${this.ssthresh}`,
+ datagramsReceived: `${this.datagramsReceived}`,
+ datagramsSent: `${this.datagramsSent}`,
+ datagramsAcknowledged: `${this.datagramsAcknowledged}`,
+ datagramsLost: `${this.datagramsLost}`,
+ };
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `SessionStats ${inspect({
+ createdAt: this.createdAt,
+ closingAt: this.closingAt,
+ destroyedAt: this.destroyedAt,
+ handshakeCompletedAt: this.handshakeCompletedAt,
+ handshakeConfirmedAt: this.handshakeConfirmedAt,
+ gracefulClosingAt: this.gracefulClosingAt,
+ bytesReceived: this.bytesReceived,
+ bytesSent: this.bytesSent,
+ bidiInStreamCount: this.bidiInStreamCount,
+ bidiOutStreamCount: this.bidiOutStreamCount,
+ uniInStreamCount: this.uniInStreamCount,
+ uniOutStreamCount: this.uniOutStreamCount,
+ lossRetransmitCount: this.lossRetransmitCount,
+ maxBytesInFlights: this.maxBytesInFlights,
+ bytesInFlight: this.bytesInFlight,
+ blockCount: this.blockCount,
+ cwnd: this.cwnd,
+ latestRtt: this.latestRtt,
+ minRtt: this.minRtt,
+ rttVar: this.rttVar,
+ smoothedRtt: this.smoothedRtt,
+ ssthresh: this.ssthresh,
+ datagramsReceived: this.datagramsReceived,
+ datagramsSent: this.datagramsSent,
+ datagramsAcknowledged: this.datagramsAcknowledged,
+ datagramsLost: this.datagramsLost,
+ }, opts)}`;
+ }
+
+ [kFinishClose]() {
+ // Snapshot the stats into a new BigUint64Array since the underlying
+ // buffer will be destroyed.
+ this[kHandle] = new BigUint64Array(this[kHandle]);
+ }
+}
+
+/**
+ *
+ * @param {ArrayBuffer} buffer
+ * @returns {SessionStats}
+ */
+function createSessionStats(buffer) {
+ return ReflectConstruct(function() {
+ this[kHandle] = new BigUint64Array(buffer);
+ }, [], SessionStats);
+}
+
+class SessionState {
+ constructor() {
+ throw new ERR_INVALID_STATE('SessionState');
+ }
+
+ /** @type {boolean} */
+ get hasPathValidationListener() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_PATH_VALIDATION);
+ }
+
+ set hasPathValidationListener(val) {
+ this[kHandle].setUint8(IDX_STATE_SESSION_PATH_VALIDATION, val ? 1 : 0);
+ }
+
+ /** @type {boolean} */
+ get hasVersionNegotiationListener() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_VERSION_NEGOTIATION);
+ }
+
+ set hasVersionNegotiationListener(val) {
+ this[kHandle].setUint8(IDX_STATE_SESSION_VERSION_NEGOTIATION, val ? 1 : 0);
+ }
+
+ /** @type {boolean} */
+ get hasDatagramListener() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_DATAGRAM);
+ }
+
+ set hasDatagramListener(val) {
+ this[kHandle].setUint8(IDX_STATE_SESSION_DATAGRAM, val ? 1 : 0);
+ }
+
+ /** @type {boolean} */
+ get hasSessionTicketListener() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_SESSION_TICKET);
+ }
+
+ set hasSessionTicketListener(val) {
+ this[kHandle].setUint8(IDX_STATE_SESSION_SESSION_TICKET, val ? 1 : 0);
+ }
+
+ /** @type {boolean} */
+ get isClosing() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_CLOSING);
+ }
+
+ /** @type {boolean} */
+ get isGracefulClose() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_GRACEFUL_CLOSE);
+ }
+
+ /** @type {boolean} */
+ get isSilentClose() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_SILENT_CLOSE);
+ }
+
+ /** @type {boolean} */
+ get isStatelessReset() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_STATELESS_RESET);
+ }
+
+ /** @type {boolean} */
+ get isDestroyed() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_DESTROYED);
+ }
+
+ /** @type {boolean} */
+ get isHandshakeCompleted() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_HANDSHAKE_COMPLETED);
+ }
+
+ /** @type {boolean} */
+ get isHandshakeConfirmed() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_HANDSHAKE_CONFIRMED);
+ }
+
+ /** @type {boolean} */
+ get isStreamOpenAllowed() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_STREAM_OPEN_ALLOWED);
+ }
+
+ /** @type {boolean} */
+ get isPrioritySupported() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_PRIORITY_SUPPORTED);
+ }
+
+ /** @type {boolean} */
+ get isWrapped() {
+ return !!this[kHandle].getUint8(IDX_STATE_SESSION_WRAPPED);
+ }
+
+ /** @type {bigint} */
+ get lastDatagramId() {
+ return this[kHandle].getBigUint64(IDX_STATE_SESSION_LAST_DATAGRAM_ID);
+ }
+
+ toJSON() {
+ return {
+ hasPathValidationListener: this.hasPathValidationListener,
+ hasVersionNegotiationListener: this.hasVersionNegotiationListener,
+ hasDatagramListener: this.hasDatagramListener,
+ hasSessionTicketListener: this.hasSessionTicketListener,
+ isClosing: this.isClosing,
+ isGracefulClose: this.isGracefulClose,
+ isSilentClose: this.isSilentClose,
+ isStatelessReset: this.isStatelessReset,
+ isDestroyed: this.isDestroyed,
+ isHandshakeCompleted: this.isHandshakeCompleted,
+ isHandshakeConfirmed: this.isHandshakeConfirmed,
+ isStreamOpenAllowed: this.isStreamOpenAllowed,
+ isPrioritySupported: this.isPrioritySupported,
+ isWrapped: this.isWrapped,
+ lastDatagramId: `${this.lastDatagramId}`,
+ };
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `SessionState ${inspect({
+ hasPathValidationListener: this.hasPathValidationListener,
+ hasVersionNegotiationListener: this.hasVersionNegotiationListener,
+ hasDatagramListener: this.hasDatagramListener,
+ hasSessionTicketListener: this.hasSessionTicketListener,
+ isClosing: this.isClosing,
+ isGracefulClose: this.isGracefulClose,
+ isSilentClose: this.isSilentClose,
+ isStatelessReset: this.isStatelessReset,
+ isDestroyed: this.isDestroyed,
+ isHandshakeCompleted: this.isHandshakeCompleted,
+ isHandshakeConfirmed: this.isHandshakeConfirmed,
+ isStreamOpenAllowed: this.isStreamOpenAllowed,
+ isPrioritySupported: this.isPrioritySupported,
+ isWrapped: this.isWrapped,
+ lastDatagramId: this.lastDatagramId,
+ }, opts)}`;
+ }
+
+ [kFinishClose]() {
+ // Snapshot the state into a new DataView since the underlying
+ // buffer will be destroyed.
+ this[kHandle] = new DataView(this[kHandle]);
+ }
+}
+
+/**
+ * @param {ArrayBuffer} buffer
+ * @returns {SessionState}
+ */
+function createSessionState(buffer) {
+ return ReflectConstruct(function() {
+ this[kHandle] = new DataView(buffer);
+ }, [], SessionState);
+
+}
+
+class Session extends EventTarget {
+ constructor() {
+ throw new ERR_ILLEGAL_CONSTRUCTOR('Session');
+ }
+
+ get #isClosedOrClosing() {
+ return this[kHandle] === undefined || this[kIsPendingClose];
+ }
+
+ /** @type {SessionStats} */
+ get stats() { return this[kStats]; }
+
+ /** @type {SessionState} */
+ get state() { return this[kState]; }
+
+ /** @type {Endpoint} */
+ get endpoint() { return this[kEndpoint]; }
+
+ /** @type {SocketAddress|undefined} */
+ get localAddress() {
+ if (this.#isClosedOrClosing) return undefined;
+ return this[kEndpoint].address;
+ }
+
+ /** @type {SocketAddress|undefined} */
+ get remoteAddress() {
+ if (this.#isClosedOrClosing) return undefined;
+ if (this[kRemoteAddress] === undefined) {
+ const addr = this[kHandle].getRemoteAddress();
+ if (addr !== undefined) {
+ this[kRemoteAddress] = new InternalSocketAddress(addr);
+ }
+ }
+ return this[kRemoteAddress];
+ }
+
+ /**
+ * @returns {BidirectionalStream}
+ */
+ openBidirectionalStream() {
+ if (this.#isClosedOrClosing) {
+ throw new ERR_INVALID_STATE('Session is closed');
+ }
+ const handle = this[kHandle].openStream(STREAM_DIRECTION_BIDIRECTIONAL);
+ if (handle === undefined) {
+ throw new ERR_QUIC_OPEN_STREAM_FAILED();
+ }
+ const stream = createBidirectionalStream(handle, this);
+ this[kStreams].add(stream);
+ return stream;
+ }
+
+ /**
+ * @returns {UnidirectionalInboundStream}
+ */
+ openUnidirectionalStream() {
+ if (this.#isClosedOrClosing) {
+ throw new ERR_INVALID_STATE('Session is closed');
+ }
+ const handle = this[kHandle].openStream(STREAM_DIRECTION_UNIDIRECTIONAL);
+ if (handle === undefined) {
+ throw new ERR_QUIC_OPEN_STREAM_FAILED();
+ }
+ const stream = createUnidirectionalOutboundStream(handle, this);
+ this[kStreams].add(stream);
+ return stream;
+ }
+
+ updateKey() {
+ if (this.#isClosedOrClosing) {
+ throw new ERR_INVALID_STATE('Session is closed');
+ }
+ this[kHandle].updateKey();
+ }
+
+ close() {
+ if (!this.#isClosedOrClosing) {
+ this[kIsPendingClose] = true;
+ this[kHandle]?.gracefulClose();
+ }
+ return this[kPendingClose].promise;
+ }
+
+ /**
+ * Returns a Readable/WritableStream pair that can be used to send and
+ * receive unreliable datagrams on this session. The order or even success
+ * of delivery is not guaranteed.
+ * @type {Datagrams}
+ */
+ get datagrams() {
+ this[kDatagramsReadable] ??= new DatagramReadableStream();
+ this[kDatagramsWritable] ??= new DatagramWritableStream(
+ async (chunk) => {
+ if (this.#isClosedOrClosing) {
+ throw new ERR_INVALID_STATE('Session is closed');
+ }
+ this[kHandle].sendDatagram(chunk);
+ },
+ async () => {
+ // The writable stream was closed. There's really not much
+ // for us to do here yet.
+ },
+ async (reason) => {
+ // The writable stream was aborted. There's really not much
+ // for us to do here yet.
+ },
+ );
+ return {
+ readable: this[kDatagramsReadable].readable,
+ writable: this[kDatagramsWritable].writable,
+ };
+ }
+
+ [kNewListener](size, type, listener, once, capture, passive, weak) {
+ super[kNewListener](size, type, listener, once, capture, passive, weak);
+ if (type === 'pathValidation') {
+ this[kState].hasPathValidationListener = true;
+ } else if (type === 'versionNegotiation') {
+ this[kState].hasVersionNegotiationListener = true;
+ } else if (type === 'datagram') {
+ this[kState].hasDatagramListener = true;
+ } else if (type === 'sessionTicket') {
+ this[kState].hasSessionTicketListener = true;
+ }
+ }
+
+ [kRemoveListener](size, type, listener, capture) {
+ super[kRemoveListener](size, type, listener, capture);
+ if (type === 'pathValidation') {
+ this[kState].hasPathValidationListener = size > 0;
+ } else if (type === 'versionNegotiation') {
+ this[kState].hasVersionNegotiationListener = size > 0;
+ } else if (type === 'datagram') {
+ this[kState].hasDatagramListener = size > 0;
+ } else if (type === 'sessionTicket') {
+ this[kState].hasSessionTicketListener = size > 0;
+ }
+ }
+
+ [kNewStream](handle) {
+ const stream = createBidirectionalStream(handle, this);
+ this[kStreams].add(stream);
+ this.dispatchEvent(new StreamEvent(stream));
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `Session ${inspect({
+ stats: this.stats,
+ state: this.state,
+ endpoint: this.endpoint,
+ }, opts)}`;
+ }
+
+ [kFinishClose](errorType, code, reason) {
+ // TODO(@jasnell): Finish the implementation
+ }
+
+ [kHandshake](sni, alpn, cipher, cipherVersion,
+ validationErrorReason,
+ validationErrorCode,
+ earlyDataAccepted) {}
+
+ [kPathValidation](result,
+ newLocalAddress,
+ newRemoteAddress,
+ oldLocalAddress,
+ oldRemoteAddress,
+ preferredAddress) {}
+
+ [kTicket](ticket) {}
+
+ [kVersionNegotiation](version, requestedVersions, supportedVersions) {}
+}
+
+/**
+ * @param {object} handle
+ * @returns {Session}
+ */
+function createSession(handle, endpoint) {
+ const instance = ReflectConstruct(EventTarget, [], Session);
+ instance[kHandle] = handle;
+ instance[kStats] = createSessionStats(handle.stats);
+ instance[kState] = createSessionState(handle.state);
+ instance[kEndpoint] = endpoint;
+ instance[kStreams] = [];
+ instance[kRemoteAddress] = undefined;
+ instance[kIsPendingClose] = false;
+ instance[kPendingClose] = PromiseWithResolvers();
+ handle[kOwner] = instance;
+ return instance;
+}
+
+class EndpointStats {
+ constructor() {
+ throw new ERR_ILLEGAL_CONSTRUCTOR('EndpointStats');
+ }
+
+ /** @type {bigint} */
+ get createdAt() {
+ return this[kHandle][IDX_STATS_ENDPOINT_CREATED_AT];
+ }
+
+ /** @type {bigint} */
+ get destroyedAt() {
+ return this[kHandle][IDX_STATS_ENDPOINT_DESTROYED_AT];
+ }
+
+ /** @type {bigint} */
+ get bytesReceived() {
+ return this[kHandle][IDX_STATS_ENDPOINT_BYTES_RECEIVED];
+ }
+
+ /** @type {bigint} */
+ get bytesSent() {
+ return this[kHandle][IDX_STATS_ENDPOINT_BYTES_SENT];
+ }
+
+ /** @type {bigint} */
+ get packetsReceived() {
+ return this[kHandle][IDX_STATS_ENDPOINT_PACKETS_RECEIVED];
+ }
+
+ /** @type {bigint} */
+ get packetsSent() {
+ return this[kHandle][IDX_STATS_ENDPOINT_PACKETS_SENT];
+ }
+
+ /** @type {bigint} */
+ get serverSessions() {
+ return this[kHandle][IDX_STATS_ENDPOINT_SERVER_SESSIONS];
+ }
+
+ /** @type {bigint} */
+ get clientSessions() {
+ return this[kHandle][IDX_STATS_ENDPOINT_CLIENT_SESSIONS];
+ }
+
+ /** @type {bigint} */
+ get serverBusyCount() {
+ return this[kHandle][IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT];
+ }
+
+ /** @type {bigint} */
+ get retryCount() {
+ return this[kHandle][IDX_STATS_ENDPOINT_RETRY_COUNT];
+ }
+
+ /** @type {bigint} */
+ get versionNegotiationCount() {
+ return this[kHandle][IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT];
+ }
+
+ /** @type {bigint} */
+ get statelessResetCount() {
+ return this[kHandle][IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT];
+ }
+
+ /** @type {bigint} */
+ get immediateCloseCount() {
+ return this[kHandle][IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT];
+ }
+
+ toJSON() {
+ return {
+ createdAt: `${this.createdAt}`,
+ destroyedAt: `${this.destroyedAt}`,
+ bytesReceived: `${this.bytesReceived}`,
+ bytesSent: `${this.bytesSent}`,
+ packetsReceived: `${this.packetsReceived}`,
+ packetsSent: `${this.packetsSent}`,
+ serverSessions: `${this.serverSessions}`,
+ clientSessions: `${this.clientSessions}`,
+ serverBusyCount: `${this.serverBusyCount}`,
+ retryCount: `${this.retryCount}`,
+ versionNegotiationCount: `${this.versionNegotiationCount}`,
+ statelessResetCount: `${this.statelessResetCount}`,
+ immediateCloseCount: `${this.immediateCloseCount}`,
+ };
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `EndpointStats ${inspect({
+ createdAt: this.createdAt,
+ destroyedAt: this.destroyedAt,
+ bytesReceived: this.bytesReceived,
+ bytesSent: this.bytesSent,
+ packetsReceived: this.packetsReceived,
+ packetsSent: this.packetsSent,
+ serverSessions: this.serverSessions,
+ clientSessions: this.clientSessions,
+ serverBusyCount: this.serverBusyCount,
+ retryCount: this.retryCount,
+ versionNegotiationCount: this.versionNegotiationCount,
+ statelessResetCount: this.statelessResetCount,
+ immediateCloseCount: this.immediateCloseCount,
+ }, opts)}`;
+ }
+
+ [kFinishClose]() {
+ // Snapshot the stats into a new BigUint64Array since the underlying
+ // buffer will be destroyed.
+ this[kHandle] = new BigUint64Array(this[kHandle]);
+ }
+}
+
+/**
+ * @param {ArrayBuffer} buffer
+ * @returns {EndpointStats}
+ */
+function createEndpointStats(buffer) {
+ return ReflectConstruct(function() {
+ this[kHandle] = new BigUint64Array(buffer);
+ }, [], EndpointStats);
+}
+
+class EndpointState {
+ constructor() {
+ throw new ERR_INVALID_STATE('EndpointState');
+ }
+
+ /** @type {boolean} */
+ get isBound() {
+ return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_BOUND);
+ }
+
+ /** @type {boolean} */
+ get isReceiving() {
+ return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_RECEIVING);
+ }
+
+ /** @type {boolean} */
+ get isListening() {
+ return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_LISTENING);
+ }
+
+ /** @type {boolean} */
+ get isClosing() {
+ return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_CLOSING);
+ }
+
+ /** @type {boolean} */
+ get isBusy() {
+ return !!this[kHandle].getUint8(IDX_STATE_ENDPOINT_BUSY);
+ }
+
+ /** @type {boolean} */
+ get pendingCallbacks() {
+ return !!this[kHandle].getBigUint64(IDX_STATE_ENDPOINT_PENDING_CALLBACKS);
+ }
+
+ toJSON() {
+ return {
+ isBound: this.isBound,
+ isReceiving: this.isReceiving,
+ isListening: this.isListening,
+ isClosing: this.isClosing,
+ isBusy: this.isBusy,
+ pendingCallbacks: `${this.pendingCallbacks}`,
+ };
+ }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `EndpointState ${inspect({
+ isBound: this.isBound,
+ isReceiving: this.isReceiving,
+ isListening: this.isListening,
+ isClosing: this.isClosing,
+ isBusy: this.isBusy,
+ pendingCallbacks: this.pendingCallbacks,
+ }, opts)}`;
+ }
+
+ [kFinishClose]() {
+ // Snapshot the state into a new DataView since the underlying
+ // buffer will be destroyed.
+ this[kHandle] = new DataView(this[kHandle]);
+ }
+}
+
+/**
+ * @param {ArrayBuffer} buffer
+ * @returns {EndpointState}
+ */
+function createEndpointState(buffer) {
+ return ReflectConstruct(function() {
+ this[kHandle] = new DataView(buffer);
+ }, [], EndpointState);
+}
+
+class Endpoint extends EventTarget {
+ #handle = undefined;
+ #busy = false;
+ #listening = false;
+ #isPendingClose = false;
+ #pendingClose = undefined;
+ #address = undefined;
+ #stats = undefined;
+ #state = undefined;
+ #sessions = [];
+
+ /**
+ * @param {EndpointOptions} [options]
+ */
+ constructor(options = {}) {
+ validateObject(options, 'options');
+
+ const {
+ address,
+ retryTokenExpiration,
+ tokenExpiration,
+ maxConnectionsPerHost,
+ maxConnectionsTotal,
+ maxStatelessResetsPerHost,
+ addressLRUSize,
+ maxRetries,
+ maxPayloadSize,
+ unacknowledgedPacketThreshold,
+ handshakeTimeout,
+ maxStreamWindow,
+ maxWindow,
+ rxDiagnosticLoss,
+ txDiagnosticLoss,
+ udpReceiveBufferSize,
+ udpSendBufferSize,
+ udpTTL,
+ noUdpPayloadSizeShaping,
+ validateAddress,
+ disableActiveMigration,
+ ipv6Only,
+ cc,
+ resetTokenSecret,
+ tokenSecret,
+ } = options;
+
+ // All of the other options will be validated internally by the C++ code
+ if (address !== undefined && !SocketAddress.isSocketAddress(address)) {
+ throw new ERR_INVALID_ARG_TYPE('options.address', 'SocketAddress', address);
+ }
+
+ super();
+ this.#pendingClose = PromiseWithResolvers();
+ this.#handle = new Endpoint_({
+ address: address?.[kSocketAddressHandle],
+ retryTokenExpiration,
+ tokenExpiration,
+ maxConnectionsPerHost,
+ maxConnectionsTotal,
+ maxStatelessResetsPerHost,
+ addressLRUSize,
+ maxRetries,
+ maxPayloadSize,
+ unacknowledgedPacketThreshold,
+ handshakeTimeout,
+ maxStreamWindow,
+ maxWindow,
+ rxDiagnosticLoss,
+ txDiagnosticLoss,
+ udpReceiveBufferSize,
+ udpSendBufferSize,
+ udpTTL,
+ noUdpPayloadSizeShaping,
+ validateAddress,
+ disableActiveMigration,
+ ipv6Only,
+ cc,
+ resetTokenSecret,
+ tokenSecret,
+ });
+ this.#handle[kOwner] = this;
+ this.#stats = createEndpointStats(this.#handle.stats);
+ this.#state = createEndpointState(this.#handle.state);
+ }
+
+ /** @type {EndpointStats} */
+ get stats() { return this.#stats; }
+
+ /** @type {EndpointState} */
+ get state() { return this.#state; }
+
+ get #isClosedOrClosing() {
+ return this.#handle === undefined || this.#isPendingClose;
+ }
+
+ /**
+ * When an endpoint is marked as busy, it will not accept new connections.
+ * Existing connections will continue to work.
+ * @type {boolean}
+ */
+ get busy() { return this.#busy; }
+
+ /**
+ * @type {boolean}
+ */
+ set busy(val) {
+ if (this.#isClosedOrClosing) {
+ throw new ERR_INVALID_STATE('Endpoint is closed');
+ }
+ // The val is allowed to be any truthy value
+ val = !!val;
+ // Non-op if there is no change
+ if (val !== this.#busy) {
+ this.#busy = val;
+ this.#handle.markBusy(this.#busy);
+ }
+ }
+
+ /**
+ * The local address the endpoint is bound to (if any)
+ * @type {SocketAddress|undefined}
+ */
+ get address() {
+ if (this.#isClosedOrClosing) return undefined;
+ if (this.#address === undefined) {
+ const addr = this.#handle.address();
+ if (addr !== undefined) this.#address = new InternalSocketAddress(addr);
+ }
+ return this.#address;
+ }
+
+ /**
+ * Configures the endpoint to listen for incoming connections.
+ * @param {SessionOptions} [options]
+ */
+ listen(options = {}) {
+ if (this.#isClosedOrClosing) {
+ throw new ERR_INVALID_STATE('Endpoint is closed');
+ }
+ if (this.#listening) {
+ throw new ERR_INVALID_STATE('Endpoint is already listening');
+ }
+
+ validateObject(options, 'options');
+
+ const {
+ version,
+ minVersion,
+ preferredAddressPolicy = 'default',
+ application = {},
+ transportParams = {},
+ tls = {},
+ qlog = false,
+ } = options;
+
+ this.#handle.listen({
+ version,
+ minVersion,
+ preferredAddressPolicy: getPreferredAddressPolicy(preferredAddressPolicy),
+ application,
+ transportParams,
+ tls: processTlsOptions(tls),
+ qlog,
+ });
+ this.#listening = true;
+ }
+
+ /**
+ * Initiates a session with a remote endpoint.
+ * @param {SocketAddress} address
+ * @param {SessionOptions} [options]
+ * @returns {Session}
+ */
+ connect(address, options = {}) {
+ if (this.#isClosedOrClosing) {
+ throw new ERR_INVALID_STATE('Endpoint is closed');
+ }
+ if (this.#busy) {
+ throw new ERR_INVALID_STATE('Endpoint is busy');
+ }
+ if (address === undefined || !SocketAddress.isSocketAddress(address)) {
+ throw new ERR_INVALID_ARG_TYPE('address', 'SocketAddress', address);
+ }
+
+ validateObject(options, 'options');
+ const {
+ version,
+ minVersion,
+ preferredAddressPolicy,
+ application,
+ transportParams,
+ tls,
+ qlog,
+ sessionTicket,
+ } = options;
+
+ const handle = this.#handle.connect(address[kSocketAddressHandle], {
+ version,
+ minVersion,
+ preferredAddressPolicy: getPreferredAddressPolicy(preferredAddressPolicy),
+ application,
+ transportParams,
+ tls: processTlsOptions(tls),
+ qlog,
+ }, sessionTicket);
+
+ if (handle === undefined) {
+ throw new ERR_QUIC_CONNECTION_FAILED();
+ }
+ const session = createSession(handle, this);
+ this.#sessions.push(session);
+ return session;
+ }
+
+ /**
+ * Gracefully closes the endpoint.
+ * @returns {Promise}
+ */
+ close() {
+ if (!this.#isClosedOrClosing) {
+ this.#isPendingClose = true;
+ this.#handle?.closeGracefully();
+ }
+ return this.#pendingClose.promise;
+ }
+
+ ref() {
+ if (!this.#isClosedOrClosing) this.#handle.ref(true);
+ }
+
+ unref() {
+ if (!this.#isClosedOrClosing) this.#handle.ref(false);
+ }
+
+ [kFinishClose](context, status) {
+ if (this.#handle === undefined) return;
+ this.#isPendingClose = false;
+ this.#address = undefined;
+ this.#busy = false;
+ this.#listening = false;
+ this.#isPendingClose = false;
+ this.#stats[kFinishClose]();
+ this.#state[kFinishClose]();
+
+ switch (context) {
+ case kCloseContextClose: {
+ this.#pendingClose.resolve();
+ break;
+ }
+ case kCloseContextBindFailure: {
+ this.#pendingClose.reject(
+ new ERR_QUIC_ENDPOINT_CLOSED('Bind failure', status));
+ break;
+ }
+ case kCloseContextListenFailure: {
+ this.#pendingClose.reject(
+ new ERR_QUIC_ENDPOINT_CLOSED('Listen failure', status));
+ break;
+ }
+ case kCloseContextReceiveFailure: {
+ this.#pendingClose.reject(
+ new ERR_QUIC_ENDPOINT_CLOSED('Receive failure', status));
+ break;
+ }
+ case kCloseContextSendFailure: {
+ this.#pendingClose.reject(
+ new ERR_QUIC_ENDPOINT_CLOSED('Send failure', status));
+ break;
+ }
+ case kCloseContextStartFailure: {
+ this.#pendingClose.reject(
+ new ERR_QUIC_ENDPOINT_CLOSED('Start failure', status));
+ break;
+ }
+ }
+
+ this.#pendingClose.resolve = undefined;
+ this.#pendingClose.reject = undefined;
+ this.#handle = undefined;
+ }
+
+ [kNewSession](handle) {
+ const session = createSession(handle, this);
+ this.#sessions.push(session);
+ this.dispatchEvent(new SessionEvent(session, this));
+ }
+
+ [SymbolAsyncDispose]() { return this.close(); }
+ [SymbolDispose]() { this.close(); }
+
+ [kInspect](depth, options) {
+ if (depth < 0)
+ return this;
+
+ const opts = {
+ ...options,
+ depth: options.depth == null ? null : options.depth - 1,
+ };
+
+ return `Endpoint ${inspect({
+ address: this.address,
+ busy: this.#busy,
+ closing: this.#isPendingClose,
+ destroyed: this.#handle === undefined,
+ listening: this.#listening,
+ stats: this.stats,
+ state: this.state,
+ }, opts)}`;
+ }
+};
+
+ObjectDefineProperties(Endpoint.prototype, {
+ busy: { __proto__: null, enumerable: true },
+ address: { __proto__: null, enumerable: true },
+ stats: { __proto__: null, enumerable: true },
+});
+ObjectDefineProperties(Endpoint, {
+ CC_ALGO_RENO: {
+ __proto__: null,
+ value: CC_ALGO_RENO,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+ CC_ALGO_CUBIC: {
+ __proto__: null,
+ value: CC_ALGO_CUBIC,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+ CC_ALGO_BBR: {
+ __proto__: null,
+ value: CC_ALGO_BBR,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+ CC_ALGO_RENO_STR: {
+ __proto__: null,
+ value: CC_ALGO_RENO_STR,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+ CC_ALGO_CUBIC_STR: {
+ __proto__: null,
+ value: CC_ALGO_CUBIC_STR,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+ CC_ALGO_BBR_STR: {
+ __proto__: null,
+ value: CC_ALGO_BBR_STR,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+});
+ObjectDefineProperties(Session, {
+ DEFAULT_CIPHERS: {
+ __proto__: null,
+ value: DEFAULT_CIPHERS,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+ DEFAULT_GROUPS: {
+ __proto__: null,
+ value: DEFAULT_GROUPS,
+ writable: false,
+ configurable: false,
+ enumerable: true,
+ },
+});
+
+module.exports = {
+ Endpoint,
+ EndpointStats,
+ EndpointState,
+ Session,
+ SessionStats,
+ SessionState,
+ Stream,
+ StreamStats,
+ StreamState,
+ BidirectionalStream,
+ UnidirectionalInboundStream,
+ UnidirectionalOutboundStream,
+ SessionEvent,
+ StreamEvent,
+};
diff --git a/src/quic/endpoint.cc b/src/quic/endpoint.cc
index 9e54e8e08ae0bc5..d06b18353fb370c 100644
--- a/src/quic/endpoint.cc
+++ b/src/quic/endpoint.cc
@@ -232,6 +232,7 @@ bool SetOption(Environment* env,
Maybe Endpoint::Options::From(Environment* env,
Local value) {
if (value.IsEmpty() || !value->IsObject()) {
+ if (value->IsUndefined()) return Just(Endpoint::Options());
THROW_ERR_INVALID_ARG_TYPE(env, "options must be an object");
return Nothing();
}
@@ -655,6 +656,25 @@ void Endpoint::InitPerContext(Realm* realm, Local