Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,21 @@ with either a `QuicEndpoint` or `EndpointOptions` as the argument.
At most, any single `QuicEndpoint` can only be configured to listen as
a server once.

## `quic.listEndpoints([options])`

<!-- YAML
added: REPLACEME
-->

* `options` {object}
* `active` {boolean} If `true` (the default), only returns endpoints that are
active (not destroyed, not closing, and not busy). If `false` returns all
endpoints.
* Returns: {quic.QuicEndpoint\[]}

Returns the list of all `QuicEndpoint` instances. By default, only active
endpoints are returned.

## `quic.constants`

<!-- YAML
Expand Down
20 changes: 20 additions & 0 deletions lib/internal/quic/quic.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
/* c8 ignore start */

const {
ArrayFrom,
ArrayIsArray,
ArrayPrototypePush,
BigInt,
Expand Down Expand Up @@ -4600,6 +4601,24 @@ function findSuitableEndpoint(targetAddress) {
return undefined;
}

/**
* Returns a list of all active endpoints.
* @param {object} [options]
* @param {boolean} [options.active] When true, only return endpoints that are not destroyed, closing, or busy.
* @returns {QuicEndpoint[]}
*/
function listEndpoints(options = kEmptyObject) {
validateObject(options, 'options');
const { active = true } = options;
validateBoolean(active, 'options.active');
if (!active) {
return ArrayFrom(endpointRegistry);
}
return ArrayFrom(endpointRegistry).filter((endpoint) => {
return !endpoint.destroyed && !endpoint.closing && !endpoint.busy;
});
}

/**
* @param {EndpointOptions|QuicEndpoint|undefined} endpoint
* @param {boolean} reuseEndpoint
Expand Down Expand Up @@ -5185,6 +5204,7 @@ module.exports = {
getQuicStreamState,
getQuicSessionState,
getQuicEndpointState,
listEndpoints,
};

/* c8 ignore stop */
58 changes: 21 additions & 37 deletions lib/quic.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
'use strict';

const {
ObjectCreate,
ObjectSeal,
} = primordials;

const {
emitExperimentalWarning,
} = require('internal/util');
Expand All @@ -24,37 +19,26 @@ const {
DEFAULT_GROUPS,
} = require('internal/quic/quic');

function getEnumerableConstant(value) {
return {
__proto__: null,
value,
enumerable: true,
configurable: false,
writable: false,
};
}

const cc = ObjectSeal(ObjectCreate(null, {
__proto__: null,
RENO: getEnumerableConstant(CC_ALGO_RENO),
CUBIC: getEnumerableConstant(CC_ALGO_CUBIC),
BBR: getEnumerableConstant(CC_ALGO_BBR),
}));
const cc = {
get RENO() { return CC_ALGO_RENO; },
get CUBIC() { return CC_ALGO_CUBIC; },
get BBR() { return CC_ALGO_BBR; },
};

const constants = ObjectSeal(ObjectCreate(null, {
__proto__: null,
cc: getEnumerableConstant(cc),
DEFAULT_CIPHERS: getEnumerableConstant(DEFAULT_CIPHERS),
DEFAULT_GROUPS: getEnumerableConstant(DEFAULT_GROUPS),
}));
const constants = {
get cc() { return cc; },
get DEFAULT_CIPHERS() { return DEFAULT_CIPHERS; },
get DEFAULT_GROUPS() { return DEFAULT_GROUPS; },
};

module.exports = ObjectSeal(ObjectCreate(null, {
__proto__: null,
connect: getEnumerableConstant(connect),
listen: getEnumerableConstant(listen),
QuicEndpoint: getEnumerableConstant(QuicEndpoint),
QuicError: getEnumerableConstant(QuicError),
QuicSession: getEnumerableConstant(QuicSession),
QuicStream: getEnumerableConstant(QuicStream),
constants: getEnumerableConstant(constants),
}));
module.exports = {
connect,
listen,
QuicEndpoint,
QuicError,
QuicSession,
QuicStream,
get constants() {
return constants;
},
};
6 changes: 0 additions & 6 deletions test/parallel/test-quic-exports.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,5 @@ strictEqual(quic.constants.DEFAULT_GROUPS, 'X25519:P-256:P-384:P-521');
throws(() => { quic.constants.cc.RENO = 'foo'; }, TypeError);
strictEqual(quic.constants.cc.RENO, 'reno');

throws(() => { quic.constants.cc.NEW_CONSTANT = 'bar'; }, TypeError);
strictEqual(quic.constants.cc.NEW_CONSTANT, undefined);

throws(() => { quic.constants.DEFAULT_CIPHERS = 123; }, TypeError);
strictEqual(typeof quic.constants.DEFAULT_CIPHERS, 'string');

throws(() => { quic.constants.NEW_CONSTANT = 456; }, TypeError);
strictEqual(quic.constants.NEW_CONSTANT, undefined);
105 changes: 105 additions & 0 deletions test/parallel/test-quic-list-endpoints.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Flags: --experimental-quic --no-warnings

// Test: listEndpoints returns live endpoints filtered by active state.
// - Returns an empty array when no endpoints exist.
// - Includes all created endpoints.
// - active=true (default) filters out destroyed, closing, and busy endpoints.
// - active=false returns all registered endpoints regardless of state.
// - Validates options argument types.

import { hasQuic, skip } from '../common/index.mjs';
import assert from 'node:assert';

const { deepStrictEqual, strictEqual, throws, ok } = assert;

if (!hasQuic) {
skip('QUIC is not enabled');
}

const { listEndpoints, QuicEndpoint } = await import('node:quic');

// listEndpoints is exported as a function.
strictEqual(typeof listEndpoints, 'function');

// Empty when no endpoints have been created.
deepStrictEqual(listEndpoints(), []);
deepStrictEqual(listEndpoints({ active: false }), []);

// Created endpoints appear in the list.
{
const ep1 = new QuicEndpoint();
const ep2 = new QuicEndpoint();

const list = listEndpoints();
strictEqual(list.length, 2);
ok(list.includes(ep1));
ok(list.includes(ep2));

// active=false also returns them.
const all = listEndpoints({ active: false });
strictEqual(all.length, 2);
ok(all.includes(ep1));
ok(all.includes(ep2));

// Returns plain QuicEndpoint instances, not wrapped in arrays.
ok(list[0] instanceof QuicEndpoint);
ok(list[1] instanceof QuicEndpoint);

await ep1.close();
await ep2.close();
}

// Destroyed endpoints are excluded by active filter.
{
const ep = new QuicEndpoint();
strictEqual(listEndpoints().length, 1);

await ep.close();
await ep.closed;
strictEqual(ep.destroyed, true);

// Default (active=true) excludes destroyed endpoint.
strictEqual(listEndpoints().length, 0);

// active=false still includes it... but destroyed endpoints are removed
// from the registry entirely in kFinishClose, so it won't appear at all.
strictEqual(listEndpoints({ active: false }).length, 0);
}

// Busy endpoints are excluded by the active filter.
{
const ep = new QuicEndpoint();
strictEqual(listEndpoints().length, 1);

ep.busy = true;
strictEqual(ep.busy, true);

// active=true excludes busy endpoint.
strictEqual(listEndpoints().length, 0);

// active=false includes it.
const all = listEndpoints({ active: false });
strictEqual(all.length, 1);
ok(all.includes(ep));

ep.busy = false;

// After un-busying, it reappears in the active list.
strictEqual(listEndpoints().length, 1);

await ep.close();
}

// Options validation.
throws(() => listEndpoints('bad'), {
code: 'ERR_INVALID_ARG_TYPE',
});
throws(() => listEndpoints(null), {
code: 'ERR_INVALID_ARG_TYPE',
});
throws(() => listEndpoints({ active: 'yes' }), {
code: 'ERR_INVALID_ARG_TYPE',
});
throws(() => listEndpoints({ active: 1 }), {
code: 'ERR_INVALID_ARG_TYPE',
});
Loading