UNPKG

2.62 kBJavaScriptView Raw
1'use strict';
2const net = require('net');
3
4class Locked extends Error {
5 constructor(port) {
6 super(`${port} is locked`);
7 }
8}
9
10const lockedPorts = {
11 old: new Set(),
12 young: new Set()
13};
14
15// On this interval, the old locked ports are discarded,
16// the young locked ports are moved to old locked ports,
17// and a new young set for locked ports are created.
18const releaseOldLockedPortsIntervalMs = 1000 * 15;
19
20// Lazily create interval on first use
21let interval;
22
23const getAvailablePort = options => new Promise((resolve, reject) => {
24 const server = net.createServer();
25 server.unref();
26 server.on('error', reject);
27 server.listen(options, () => {
28 const {port} = server.address();
29 server.close(() => {
30 resolve(port);
31 });
32 });
33});
34
35const portCheckSequence = function * (ports) {
36 if (ports) {
37 yield * ports;
38 }
39
40 yield 0; // Fall back to 0 if anything else failed
41};
42
43module.exports = async options => {
44 let ports;
45
46 if (options) {
47 ports = typeof options.port === 'number' ? [options.port] : options.port;
48 }
49
50 if (interval === undefined) {
51 interval = setInterval(() => {
52 lockedPorts.old = lockedPorts.young;
53 lockedPorts.young = new Set();
54 }, releaseOldLockedPortsIntervalMs);
55
56 // Does not exist in some environments (Electron, Jest jsdom env, browser, etc).
57 if (interval.unref) {
58 interval.unref();
59 }
60 }
61
62 for (const port of portCheckSequence(ports)) {
63 try {
64 let availablePort = await getAvailablePort({...options, port}); // eslint-disable-line no-await-in-loop
65 while (lockedPorts.old.has(availablePort) || lockedPorts.young.has(availablePort)) {
66 if (port !== 0) {
67 throw new Locked(port);
68 }
69
70 availablePort = await getAvailablePort({...options, port}); // eslint-disable-line no-await-in-loop
71 }
72
73 lockedPorts.young.add(availablePort);
74 return availablePort;
75 } catch (error) {
76 if (!['EADDRINUSE', 'EACCES'].includes(error.code) && !(error instanceof Locked)) {
77 throw error;
78 }
79 }
80 }
81
82 throw new Error('No available ports found');
83};
84
85module.exports.makeRange = (from, to) => {
86 if (!Number.isInteger(from) || !Number.isInteger(to)) {
87 throw new TypeError('`from` and `to` must be integer numbers');
88 }
89
90 if (from < 1024 || from > 65535) {
91 throw new RangeError('`from` must be between 1024 and 65535');
92 }
93
94 if (to < 1024 || to > 65536) {
95 throw new RangeError('`to` must be between 1024 and 65536');
96 }
97
98 if (to < from) {
99 throw new RangeError('`to` must be greater than or equal to `from`');
100 }
101
102 const generator = function * (from, to) {
103 for (let port = from; port <= to; port++) {
104 yield port;
105 }
106 };
107
108 return generator(from, to);
109};