import * as NodeNet from "node:net"; import * as Data from "effect/Data"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Context from "effect/Context"; import * as Predicate from "effect/Predicate"; export class NetError extends Data.TaggedError("NetError")<{ readonly message: string; readonly cause?: unknown; }> {} const isErrnoExceptionWithCode = ( cause: unknown, ): cause is { readonly code: string; } => Predicate.isObject(cause) && Predicate.hasProperty(cause, "code") && Predicate.isString(cause.code); const closeServer = (server: NodeNet.Server) => { try { server.close(); } catch { // Ignore close failures during cleanup. } }; export interface NetServiceShape { /** * 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; /** * Returns true when something accepts TCP connections on {host, port}. * Unlike the bind-side checks this works for privileged ports (<1024). */ readonly hasListenerOnHost: (port: number, host: string) => 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; } /** * NetService - Service tag for startup networking helpers. */ export class NetService extends Context.Service()( "@t3tools/shared/Net/NetService", ) {} /** @public Service construction is part of the canonical Effect module API. */ export const make = () => { /** * 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 = NodeNet.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 = NodeNet.createServer(); let settled = false; const settle = (effect: Effect.Effect) => { if (settled) return; settled = true; resume(effect); }; probe.once("error", (cause) => { settle(Effect.fail(new NetError({ message: "Failed to reserve loopback port", cause }))); }); probe.listen(0, host, () => { const address = probe.address(); const port = typeof address === "object" && address !== null ? address.port : 0; probe.close(() => { if (port > 0) { settle(Effect.succeed(port)); return; } settle(Effect.fail(new NetError({ message: "Failed to reserve loopback port" }))); }); }); return Effect.sync(() => { closeServer(probe); }); }); return { canListenOnHost, isPortAvailableOnLoopback, hasListenerOnHost, reserveLoopbackPort, findAvailablePort: (preferred) => Effect.gen(function* () { if (preferred > 0 && (yield* isPortAvailableOnLoopback(preferred))) { return preferred; } return yield* reserveLoopbackPort(); }), } satisfies NetServiceShape; }; export const layer = Layer.sync(NetService, make);