Skip to content

Commit

Permalink
Add exclude option (#53)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
mastrzyz and sindresorhus committed Feb 16, 2022
1 parent 3165af1 commit d03c07b
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 5 deletions.
7 changes: 7 additions & 0 deletions index.d.ts
Expand Up @@ -6,6 +6,13 @@ export interface Options extends Omit<ListenOptions, 'port'> {
*/
readonly port?: number | Iterable<number>;

/**
Ports that should not be returned.
You could, for example, pass it the return value of the `portNumbers()` function.
*/
readonly exclude?: Iterable<number>;

/**
The host on which port resolution should be performed. Can be either an IPv4 or IPv6 address.
Expand Down
40 changes: 35 additions & 5 deletions index.js
Expand Up @@ -17,6 +17,9 @@ const lockedPorts = {
// and a new young set for locked ports are created.
const releaseOldLockedPortsIntervalMs = 1000 * 15;

const minPort = 1024;
const maxPort = 65_535;

// Lazily create interval on first use
let interval;

Expand Down Expand Up @@ -78,9 +81,32 @@ const portCheckSequence = function * (ports) {

export default async function getPorts(options) {
let ports;
let exclude = new Set();

if (options) {
ports = typeof options.port === 'number' ? [options.port] : options.port;
if (options.port) {
ports = typeof options.port === 'number' ? [options.port] : options.port;
}

if (options.exclude) {
const excludeIterable = options.exclude;

if (typeof excludeIterable[Symbol.iterator] !== 'function') {
throw new TypeError('The `exclude` option must be an iterable.');
}

for (const element of excludeIterable) {
if (typeof element !== 'number') {
throw new TypeError('Each item in the `exclude` option must be a number corresponding to the port you want excluded.');
}

if (!Number.isSafeInteger(element)) {
throw new TypeError(`Number ${element} in the exclude option is not a safe integer and can't be used`);
}
}

exclude = new Set(excludeIterable);
}
}

if (interval === undefined) {
Expand All @@ -99,6 +125,10 @@ export default async function getPorts(options) {

for (const port of portCheckSequence(ports)) {
try {
if (exclude.has(port)) {
continue;
}

let availablePort = await getAvailablePort({...options, port}, hosts); // eslint-disable-line no-await-in-loop
while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) {
if (port !== 0) {
Expand Down Expand Up @@ -126,12 +156,12 @@ export function portNumbers(from, to) {
throw new TypeError('`from` and `to` must be integer numbers');
}

if (from < 1024 || from > 65_535) {
throw new RangeError('`from` must be between 1024 and 65535');
if (from < minPort || from > maxPort) {
throw new RangeError(`'from' must be between ${minPort} and ${maxPort}`);
}

if (to < 1024 || to > 65_536) {
throw new RangeError('`to` must be between 1024 and 65536');
if (to < minPort || to > maxPort + 1) {
throw new RangeError(`'to' must be between ${minPort} and ${maxPort + 1}`);
}

if (to < from) {
Expand Down
2 changes: 2 additions & 0 deletions index.test-d.ts
Expand Up @@ -3,6 +3,8 @@ import getPort, {portNumbers} from './index.js';

expectType<Promise<number>>(getPort());
expectType<Promise<number>>(getPort({port: 3000}));
expectType<Promise<number>>(getPort({exclude: [3000]}));
expectType<Promise<number>>(getPort({exclude: [3000, 3001]}));
expectType<Promise<number>>(getPort({port: [3000, 3001, 3002]}));
expectType<Promise<number>>(getPort({host: 'https://localhost'}));
expectType<Promise<number>>(getPort({ipv6Only: true}));
Expand Down
8 changes: 8 additions & 0 deletions readme.md
Expand Up @@ -60,6 +60,14 @@ Type: `number | Iterable<number>`

A preferred port or an iterable of preferred ports to use.

##### exclude

Type: `Iterable<number>`

Ports that should not be returned.

You could, for example, pass it the return value of the `portNumbers()` function.

##### host

Type: `string`
Expand Down
23 changes: 23 additions & 0 deletions test.js
Expand Up @@ -139,6 +139,29 @@ test('makeRange produces valid ranges', t => {
t.deepEqual([...portNumbers(1024, 1027)], [1024, 1025, 1026, 1027]);
});

test('exclude produces ranges that exclude provided exclude list', async t => {
const exclude = [1024, 1026];
const foundPorts = await getPort({exclude, port: portNumbers(1024, 1026)});

// We should not find any of the exclusions in `foundPorts`.
t.is(foundPorts, 1025);
});

test('exclude throws error if not provided with a valid iterator', async t => {
const exclude = 42;
await t.throwsAsync(getPort({exclude}));
});

test('exclude throws error if provided iterator contains items which are non number', async t => {
const exclude = ['foo'];
await t.throwsAsync(getPort({exclude}));
});

test('exclude throws error if provided iterator contains items which are unsafe numbers', async t => {
const exclude = [Number.NaN];
await t.throwsAsync(getPort({exclude}));
});

// TODO: Re-enable this test when ESM supports import hooks.
// test('ports are locked for up to 30 seconds', async t => {
// // Speed up the test by overriding `setInterval`.
Expand Down

0 comments on commit d03c07b

Please sign in to comment.