import * as NodeNet from "node:net"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Predicate from "effect/Predicate"; import * as Schema from "effect/Schema"; export class LoopbackPortListenError extends Schema.TaggedErrorClass()( "LoopbackPortListenError", { host: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to listen for an ephemeral loopback port on ${this.host}.`; } } export class LoopbackPortAddressUnavailableError extends Schema.TaggedErrorClass()( "LoopbackPortAddressUnavailableError", { host: Schema.String, address: Schema.NullOr(Schema.String), family: Schema.NullOr(Schema.String), port: Schema.NullOr(Schema.Number), cause: Schema.optional(Schema.Defect()), }, ) { override get message(): string { return `Failed to read a usable ephemeral loopback port for ${this.host} (address ${this.address ?? "unavailable"}, family ${this.family ?? "unavailable"}, port ${this.port ?? "unavailable"}).`; } } export class LoopbackPortReleaseError extends Schema.TaggedErrorClass()( "LoopbackPortReleaseError", { host: Schema.String, port: Schema.Number, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to release ephemeral loopback port ${this.port} on ${this.host}.`; } } export const NetError = Schema.Union([ LoopbackPortListenError, LoopbackPortAddressUnavailableError, LoopbackPortReleaseError, ]); export type NetError = typeof NetError.Type; const isErrnoExceptionWithCode = ( cause: unknown, ): cause is { readonly code: string; } => Predicate.isObject(cause) && Predicate.hasProperty(cause, "code") && Predicate.isString(cause.code); const isUsablePort = (port: number | null): port is number => port !== null && Number.isInteger(port) && port > 0 && port <= 65_535; const closeServer = (server: NodeNet.Server) => { try { server.close(); } catch { // Ignore close failures during cleanup. } }; /** * NetService - Service tag for startup networking helpers. */ export class NetService extends Context.Service< NetService, { /** * Returns true when a TCP server can bind to {host, port}. */ readonly canListenOnHost: (port: number, host: string) => Effect.Effect; /** * Checks loopback availability on both IPv4 and IPv6 localhost addresses. */ readonly isPortAvailableOnLoopback: (port: number) => Effect.Effect; /** * Reserve an ephemeral loopback port and release it immediately. */ readonly reserveLoopbackPort: (host?: string) => Effect.Effect; /** * Resolve an available listening port, preferring the provided port first. */ readonly findAvailablePort: (preferred: number) => Effect.Effect; } >()("@t3tools/shared/Net/NetService") {} export const make = ( options: { readonly createServer?: () => NodeNet.Server; } = {}, ) => { const createServer = options.createServer ?? NodeNet.createServer; /** * Returns true when a TCP server can bind to {host, port}. * `EADDRNOTAVAIL` is treated as available so IPv6-absent hosts don't fail * loopback availability checks. */ const canListenOnHost = (port: number, host: string): Effect.Effect => Effect.callback((resume) => { const server = createServer(); let settled = false; const settle = (value: boolean) => { if (settled) return; settled = true; resume(Effect.succeed(value)); }; server.unref(); server.once("error", (cause) => { if (isErrnoExceptionWithCode(cause) && cause.code === "EADDRNOTAVAIL") { settle(true); return; } settle(false); }); server.once("listening", () => { server.close(() => { settle(true); }); }); server.listen({ host, port }); return Effect.sync(() => { closeServer(server); }); }); const hasListenerOnHost = (port: number, host: string): Effect.Effect => Effect.callback((resume) => { const socket = NodeNet.createConnection({ host, port }); let settled = false; const settle = (value: boolean) => { if (settled) return; settled = true; socket.destroy(); resume(Effect.succeed(value)); }; socket.unref(); socket.setTimeout(250); socket.once("connect", () => { settle(true); }); socket.once("error", () => { settle(false); }); socket.once("timeout", () => { settle(false); }); return Effect.sync(() => { socket.destroy(); }); }); const isPortAvailableOnLoopback = (port: number): Effect.Effect => Effect.gen(function* () { const hasListener = yield* Effect.zipWith( hasListenerOnHost(port, "127.0.0.1"), hasListenerOnHost(port, "::1"), (ipv4, ipv6) => ipv4 || ipv6, ); if (hasListener) { return false; } return yield* Effect.zipWith( canListenOnHost(port, "127.0.0.1"), canListenOnHost(port, "::1"), (ipv4, ipv6) => ipv4 && ipv6, ); }); /** * Reserve an ephemeral loopback port and release it immediately. * Returns the reserved port number. */ const reserveLoopbackPort = (host = "127.0.0.1"): Effect.Effect => Effect.callback((resume) => { const probe = createServer(); let settled = false; let addressDetails: | { readonly address: string | null; readonly family: string | null; readonly port: number | null; } | undefined; const settle = (effect: Effect.Effect) => { if (settled) return; settled = true; resume(effect); }; probe.once("error", (cause) => { settle( Effect.fail( addressDetails === undefined ? new LoopbackPortListenError({ host, cause }) : isUsablePort(addressDetails.port) ? new LoopbackPortReleaseError({ host, port: addressDetails.port, cause }) : new LoopbackPortAddressUnavailableError({ host, ...addressDetails, cause, }), ), ); }); try { probe.listen(0, host, () => { const address = probe.address(); const resolvedAddressDetails = typeof address === "object" && address !== null ? { address: address.address, family: address.family, port: typeof address.port === "number" && Number.isFinite(address.port) ? address.port : null, } : { address, family: null, port: null, }; addressDetails = resolvedAddressDetails; probe.close((cause) => { const port = resolvedAddressDetails.port; if (!isUsablePort(port)) { settle( Effect.fail( new LoopbackPortAddressUnavailableError({ host, ...resolvedAddressDetails, ...(cause === undefined ? {} : { cause }), }), ), ); return; } if (cause) { settle( Effect.fail( new LoopbackPortReleaseError({ host, port, cause, }), ), ); return; } settle(Effect.succeed(port)); }); }); } catch (cause) { settle(Effect.fail(new LoopbackPortListenError({ host, cause }))); } return Effect.sync(() => { closeServer(probe); }); }); return NetService.of({ canListenOnHost, isPortAvailableOnLoopback, reserveLoopbackPort, findAvailablePort: (preferred) => Effect.gen(function* () { if (preferred > 0 && (yield* isPortAvailableOnLoopback(preferred))) { return preferred; } return yield* reserveLoopbackPort(); }), }); }; export const layer = Layer.sync(NetService, make);