@oxog/port-finder
Version:
Zero-dependency port finder for Node.js applications with plugin support
145 lines (144 loc) • 4.81 kB
JavaScript
import { createServer } from 'net';
import { PortFinderError } from './types';
const DEFAULT_HOST = '0.0.0.0';
const MIN_PORT = 1;
const MAX_PORT = 65535;
/**
* Validates that a port number is within the valid range (1-65535)
* @param port - The port number to validate
* @throws {PortFinderError} If the port is invalid
*/
export function validatePort(port) {
if (!Number.isInteger(port) || port < MIN_PORT || port > MAX_PORT) {
throw new PortFinderError(`Port must be an integer between ${MIN_PORT} and ${MAX_PORT}`, 'INVALID_PORT', { port });
}
}
/**
* Validates that a port range is valid
* @param start - The starting port number
* @param end - The ending port number
* @throws {PortFinderError} If the range is invalid
*/
export function validatePortRange(start, end) {
validatePort(start);
validatePort(end);
if (start > end) {
throw new PortFinderError('Start port must be less than or equal to end port', 'INVALID_RANGE', { start, end });
}
}
/**
* Checks if a port is available by attempting to create a server
* @param port - The port to check
* @param host - The host to bind to
* @returns Promise resolving to true if available, false otherwise
*/
export async function checkPort(port, host = DEFAULT_HOST) {
return new Promise((resolve) => {
const server = createServer();
const cleanup = () => {
server.removeAllListeners();
server.close();
};
server.once('error', (err) => {
cleanup();
if (err.code === 'EADDRINUSE' || err.code === 'EACCES') {
resolve(false);
}
else {
resolve(false);
}
});
server.once('listening', () => {
cleanup();
resolve(true);
});
try {
server.listen(port, host);
}
catch (err) {
cleanup();
resolve(false);
}
});
}
/**
* Scans ports sequentially to find available ones
* @param start - Starting port number
* @param end - Ending port number
* @param host - Host to bind to
* @param exclude - Set of ports to exclude
* @param validator - Optional validator function
* @param count - Number of ports to find
* @param consecutive - Whether to find consecutive ports
* @returns Promise resolving to array of available ports
*/
export async function scanPorts(start, end, host = DEFAULT_HOST, exclude = new Set(), validator, count = 1, consecutive = false) {
const found = [];
let consecutiveStart = -1;
let consecutiveCount = 0;
for (let port = start; port <= end && found.length < count; port++) {
if (exclude.has(port)) {
consecutiveCount = 0;
continue;
}
if (validator && !validator(port)) {
consecutiveCount = 0;
continue;
}
const available = await checkPort(port, host);
if (available) {
if (consecutive) {
if (consecutiveCount === 0) {
consecutiveStart = port;
}
consecutiveCount++;
if (consecutiveCount === count) {
for (let i = 0; i < count; i++) {
found.push(consecutiveStart + i);
}
break;
}
}
else {
found.push(port);
}
}
else {
consecutiveCount = 0;
}
}
return found;
}
/**
* Scans ports in parallel for better performance
* @param start - Starting port number
* @param end - Ending port number
* @param host - Host to bind to
* @param exclude - Set of ports to exclude
* @param validator - Optional validator function
* @param count - Number of ports to find
* @param maxConcurrency - Maximum concurrent port checks
* @returns Promise resolving to array of available ports
*/
export async function scanPortsParallel(start, end, host = DEFAULT_HOST, exclude = new Set(), validator, count = 1, maxConcurrency = 100) {
const found = [];
const ports = [];
for (let port = start; port <= end; port++) {
if (!exclude.has(port) && (!validator || validator(port))) {
ports.push(port);
}
}
for (let i = 0; i < ports.length && found.length < count; i += maxConcurrency) {
const batch = ports.slice(i, i + maxConcurrency);
const results = await Promise.all(batch.map(async (port) => ({
port,
available: await checkPort(port, host)
})));
for (const { port, available } of results) {
if (available && found.length < count) {
found.push(port);
}
}
}
return found;
}