/** * Copyright 2023 Kapeta Inc. * SPDX-License-Identifier: BUSL-1.1 */ import { normalizeKapetaUri } from '@kapeta/nodejs-utils'; import net from 'net'; const DEFAULT_SERVER_PORT = 35100; const DEFAULT_START_PORT = 40000; const DEFAULT_HOST = '127.0.0.1'; class ClusterService { private _port: number; private _currentPort: number; private _initialized: boolean; private _reservedPorts: number[]; private _host: string; constructor() { this._port = DEFAULT_SERVER_PORT; this._currentPort = DEFAULT_START_PORT; this._initialized = false; this._reservedPorts = []; this._host = DEFAULT_HOST; } reservePort(port: number | string) { const intPort = parseInt(port as string); if (this._reservedPorts.indexOf(intPort) > -1) { throw new Error('Port already reserved: ' + intPort); } this._reservedPorts.push(intPort); } async init() { if (this._initialized) { return; } this._initialized = true; await this._findClusterServicePort(); } async _findClusterServicePort() { for (; this._port <= 65535; this._port++) { try { await this.resolveWithAvailablePort(this._port); return; } catch (e) { // try again } } throw new Error('No available ports'); } /** * Gets next available port */ public async getNextAvailablePort(startPort: number = -1) { for (let nextPort = startPort > 0 ? startPort : this._currentPort; nextPort <= 65535; nextPort++) { if (this._reservedPorts.indexOf(nextPort) > -1) { continue; } try { // Try both IPv4 and IPv6 addresses await this.resolveWithAvailablePort(nextPort); await this.resolveWithAvailablePort(nextPort, '0.0.0.0'); // Save the state if we're looking for a system port for the cluster itself if (startPort <= 0) { this._currentPort = nextPort; } return nextPort; } catch (e) { // try again } } } throw new Error('No available ports'); } private async resolveWithAvailablePort(port: number, host: string = this._host) { return new Promise((resolve, reject) => { const server = net.createServer(); server.unref(); server.on('error', reject); server.listen({ port, host }, (...args) => { server.close(() => { resolve(port); }); }); }); } /** * The port of this local cluster service itself */ getClusterServicePort() { return this._port; } /* *Gets the host name ( 127.0.0.1 ) on which Express JS is listening */ getClusterServiceHost() { return this._host; } /** * Set the port to be used for this local service * @param port */ setClusterServicePort(port: number) { this._port = port; } setClusterServiceHost(host: string) { this._host = host; } /** * Gets that proxy path of a given request * * @param systemId * @param consumerInstanceId * @param consumerResourceName * @param portType * @return {string} */ getProxyPath(systemId: string, consumerInstanceId: string, consumerResourceName: string, portType: string) { systemId = normalizeKapetaUri(systemId); return `/proxy/${encodeURIComponent(systemId)}/${encodeURIComponent(consumerInstanceId)}/${encodeURIComponent( consumerResourceName )}/${encodeURIComponent(portType)}/`; } } export const clusterService = new ClusterService();