diff --git a/index.d.ts b/index.d.ts index 7ebc6fe..9f327b9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,6 +6,13 @@ export interface Options extends Omit { */ readonly port?: number | Iterable; + /** + Ports that should not be returned. + + You could, for example, pass it the return value of the `portNumbers()` function. + */ + readonly exclude?: Iterable; + /** The host on which port resolution should be performed. Can be either an IPv4 or IPv6 address. diff --git a/index.js b/index.js index 06c8e25..9ee3cd8 100644 --- a/index.js +++ b/index.js @@ -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; @@ -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) { @@ -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) { @@ -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) { diff --git a/index.test-d.ts b/index.test-d.ts index 03e8502..d440ae2 100644 --- a/index.test-d.ts +++ b/index.test-d.ts @@ -3,6 +3,8 @@ import getPort, {portNumbers} from './index.js'; expectType>(getPort()); expectType>(getPort({port: 3000})); +expectType>(getPort({exclude: [3000]})); +expectType>(getPort({exclude: [3000, 3001]})); expectType>(getPort({port: [3000, 3001, 3002]})); expectType>(getPort({host: 'https://localhost'})); expectType>(getPort({ipv6Only: true})); diff --git a/readme.md b/readme.md index fb78e04..b6cd616 100644 --- a/readme.md +++ b/readme.md @@ -60,6 +60,14 @@ Type: `number | Iterable` A preferred port or an iterable of preferred ports to use. +##### exclude + +Type: `Iterable` + +Ports that should not be returned. + +You could, for example, pass it the return value of the `portNumbers()` function. + ##### host Type: `string` diff --git a/test.js b/test.js index ed94b63..6226f73 100644 --- a/test.js +++ b/test.js @@ -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`.