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/quic.js b/lib/internal/quic/quic.js new file mode 100644 index 000000000000000..012ad291bfa0478 --- /dev/null +++ b/lib/internal/quic/quic.js @@ -0,0 +1,2694 @@ +'use strict'; + +const { + ArrayIsArray, + BigUint64Array, + DataView, + ObjectDefineProperties, + ReflectConstruct, + Symbol, + SymbolAsyncDispose, + SymbolDispose, + Uint8Array, +} = 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 { + isArrayBufferView, +} = require('util/types'); + +const { + Buffer, +} = require('node:buffer'); + +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 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 kIsPendingClose = Symbol('kIsPendingClose'); +const kPendingClose = Symbol('kPendingClose'); + +/** + * @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 + */ + +/** + * @typedef {object} Path + * @property {SocketAddress} local The local address + * @property {SocketAddress} remote The remote address + */ + +/** + * 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)}`; + } +} +ObjectDefineProperties(SessionEvent.prototype, { + session: { __proto__: null, enumerable: true }, + endpoint: { __proto__: null, enumerable: true }, +}); + +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)}`; + } +} +ObjectDefineProperties(StreamEvent.prototype, { + stream: { __proto__: null, enumerable: true }, + session: { __proto__: null, enumerable: true }, +}); + +class DatagramEvent extends Event { + #datagram = undefined; + #session = undefined; + #early = false; + constructor(datagram, session, early) { + super('datagram'); + this.#datagram = datagram; + this.#session = session; + this.#early = early; + } + + /** @type {Uint8Array} */ + get datagram() { return this.#datagram; } + + /** @type {Session} */ + get session() { return this.#session; } + + /** @type {boolean} */ + get early() { return this.#early; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `DatagramEvent ${inspect({ + datagram: this.datagram, + session: this.session, + early: this.early, + }, opts)}`; + } +} +ObjectDefineProperties(DatagramEvent.prototype, { + datagram: { __proto__: null, enumerable: true }, + session: { __proto__: null, enumerable: true }, + early: { __proto__: null, enumerable: true }, +}); + +class DatagramStatusEvent extends Event { + #id = undefined; + #status = undefined; + #session = undefined; + + constructor(id, status, session) { + super('datagram-status'); + this.#id = id; + this.#status = status; + this.#session = session; + } + + /** @type {bigint} */ + get id() { return this.#id; } + + /** @type {'lost'|'acknowledged'} */ + get status() { return this.#status; } + + /** @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 `DatagramStatusEvent ${inspect({ + id: this.id, + status: this.status, + session: this.session, + }, opts)}`; + } +} +ObjectDefineProperties(DatagramStatusEvent.prototype, { + id: { __proto__: null, enumerable: true }, + status: { __proto__: null, enumerable: true }, + session: { __proto__: null, enumerable: true }, +}); + +class PathValidationEvent extends Event { + #result = undefined; + #newLocalAddress = undefined; + #newRemoteAddress = undefined; + #oldLocalAddress = undefined; + #oldRemoteAddress = undefined; + #preferredAddress = undefined; + #session = undefined; + + constructor(result, newLocalAddress, newRemoteAddress, + oldLocalAddress, oldRemoteAddress, + preferredAddress, session) { + super('path-validation'); + this.#result = result; + this.#newLocalAddress = newLocalAddress; + this.#newRemoteAddress = newRemoteAddress; + this.#oldLocalAddress = oldLocalAddress; + this.#oldRemoteAddress = oldRemoteAddress; + this.#preferredAddress = preferredAddress; + this.#session = session; + } + + /** @type {Session} */ + get session() { return this.#session; } + + /** @type {'aborted'|'failure'|'success'} */ + get result() { return this.#result; } + + /** @type {Path} */ + get newPath() { + return { + local: this.#newLocalAddress, + remote: this.#newRemoteAddress, + }; + } + + /** @type {Path} */ + get oldPath() { + return { + local: this.#oldLocalAddress, + remote: this.#oldRemoteAddress, + }; + } + + /** @type {boolean} */ + get isPreferredAddress() { return this.#preferredAddress; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `PathValidationEvent ${inspect({ + result: this.result, + newPath: this.newPath, + oldPath: this.oldPath, + isPreferredAddress: this.isPreferredAddress, + }, opts)}`; + } + +} +ObjectDefineProperties(PathValidationEvent.prototype, { + result: { __proto__: null, enumerable: true }, + newPath: { __proto__: null, enumerable: true }, + oldPath: { __proto__: null, enumerable: true }, + isPreferredAddress: { __proto__: null, enumerable: true }, +}); + +class SessionTicketEvent extends Event { + #ticket = undefined; + #session = undefined; + + constructor(ticket, session) { + super('session-ticket'); + this.#ticket = ticket; + this.#session = session; + } + + /** @type {Session} */ + get session() { return this.#session; } + + /** @type {object} */ + get ticket() { return this.#ticket; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `SessionTicketEvent ${inspect({ + ticket: this.ticket, + session: this.session, + }, opts)}`; + } +} +ObjectDefineProperties(SessionTicketEvent.prototype, { + ticket: { __proto__: null, enumerable: true }, + session: { __proto__: null, enumerable: true }, +}); + +class VersionNegotiationEvent extends Event { + #session = undefined; + #version = undefined; + #requestedVersions = undefined; + #supportedVersions = undefined; + + constructor(version, requestedVersions, supportedVersions, session) { + super('version-negotiation'); + this.#session = session; + this.#version = version; + this.#requestedVersions = requestedVersions; + this.#supportedVersions = supportedVersions; + } + + /** @type {Session} */ + get session() { return this.#session; } + + /** @type {number} */ + get version() { return this.#version; } + + /** @type {Array} */ + get requestedVersions() { return this.#requestedVersions; } + + /** @type {Array} */ + get supportedVersions() { return this.#supportedVersions; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `VersionNegotiationEvent ${inspect({ + version: this.version, + requestedVersions: this.requestedVersions, + supportedVersions: this.supportedVersions, + session: this.session, + }, opts)}`; + } +} +ObjectDefineProperties(VersionNegotiationEvent.prototype, { + version: { __proto__: null, enumerable: true }, + requestedVersions: { __proto__: null, enumerable: true }, + supportedVersions: { __proto__: null, enumerable: true }, + session: { __proto__: null, enumerable: true }, +}); + +class HandshakeEvent extends Event { + #session = undefined; + #sni = undefined; + #alpn = undefined; + #cipher = undefined; + #cipherVersion = undefined; + #validationErrorReason = undefined; + #validationErrorCode = undefined; + #earlyDataAccepted = false; + + constructor(sni, alpn, cipher, cipherVersion, + validationErrorReason, validationErrorCode, + earlyDataAccepted, session) { + super('handshake'); + this.#session = session; + this.#sni = sni; + this.#alpn = alpn; + this.#cipher = cipher; + this.#cipherVersion = cipherVersion; + this.#validationErrorReason = validationErrorReason; + this.#validationErrorCode = validationErrorCode; + this.#earlyDataAccepted = earlyDataAccepted; + } + + /** @type {Session} */ + get session() { return this.#session; } + + /** @type {string} */ + get sni() { return this.#sni; } + + /** @type {string} */ + get alpn() { return this.#alpn; } + + /** @type {string} */ + get cipher() { return this.#cipher; } + + /** @type {string} */ + get cipherVersion() { return this.#cipherVersion; } + + /** @type {string} */ + get validationErrorReason() { return this.#validationErrorReason; } + + /** @type {number} */ + get validationErrorCode() { return this.#validationErrorCode; } + + /** @type {boolean} */ + get earlyDataAccepted() { return this.#earlyDataAccepted; } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `HandshakeEvent ${inspect({ + sni: this.sni, + alpn: this.alpn, + cipher: this.cipher, + cipherVersion: this.cipherVersion, + validationErrorReason: this.validationErrorReason, + validationErrorCode: this.validationErrorCode, + earlyDataAccepted: this.earlyDataAccepted, + session: this.session, + }, opts)}`; + } +} +ObjectDefineProperties(HandshakeEvent.prototype, { + sni: { __proto__: null, enumerable: true }, + alpn: { __proto__: null, enumerable: true }, + cipher: { __proto__: null, enumerable: true }, + cipherVersion: { __proto__: null, enumerable: true }, + validationErrorReason: { __proto__: null, enumerable: true }, + validationErrorCode: { __proto__: null, enumerable: true }, + earlyDataAccepted: { __proto__: null, enumerable: true }, + session: { __proto__: null, enumerable: true }, +}); + +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]; + // The event is ignored if the session has been destroyed. + if (session.destroyed) return; + const datagram = new DatagramEvent(uint8Array, session, early); + session.dispatchEvent(datagram); + }, + onSessionDatagramStatus(id, status) { + const session = this[kOwner]; + // The event is ignored if the session has been destroyed. + if (session.destroyed) return; + const datagramStatus = new DatagramStatusEvent(id, status, session); + session.dispatchEvent(datagramStatus); + }, + onSessionHandshake(sni, alpn, cipher, cipherVersion, + validationErrorReason, + validationErrorCode, + earlyDataAccepted) { + const session = this[kOwner]; + // The event is ignored if the session has been destroyed. + if (session.destroyed) return; + const handshake = + new HandshakeEvent(sni, alpn, cipher, cipherVersion, + validationErrorReason, validationErrorCode, + earlyDataAccepted, session); + session.dispatchEvent(handshake); + }, + onSessionPathValidation(result, + newLocalAddress, + newRemoteAddress, + oldLocalAddress, + oldRemoteAddress, + preferredAddress) { + const session = this[kOwner]; + // The event is ignored if the session has been destroyed. + if (session.destroyed) return; + const pathValidation = new PathValidationEvent(result, + newLocalAddress, + newRemoteAddress, + oldLocalAddress, + oldRemoteAddress, + preferredAddress, + session); + session.dispatchEvent(pathValidation); + }, + onSessionTicket(ticket) { + const session = this[kOwner]; + // The event is ignored if the session has been destroyed. + if (session.destroyed) return; + const sessionTicket = new SessionTicketEvent(ticket, session); + session.dispatchEvent(sessionTicket); + }, + onSessionVersionNegotiation(version, + requestedVersions, + supportedVersions) { + const session = this[kOwner]; + // The event is ignored if the session has been destroyed. + if (session.destroyed) return; + const versionNegotiation = + new VersionNegotiationEvent(version, + requestedVersions, + supportedVersions, + session); + session.dispatchEvent(versionNegotiation); + // Note that immediately following a version negotiation event, the + // session will be destroyed. + }, + onStreamCreated(stream, direction) { + const session = this[kOwner]; + // The event is ignored if the session has been destroyed. + if (session.destroyed) { + stream.destroy(); + return; + }; + session[kNewStream](stream, direction); + }, + onStreamBlocked() {}, + onStreamClose(error) {}, + onStreamReset(error) {}, + onStreamHeaders(headers, kind) {}, + onStreamTrailers() {}, +}); + +/** + * @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]); + } +} +ObjectDefineProperties(StreamStats.prototype, { + createdAt: { __proto__: null, enumerable: true }, + receivedAt: { __proto__: null, enumerable: true }, + ackedAt: { __proto__: null, enumerable: true }, + closingAt: { __proto__: null, enumerable: true }, + destroyedAt: { __proto__: null, enumerable: true }, + bytesReceived: { __proto__: null, enumerable: true }, + bytesSent: { __proto__: null, enumerable: true }, + maxOffset: { __proto__: null, enumerable: true }, + maxOffsetAcknowledged: { __proto__: null, enumerable: true }, + maxOffsetReceived: { __proto__: null, enumerable: true }, + finalSize: { __proto__: null, enumerable: true }, +}); + +/** + * @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)}`; + } +} +ObjectDefineProperties(StreamState.prototype, { + id: { __proto__: null, enumerable: true }, + finSent: { __proto__: null, enumerable: true }, + finReceived: { __proto__: null, enumerable: true }, + readEnded: { __proto__: null, enumerable: true }, + writeEnded: { __proto__: null, enumerable: true }, + destroyed: { __proto__: null, enumerable: true }, + paused: { __proto__: null, enumerable: true }, + reset: { __proto__: null, enumerable: true }, + hasReader: { __proto__: null, enumerable: true }, + wantsBlock: { __proto__: null, enumerable: true }, + wantsHeaders: { __proto__: null, enumerable: true }, + wantsReset: { __proto__: null, enumerable: true }, + wantsTrailers: { __proto__: null, enumerable: true }, +}); + +/** + * @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; } +} +ObjectDefineProperties(Stream.prototype, { + stats: { __proto__: null, enumerable: true }, + state: { __proto__: null, enumerable: true }, + session: { __proto__: null, enumerable: true }, + id: { __proto__: null, enumerable: true }, +}); + +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)}`; + } +} +ObjectDefineProperties(BidirectionalStream.prototype, { + readable: { __proto__: null, enumerable: true }, + writable: { __proto__: null, enumerable: true }, +}); + +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)}`; + } +} +ObjectDefineProperties(UnidirectionalInboundStream.prototype, { + readable: { __proto__: null, enumerable: true }, +}); + +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)}`; + } +} +ObjectDefineProperties(UnidirectionalOutboundStream.prototype, { + writable: { __proto__: null, enumerable: true }, +}); + +/** + * @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) { + 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]); + } +} +ObjectDefineProperties(SessionStats.prototype, { + createdAt: { __proto__: null, enumerable: true }, + closingAt: { __proto__: null, enumerable: true }, + destroyedAt: { __proto__: null, enumerable: true }, + handshakeCompletedAt: { __proto__: null, enumerable: true }, + handshakeConfirmedAt: { __proto__: null, enumerable: true }, + gracefulClosingAt: { __proto__: null, enumerable: true }, + bytesReceived: { __proto__: null, enumerable: true }, + bytesSent: { __proto__: null, enumerable: true }, + bidiInStreamCount: { __proto__: null, enumerable: true }, + bidiOutStreamCount: { __proto__: null, enumerable: true }, + uniInStreamCount: { __proto__: null, enumerable: true }, + uniOutStreamCount: { __proto__: null, enumerable: true }, + lossRetransmitCount: { __proto__: null, enumerable: true }, + maxBytesInFlights: { __proto__: null, enumerable: true }, + bytesInFlight: { __proto__: null, enumerable: true }, + blockCount: { __proto__: null, enumerable: true }, + cwnd: { __proto__: null, enumerable: true }, + latestRtt: { __proto__: null, enumerable: true }, + minRtt: { __proto__: null, enumerable: true }, + rttVar: { __proto__: null, enumerable: true }, + smoothedRtt: { __proto__: null, enumerable: true }, + ssthresh: { __proto__: null, enumerable: true }, + datagramsReceived: { __proto__: null, enumerable: true }, + datagramsSent: { __proto__: null, enumerable: true }, + datagramsAcknowledged: { __proto__: null, enumerable: true }, + datagramsLost: { __proto__: null, enumerable: true }, +}); + +/** + * + * @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]); + } +} +ObjectDefineProperties(SessionState.prototype, { + hasPathValidationListener: { __proto__: null, enumerable: true }, + hasVersionNegotiationListener: { __proto__: null, enumerable: true }, + hasDatagramListener: { __proto__: null, enumerable: true }, + hasSessionTicketListener: { __proto__: null, enumerable: true }, + isClosing: { __proto__: null, enumerable: true }, + isGracefulClose: { __proto__: null, enumerable: true }, + isSilentClose: { __proto__: null, enumerable: true }, + isStatelessReset: { __proto__: null, enumerable: true }, + isDestroyed: { __proto__: null, enumerable: true }, + isHandshakeCompleted: { __proto__: null, enumerable: true }, + isHandshakeConfirmed: { __proto__: null, enumerable: true }, + isStreamOpenAllowed: { __proto__: null, enumerable: true }, + isPrioritySupported: { __proto__: null, enumerable: true }, + isWrapped: { __proto__: null, enumerable: true }, + lastDatagramId: { __proto__: null, enumerable: true }, +}); + +/** + * @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 {Path} */ + get path() { + if (this.#isClosedOrClosing) return undefined; + if (this[kRemoteAddress] === undefined) { + const addr = this[kHandle].getRemoteAddress(); + if (addr !== undefined) { + this[kRemoteAddress] = new InternalSocketAddress(addr); + } + } + return { + local: this[kEndpoint].address, + remote: this[kRemoteAddress], + }; + } + + /** + * @returns {BidirectionalStream} + */ + openBidirectionalStream() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + if (!this.state.isStreamOpenAllowed) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + 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].push(stream); + return stream; + } + + /** + * @returns {UnidirectionalInboundStream} + */ + openUnidirectionalStream() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + if (!this.state.isStreamOpenAllowed) { + throw new ERR_QUIC_OPEN_STREAM_FAILED(); + } + 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].push(stream); + return stream; + } + + /** + * Initiate a key update. + */ + updateKey() { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + this[kHandle].updateKey(); + } + + /** + * Gracefully closes the session. Any streams created on the session will be + * allowed to complete gracefully and any datagrams that have already been + * queued for sending will be allowed to complete. Once all streams and been + * completed and all datagrams have been sent, the session will be closed. + * New streams will not be allowed to be created. The returns promise will + * be resolved when the session closes, or will be rejected if the session + * closes abruptly due to an error. + * @returns {Promise} + */ + close() { + if (!this.#isClosedOrClosing) { + this[kIsPendingClose] = true; + this[kHandle]?.gracefulClose(); + } + return this.closed; + } + + /** + * A promise that is resolved when the session is closed, or is rejected if + * the session is closed abruptly due to an error. + * @type {Promise} + */ + get closed() { return this[kPendingClose].promise; } + + /** @type {boolean} */ + get destroyed() { return this[kHandle] === undefined; } + + /** + * Forcefully closes the session abruptly without waiting for streams to be + * completed naturally. Any streams that are still open will be immediately + * destroyed and any queued datagrams will be dropped. If an error is given, + * the closed promise will be rejected with that error. If no error is given, + * the closed promise will be resolved. + * @param {any} error + */ + destroy(error) { + // TODO(@jasnell): Implement. + } + + /** + * Send a datagram. The id of the sent datagram will be returned. The status + * of the sent datagram will be reported via the datagram-status event if + * possible. + * + * If a string is given it will be encoded as UTF-8. + * + * If an ArrayBufferView is given, the view will be copied. + * @param {ArrayBufferView|string} datagram The datagram payload + * @returns {bigint} The datagram ID + */ + sendDatagram(datagram) { + if (this.#isClosedOrClosing) { + throw new ERR_INVALID_STATE('Session is closed'); + } + if (typeof datagram === 'string') { + datagram = Buffer.from(datagram, 'utf8'); + } else { + if (!isArrayBufferView(datagram)) { + throw new ERR_INVALID_ARG_TYPE('datagram', + ['ArrayBufferView', 'string'], + datagram); + } + datagram = new Uint8Array(datagram.buffer, + datagram.byteOffset, + datagram.byteLength); + } + return this[kHandle].sendDatagram(new Uint8Array(datagram)); + } + + [kNewListener](size, type, listener, once, capture, passive, weak) { + super[kNewListener](size, type, listener, once, capture, passive, weak); + if (type === 'path-validation') { + this[kState].hasPathValidationListener = true; + } else if (type === 'version-negotiation') { + this[kState].hasVersionNegotiationListener = true; + } else if (type === 'datagram') { + this[kState].hasDatagramListener = true; + } else if (type === 'session-ticket') { + this[kState].hasSessionTicketListener = true; + } + } + + [kRemoveListener](size, type, listener, capture) { + super[kRemoveListener](size, type, listener, capture); + if (type === 'path-validation') { + this[kState].hasPathValidationListener = size > 0; + } else if (type === 'version-negotiation') { + this[kState].hasVersionNegotiationListener = size > 0; + } else if (type === 'datagram') { + this[kState].hasDatagramListener = size > 0; + } else if (type === 'session-ticket') { + this[kState].hasSessionTicketListener = size > 0; + } + } + + [kNewStream](handle, direction) { + const stream = (() => { + if (direction === 0) { + return createBidirectionalStream(handle, this); + } + return createUnidirectionalInboundStream(handle, this); + })(); + this[kStreams].push(stream); + const event = new StreamEvent(stream, this); + this.dispatchEvent(event); + } + + [kInspect](depth, options) { + if (depth < 0) + return this; + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `Session ${inspect({ + closed: this[kPendingClose].promise, + closing: this[kIsPendingClose], + destroyed: this.destroyed, + endpoint: this.endpoint, + path: this.path, + state: this.state, + stats: this.stats, + streams: this[kStreams], + }, opts)}`; + } + + [kFinishClose](errorType, code, reason) { + // TODO(@jasnell): Finish the implementation + } +} +ObjectDefineProperties(Session.prototype, { + closed: { __proto__: null, enumerable: true }, + destroyed: { __proto__: null, enumerable: true }, + endpoint: { __proto__: null, enumerable: true }, + localAddress: { __proto__: null, enumerable: true }, + remoteAddress: { __proto__: null, enumerable: true }, + state: { __proto__: null, enumerable: true }, + stats: { __proto__: null, enumerable: true }, +}); + +/** + * @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] = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials + 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]); + } +} +ObjectDefineProperties(EndpointStats.prototype, { + createdAt: { __proto__: null, enumerable: true }, + destroyedAt: { __proto__: null, enumerable: true }, + bytesReceived: { __proto__: null, enumerable: true }, + bytesSent: { __proto__: null, enumerable: true }, + packetsReceived: { __proto__: null, enumerable: true }, + packetsSent: { __proto__: null, enumerable: true }, + serverSessions: { __proto__: null, enumerable: true }, + clientSessions: { __proto__: null, enumerable: true }, + serverBusyCount: { __proto__: null, enumerable: true }, + retryCount: { __proto__: null, enumerable: true }, + versionNegotiationCount: { __proto__: null, enumerable: true }, + statelessResetCount: { __proto__: null, enumerable: true }, + immediateCloseCount: { __proto__: null, enumerable: true }, +}); + +/** + * @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); + } + + /** + * The number of underlying callbacks that are pending. If the session + * is closing, these are the number of callbacks that the session is + * waiting on before it can be closed. + * @type {bigint} + */ + 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]); + } +} +ObjectDefineProperties(EndpointState.prototype, { + isBound: { __proto__: null, enumerable: true }, + isReceiving: { __proto__: null, enumerable: true }, + isListening: { __proto__: null, enumerable: true }, + isClosing: { __proto__: null, enumerable: true }, + isBusy: { __proto__: null, enumerable: true }, + pendingCallbacks: { __proto__: null, enumerable: true }, +}); + +/** + * @param {ArrayBuffer} buffer + * @returns {EndpointState} + */ +function createEndpointState(buffer) { + return ReflectConstruct(function() { + this[kHandle] = new DataView(buffer); + }, [], EndpointState); +} + +class Endpoint extends EventTarget { + #address = undefined; + #busy = false; + #handle = undefined; + #isPendingClose = false; + #listening = false; + #pendingClose = undefined; + #pendingError = undefined; + #sessions = []; + #state = undefined; + #stats = undefined; + + /** + * @param {EndpointOptions} [options] + */ + constructor(options = {}) { + validateObject(options, 'options'); + + let { address } = options; + const { + 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)) { + if (typeof address === 'object' && address !== null) { + address = new SocketAddress(address); + } else { + throw new ERR_INVALID_ARG_TYPE('options.address', 'SocketAddress', address); + } + } + + super(); + this.#pendingClose = Promise.withResolvers(); // eslint-disable-line node-core/prefer-primordials + 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)) { + if (typeof address === 'object' && address !== null) { + address = new SocketAddress(address); + } else { + throw new ERR_INVALID_ARG_TYPE('address', 'SocketAddress', address); + } + } + + validateObject(options, 'options'); + const { + version, + minVersion, + preferredAddressPolicy = 'default', + application = {}, + transportParams = {}, + tls = {}, + qlog = false, + 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. Any existing sessions will be permitted + * to gracefully, afterwhich the endpoint will be closed. New sessions will + * not be accepted or created. The returned promise will be resolved when + * closing is complete, or will be rejected if the endpoint is closed abruptly + * due to an error. + * @returns {Promise} + */ + close() { + if (!this.#isClosedOrClosing) { + this.#isPendingClose = true; + this.#handle?.closeGracefully(); + } + return this.closed; + } + + /** + * Returns a promise that is resolved when the endpoint is closed or rejects + * if the endpoint is closed abruptly due to an error. The closed property + * is set to the same promise that is returned by the close() method. + * @type {Promise} + */ + get closed() { return this.#pendingClose.promise; } + + /** @type {boolean} */ + get destroyed() { return this.#handle === undefined; } + + /** + * Forcefully terminates the endpoint by immediately destroying all sessions + * after calling close. If an error is given, the closed promise will be + * rejected with that error. If no error is given, the closed promise will + * be resolved. + * @param {any} error + */ + destroy(error) { + if (!this.#isClosedOrClosing) { + // Start closing the endpoint. + this.#pendingError = error; + // Trigger a graceful close of the endpoint that'll ensure that the + // endpoint is closed down after all sessions are closed... + this.close(); + } + // Now, force all sessions to be abruptly closed... + for (const session of this.#sessions) { + session.destroy(error); + } + } + + ref() { + if (this.#handle !== undefined) this.#handle.ref(true); + } + + unref() { + if (this.#handle !== undefined) 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](); + this.#sessions = undefined; + + switch (context) { + case kCloseContextClose: { + if (this.#pendingError !== undefined) { + this.#pendingClose.reject(this.#pendingError); + } else { + 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.#pendingError = undefined; + 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, + closed: this.#pendingClose.promise, + closing: this.#isPendingClose, + destroyed: this.destroyed, + listening: this.#listening, + sessions: this.#sessions, + stats: this.stats, + state: this.state, + }, opts)}`; + } +}; + +ObjectDefineProperties(Endpoint.prototype, { + address: { __proto__: null, enumerable: true }, + busy: { __proto__: null, enumerable: true }, + closed: { __proto__: null, enumerable: true }, + destroyed: { __proto__: null, enumerable: true }, + state: { __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, + DatagramEvent, + DatagramStatusEvent, + HandshakeEvent, + PathValidationEvent, + SessionEvent, + SessionTicketEvent, + StreamEvent, + VersionNegotiationEvent, +}; 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 target) { NODE_DEFINE_CONSTANT(target, DEFAULT_REGULARTOKEN_EXPIRATION); NODE_DEFINE_CONSTANT(target, DEFAULT_MAX_PACKET_LENGTH); + static constexpr auto CLOSECONTEXT_CLOSE = + static_cast(CloseContext::CLOSE); + static constexpr auto CLOSECONTEXT_BIND_FAILURE = + static_cast(CloseContext::BIND_FAILURE); + static constexpr auto CLOSECONTEXT_LISTEN_FAILURE = + static_cast(CloseContext::LISTEN_FAILURE); + static constexpr auto CLOSECONTEXT_RECEIVE_FAILURE = + static_cast(CloseContext::RECEIVE_FAILURE); + static constexpr auto CLOSECONTEXT_SEND_FAILURE = + static_cast(CloseContext::SEND_FAILURE); + static constexpr auto CLOSECONTEXT_START_FAILURE = + static_cast(CloseContext::START_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_CLOSE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_BIND_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_LISTEN_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_RECEIVE_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_SEND_FAILURE); + NODE_DEFINE_CONSTANT(target, CLOSECONTEXT_START_FAILURE); + SetConstructorFunction(realm->context(), target, "Endpoint", @@ -681,6 +701,7 @@ Endpoint::Endpoint(Environment* env, udp_(this), addrLRU_(options_.address_lru_size) { MakeWeak(); + STAT_RECORD_TIMESTAMP(Stats, created_at); IF_QUIC_DEBUG(env) { Debug(this, "Endpoint created. Options %s", options.ToString()); } @@ -703,6 +724,7 @@ SocketAddress Endpoint::local_address() const { void Endpoint::MarkAsBusy(bool on) { Debug(this, "Marking endpoint as %s", on ? "busy" : "not busy"); + if (on) STAT_INCREMENT(Stats, server_busy_count); state_->busy = on ? 1 : 0; } @@ -1086,6 +1108,7 @@ void Endpoint::Destroy(CloseContext context, int status) { state_->bound = 0; state_->receiving = 0; BindingData::Get(env()).listening_endpoints.erase(this); + STAT_RECORD_TIMESTAMP(Stats, destroyed_at); EmitClose(close_context_, close_status_); } @@ -1690,7 +1713,7 @@ void Endpoint::New(const FunctionCallbackInfo& args) { void Endpoint::DoConnect(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); // args[0] is a SocketAddress // args[1] is a Session OptionsObject (see session.cc) @@ -1723,7 +1746,7 @@ void Endpoint::DoConnect(const FunctionCallbackInfo& args) { void Endpoint::DoListen(const FunctionCallbackInfo& args) { Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); auto env = Environment::GetCurrent(args); Session::Options options; @@ -1734,20 +1757,20 @@ void Endpoint::DoListen(const FunctionCallbackInfo& args) { void Endpoint::MarkBusy(const FunctionCallbackInfo& args) { Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); endpoint->MarkAsBusy(args[0]->IsTrue()); } void Endpoint::DoCloseGracefully(const FunctionCallbackInfo& args) { Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); endpoint->CloseGracefully(); } void Endpoint::LocalAddress(const FunctionCallbackInfo& args) { auto env = Environment::GetCurrent(args); Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); if (endpoint->is_closed() || !endpoint->udp_.is_bound()) return; auto addr = SocketAddressBase::Create( env, std::make_shared(endpoint->local_address())); @@ -1756,7 +1779,7 @@ void Endpoint::LocalAddress(const FunctionCallbackInfo& args) { void Endpoint::Ref(const FunctionCallbackInfo& args) { Endpoint* endpoint; - ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.Holder()); + ASSIGN_OR_RETURN_UNWRAP(&endpoint, args.This()); auto env = Environment::GetCurrent(args); if (args[0]->BooleanValue(env->isolate())) { endpoint->udp_.Ref(); diff --git a/src/quic/session.cc b/src/quic/session.cc index c53e7f584470f39..c3381930540d9fa 100644 --- a/src/quic/session.cc +++ b/src/quic/session.cc @@ -1681,10 +1681,16 @@ void Session::EmitStream(BaseObjectPtr stream) { if (is_destroyed()) return; if (!env()->can_call_into_js()) return; CallbackScope cb_scope(this); - Local arg = stream->object(); + auto isolate = env()->isolate(); + Local argv[] = { + stream->object(), + Integer::NewFromUnsigned(isolate, + static_cast(stream->direction())), + }; Debug(this, "Notifying JavaScript of stream created"); - MakeCallback(BindingData::Get(env()).stream_created_callback(), 1, &arg); + MakeCallback( + BindingData::Get(env()).stream_created_callback(), arraysize(argv), argv); } void Session::EmitVersionNegotiation(const ngtcp2_pkt_hd& hd, @@ -2357,6 +2363,11 @@ void Session::InitPerContext(Realm* realm, Local target) { NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MAX); NODE_DEFINE_CONSTANT(target, QUIC_PROTO_MIN); + NODE_DEFINE_STRING_CONSTANT( + target, "DEFAULT_CIPHERS", TLSContext::DEFAULT_CIPHERS); + NODE_DEFINE_STRING_CONSTANT( + target, "DEFAULT_GROUPS", TLSContext::DEFAULT_GROUPS); + #define V(name, _) IDX_STATS_SESSION_##name, enum SessionStatsIdx { SESSION_STATS(V) IDX_STATS_SESSION_COUNT }; #undef V diff --git a/src/quic/tlscontext.cc b/src/quic/tlscontext.cc index 8441e491d1ca6a4..6af5bc3bf63a689 100644 --- a/src/quic/tlscontext.cc +++ b/src/quic/tlscontext.cc @@ -276,7 +276,7 @@ crypto::SSLCtxPointer TLSContext::Initialize() { break; } case Side::CLIENT: { - ctx_.reset(SSL_CTX_new(TLS_client_method())); + ctx.reset(SSL_CTX_new(TLS_client_method())); CHECK_EQ(ngtcp2_crypto_quictls_configure_client_context(ctx.get()), 0); SSL_CTX_set_session_cache_mode( diff --git a/test/parallel/test-quic-internal-endpoint-options.js b/test/parallel/test-quic-internal-endpoint-options.js index 672fac18b437797..cdb9c13b93a3b74 100644 --- a/test/parallel/test-quic-internal-endpoint-options.js +++ b/test/parallel/test-quic-internal-endpoint-options.js @@ -8,50 +8,19 @@ const { throws, } = require('node:assert'); -const { internalBinding } = require('internal/test/binding'); -const quic = internalBinding('quic'); +const { Endpoint } = require('internal/quic/quic'); -quic.setCallbacks({ - onEndpointClose() {}, - onSessionNew() {}, - onSessionClose() {}, - onSessionDatagram() {}, - onSessionDatagramStatus() {}, - onSessionHandshake() {}, - onSessionPathValidation() {}, - onSessionTicket() {}, - onSessionVersionNegotiation() {}, - onStreamCreated() {}, - onStreamBlocked() {}, - onStreamClose() {}, - onStreamReset() {}, - onStreamHeaders() {}, - onStreamTrailers() {}, -}); - -throws(() => new quic.Endpoint(), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); - -throws(() => new quic.Endpoint('a'), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); - -throws(() => new quic.Endpoint(null), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' -}); - -throws(() => new quic.Endpoint(false), { - code: 'ERR_INVALID_ARG_TYPE', - message: 'options must be an object' +['a', null, false, NaN].forEach((i) => { + throws(() => new Endpoint(i), { + code: 'ERR_INVALID_ARG_TYPE', + }); }); { // Just Works... using all defaults - new quic.Endpoint({}); + new Endpoint({}); + new Endpoint(); + new Endpoint(undefined); } const cases = [ @@ -136,14 +105,12 @@ const cases = [ { key: 'cc', valid: [ - quic.CC_ALGO_RENO, - quic.CC_ALGO_CUBIC, - quic.CC_ALGO_BBR, - quic.CC_ALGO_BBR2, - quic.CC_ALGO_RENO_STR, - quic.CC_ALGO_CUBIC_STR, - quic.CC_ALGO_BBR_STR, - quic.CC_ALGO_BBR2_STR, + Endpoint.CC_ALGO_RENO, + Endpoint.CC_ALGO_CUBIC, + Endpoint.CC_ALGO_BBR, + Endpoint.CC_ALGO_RENO_STR, + Endpoint.CC_ALGO_CUBIC_STR, + Endpoint.CC_ALGO_BBR_STR, ], invalid: [-1, 4, 1n, 'a', null, false, true, {}, [], () => {}], }, @@ -202,13 +169,13 @@ for (const { key, valid, invalid } of cases) { for (const value of valid) { const options = {}; options[key] = value; - new quic.Endpoint(options); + new Endpoint(options); } for (const value of invalid) { const options = {}; options[key] = value; - throws(() => new quic.Endpoint(options), { + throws(() => new Endpoint(options), { code: 'ERR_INVALID_ARG_VALUE', }); } diff --git a/test/parallel/test-quic-internal-endpoint-stats-state.js b/test/parallel/test-quic-internal-endpoint-stats-state.js index 566dd675d73e26a..5012ce9c1255680 100644 --- a/test/parallel/test-quic-internal-endpoint-stats-state.js +++ b/test/parallel/test-quic-internal-endpoint-stats-state.js @@ -63,17 +63,17 @@ endpoint.markBusy(false); strictEqual(state.getUint8(IDX_STATE_ENDPOINT_BUSY), 0); const stats = new BigUint64Array(endpoint.stats); -strictEqual(stats[IDX_STATS_ENDPOINT_CREATED_AT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_DESTROYED_AT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_RECEIVED], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_BYTES_SENT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_RECEIVED], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_PACKETS_SENT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_SESSIONS], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_CLIENT_SESSIONS], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_RETRY_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT], 0n); -strictEqual(stats[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT], 0n); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_CREATED_AT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_DESTROYED_AT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_BYTES_RECEIVED], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_BYTES_SENT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_PACKETS_RECEIVED], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_PACKETS_SENT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_SERVER_SESSIONS], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_CLIENT_SESSIONS], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_SERVER_BUSY_COUNT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_RETRY_COUNT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_VERSION_NEGOTIATION_COUNT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_STATELESS_RESET_COUNT], 'bigint'); +strictEqual(typeof stats[IDX_STATS_ENDPOINT_IMMEDIATE_CLOSE_COUNT], 'bigint'); strictEqual(IDX_STATS_ENDPOINT_COUNT, 13);