UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

141 lines (139 loc) • 4.94 kB
import { claimedPorts } from "../services/processes/registry.mjs"; import { Context, Data, Effect, Layer } from "effect"; import { createServer } from "node:net"; //#region src/infra/ports.ts const PROBE_TIMEOUT_MS = 250; const MAX_PORT_SCAN_STEPS = 1e3; const PARALLEL_PROBE_WINDOW = 8; const BLOCK_STEP = 100; var PortAllocationError = class extends Data.TaggedError("PortAllocationError") {}; var PortAllocator = class extends Context.Service()("PortAllocator") {}; const scheduleTimeout = (ms, fn) => setTimeout(fn, ms); function probePortBindable(port) { return Effect.callback((resume) => { const server = createServer(); server.once("listening", () => { server.close(() => { resume(Effect.succeed(true)); }); }); server.once("error", () => { server.removeAllListeners(); resume(Effect.succeed(false)); }); server.listen(port, "127.0.0.1"); const timer = scheduleTimeout(PROBE_TIMEOUT_MS, () => { server.removeAllListeners(); try { server.close(); } catch {} resume(Effect.succeed(false)); }); server.once("listening", () => clearTimeout(timer)); server.once("error", () => clearTimeout(timer)); }); } function pickAvailablePort(preferred, usedPorts, budget) { return Effect.gen(function* () { const within = (candidate) => !budget || candidate >= budget.min && candidate <= budget.max; let port = preferred; if (!within(port)) port = budget ? budget.min : port; const ceiling = budget ? budget.max + 1 : Number.MAX_SAFE_INTEGER; let steps = 0; const fail = () => Effect.fail(new PortAllocationError({ preferred, budget, cause: budget ? `No free port in budget [${budget.min}, ${budget.max}] starting from ${preferred}` : `No free port found starting from ${preferred} within ${MAX_PORT_SCAN_STEPS} steps` })); while (true) { if (port >= ceiling || steps > MAX_PORT_SCAN_STEPS) return yield* fail(); const candidates = []; for (let i = 0; i < PARALLEL_PROBE_WINDOW && port + i < ceiling; i++) { const candidate = port + i; if (!usedPorts.has(candidate)) candidates.push(candidate); } if (candidates.length === 0) { port += PARALLEL_PROBE_WINDOW; steps += PARALLEL_PROBE_WINDOW; continue; } const firstFree = (yield* Effect.forEach(candidates, (c) => probePortBindable(c).pipe(Effect.map((free) => ({ port: c, free }))), { concurrency: "unbounded" })).find((r) => r.free); if (firstFree) { usedPorts.add(firstFree.port); return firstFree.port; } port += PARALLEL_PROBE_WINDOW; steps += PARALLEL_PROBE_WINDOW; } }); } const PortAllocatorLive = Layer.sync(PortAllocator, () => { const usedPorts = claimedPorts(); return { pickAvailable: (preferred, budget) => pickAvailablePort(preferred, usedPorts, budget), acquireBlock: (request) => acquireBlockPort(request, usedPorts) }; }); function acquireBlockPort(request, usedPorts) { const step = request.step ?? BLOCK_STEP; const fail = (cause) => Effect.fail(new PortAllocationError({ preferred: request.base, cause })); return Effect.gen(function* () { let base = request.base; let steps = 0; let firstConflicts = []; while (steps <= MAX_PORT_SCAN_STEPS) { const shift = base - request.base; const candidates = request.entries.map((entry) => ({ ...entry, port: entry.pinned ? entry.preferred : entry.preferred + shift })); let pinnedHeldByListener; for (const candidate of candidates.filter((c) => c.pinned)) { if (!(yield* probePortBindable(candidate.port))) { pinnedHeldByListener = candidate; break; } usedPorts.delete(candidate.port); } if (pinnedHeldByListener) return yield* fail(`explicitly-requested port ${pinnedHeldByListener.port} (${pinnedHeldByListener.key}) is occupied by a listener`); const unpinned = candidates.filter((c) => !c.pinned); const claimConflicts = unpinned.filter((c) => usedPorts.has(c.port)).map((c) => ({ key: c.key, port: c.port, claimed: true })); if (shift === 0) firstConflicts = claimConflicts; if (claimConflicts.length === 0) { const busy = (yield* Effect.forEach(unpinned, (c) => probePortBindable(c.port).pipe(Effect.map((free) => ({ ...c, free }))), { concurrency: "unbounded" })).filter((r) => !r.free); if (shift === 0) firstConflicts = busy.map((b) => ({ key: b.key, port: b.port, claimed: false })); if (busy.length === 0) { for (const candidate of candidates) usedPorts.add(candidate.port); return { base, ports: Object.fromEntries(candidates.map((c) => [c.key, c.port])), conflicts: firstConflicts }; } } base += step; steps += 1; } return yield* fail(`no free port block within ${MAX_PORT_SCAN_STEPS} steps of base ${request.base} (step ${step})`); }); } //#endregion export { PortAllocationError, PortAllocator, PortAllocatorLive, probePortBindable }; //# sourceMappingURL=ports.mjs.map