{"_id":"@adhd/sox-listen-guard","_rev":"2-4e8c7736c94f149ea0b32129bf300557","name":"@adhd/sox-listen-guard","dist-tags":{"latest":"0.1.1"},"versions":{"0.1.0":{"name":"@adhd/sox-listen-guard","version":"0.1.0","keywords":["listen","socket","singleton","eaddrinuse","typescript"],"license":"MIT","_id":"@adhd/sox-listen-guard@0.1.0","maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"homepage":"https://github.com/PseudoSky/adhd","bugs":{"url":"https://github.com/PseudoSky/adhd/issues"},"dist":{"shasum":"4a2fda4ff4797d4307a8017b5c678edbd0ba1591","tarball":"https://registry.npmjs.org/@adhd/sox-listen-guard/-/sox-listen-guard-0.1.0.tgz","fileCount":11,"integrity":"sha512-3X31nTdfSGQl5T4AhXIecDlz8N/GTzD/bnCATV+5M5quA3tECiGBhXn7VOoAhZXA0GzCwxazkEKPLz8G5uDItQ==","signatures":[{"sig":"MEUCIQDAmjVs7ITtP/JOyt5kPpPT/1xWKaVOYCJUsBEb65KZlQIgVBVxOSu2CiRXMwU2TVSyWhFoB1lTvkmA5y75YXyded0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":26916},"main":"./dist/index.js","_from":"file:adhd-sox-listen-guard-0.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"_npmUser":{"name":"pseudosky","email":"skywinston.sk@gmail.com"},"_resolved":"/private/var/folders/yg/cfczgtx54bzfh74lx2_mv0z80000gp/T/42083d6cba767345751437cba0e23f36/adhd-sox-listen-guard-0.1.0.tgz","_integrity":"sha512-3X31nTdfSGQl5T4AhXIecDlz8N/GTzD/bnCATV+5M5quA3tECiGBhXn7VOoAhZXA0GzCwxazkEKPLz8G5uDItQ==","repository":{"url":"git+https://github.com/PseudoSky/adhd.git","type":"git"},"_npmVersion":"11.6.2","description":"Repo-wide listen() safety invariant — probe-before-bind + guarded listen + structured JSONL failure records (BL-619). Dependency-free leaf (node builtins only).","directories":{},"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/sox-listen-guard_0.1.0_1788558508046_0.3705020268144381","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"_id":"@adhd/sox-listen-guard@0.1.1","bugs":{"url":"https://github.com/PseudoSky/adhd/issues"},"dist":{"shasum":"bf620e51db9795a89842840886c7bc46791b3a1c","tarball":"https://registry.npmjs.org/@adhd/sox-listen-guard/-/sox-listen-guard-0.1.1.tgz","fileCount":12,"integrity":"sha512-Ucl1v8LxoCoZoaO+wvuF3VY5Z/B8FtPmzxYFDjrE+NJjoBjyo96U0st/VNdW3FkQEpWOrF5MvjTXLxwloAm5IQ==","signatures":[{"sig":"MEUCIAePsf5qDbyHHN/V4LBWcKvCi/N7CdJDWu8DX58dCeJwAiEA3aK40IG1kSR+W+nDQEeF1ngwHHh8Mc9NjeXqi28fXE4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBKzxxTDsYvrPB++PC8eDrKLSNWn59AJNWTIBRF3TVCrAiBxeF9ubCDl/bMtqZ8Coc3gVFMo9/raeiFJNuw3ufQVzw=="}],"unpackedSize":32617},"main":"./dist/index.js","name":"@adhd/sox-listen-guard","_from":"file:adhd-sox-listen-guard-0.1.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"}},"license":"MIT","version":"0.1.1","_npmUser":{"name":"pseudosky","email":"skywinston.sk@gmail.com"},"homepage":"https://github.com/PseudoSky/adhd","keywords":["listen","socket","singleton","eaddrinuse","typescript"],"_resolved":"/private/var/folders/yg/cfczgtx54bzfh74lx2_mv0z80000gp/T/c251ff6e6dd5d3213f9c59f848d36d75/adhd-sox-listen-guard-0.1.1.tgz","_integrity":"sha512-Ucl1v8LxoCoZoaO+wvuF3VY5Z/B8FtPmzxYFDjrE+NJjoBjyo96U0st/VNdW3FkQEpWOrF5MvjTXLxwloAm5IQ==","repository":{"url":"git+https://github.com/PseudoSky/adhd.git","type":"git"},"_npmVersion":"11.6.2","description":"Repo-wide listen() safety invariant — probe-before-bind + guarded listen + structured JSONL failure records (BL-619). Dependency-free leaf (node builtins only).","directories":{},"maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"_nodeVersion":"24.11.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sox-listen-guard_0.1.1_1788566543340_0.35360508449346995"}}},"time":{"created":"2026-09-04T21:48:27.886Z","modified":"2026-09-05T00:02:23.650Z","0.1.0":"2026-09-04T21:48:28.205Z","0.1.1":"2026-09-05T00:02:23.451Z"},"bugs":{"url":"https://github.com/PseudoSky/adhd/issues"},"license":"MIT","homepage":"https://github.com/PseudoSky/adhd","keywords":["listen","socket","singleton","eaddrinuse","typescript"],"repository":{"url":"git+https://github.com/PseudoSky/adhd.git","type":"git"},"description":"Repo-wide listen() safety invariant — probe-before-bind + guarded listen + structured JSONL failure records (BL-619). Dependency-free leaf (node builtins only).","maintainers":[{"name":"pseudosky","email":"skywinston.sk@gmail.com"}],"readme":"# @adhd/sox-listen-guard\n\nA safety wrapper around `net.Server#listen()`: probe the port before binding, attach the `'error'`\nlistener *before* calling `listen()` so a bind failure resolves instead of crashing the process, and\nappend a structured JSONL record of what happened. Dependency-free — Node builtins only (`net`,\n`fs`, `path`), nothing else in `node_modules` to audit.\n\nThe problem this solves: `server.listen(port, host, cb)` with no `'error'` listener attached first\nmeans a port collision throws an *unhandled* `'error'` event, which Node treats as fatal — the whole\nprocess dies with a raw crash dump instead of a clean, recoverable outcome.\n\n```bash\npnpm add @adhd/sox-listen-guard\n```\n\n## Quick start\n\n```typescript\nimport * as net from 'node:net';\nimport { listenGuarded } from '@adhd/sox-listen-guard';\n\nconst server = net.createServer((socket) => {\n  socket.end('hello\\n');\n});\n\nconst outcome = await listenGuarded(server, { port: 3099, host: '127.0.0.1' });\n\nif (outcome.ok) {\n  console.log('listening on 3099');\n} else if (outcome.disposition === 'already-running') {\n  // another instance is already serving this port — exit 0, not an error\n  console.log('already running, nothing to do:', outcome.failure.message);\n  process.exit(0);\n} else {\n  // a genuine bind fault (EACCES, ENOENT, ...) — exit 1\n  console.error('listen failed:', outcome.failure.message);\n  process.exit(1);\n}\n```\n\nNote what never happens here: no `try`/`catch`, no `server.on('error', ...)` of your own, and no\nunhandled-error crash if the port is already held — `listenGuarded` resolves a typed outcome in every\ncase.\n\n## API reference\n\n```typescript\nfunction listenGuarded(\n  server: net.Server,\n  target: ListenTarget,\n  opts?: ListenGuardOptions,\n): Promise<ListenOutcome>;\n\ninterface ListenTarget {\n  port?: number;       // TCP\n  host?: string;       // TCP, default '127.0.0.1'\n  socketPath?: string;  // Unix domain socket\n  fd?: number;           // an inherited/socket-activated file descriptor\n}\n\ninterface ListenGuardOptions {\n  recordFile?: string;                    // JSONL path to append a ListenFailureRecord to on failure\n  onDiagnostic?: (line: string) => void;  // stderr-style diagnostics sink\n  probeTimeoutMs?: number;                 // TCP probe timeout, default 250\n}\n\ntype ListenOutcome =\n  | { ok: true }\n  | { ok: false; disposition: ListenDisposition; failure: ListenFailureRecord };\n\ntype ListenDisposition = 'already-running' | 'other';\n```\n\nFor a TCP `target` (`port` set), `listenGuarded` first probes the port with a real connection\nattempt. If something is already accepting connections there, it resolves\n`{ ok: false, disposition: 'already-running' }` **without ever calling `listen()`** — the fast path.\nOtherwise it attaches the `'error'` listener, calls `listen()`, and resolves once either `'listening'`\nor `'error'` fires. A Unix-domain-socket `target` (`socketPath` set) skips the probe — the caller owns\nits own stale-socket logic — and relies solely on the pre-attached error guard.\n\n### Building blocks\n\n`listenGuarded` is built from four standalone functions, each exported so an already-guarded\n`listen()` call site can reuse just the piece it needs:\n\n```typescript\nfunction probeTcp(host: string, port: number, timeoutMs?: number): Promise<boolean>;\nfunction classifyListenError(err: unknown): ListenDisposition;\nfunction buildFailureRecord(err: unknown, disposition: ListenDisposition, target: ListenTarget): ListenFailureRecord;\nfunction emitListenFailure(rec: ListenFailureRecord, opts?: EmitFailureOptions): void;\n\ninterface EmitFailureOptions {\n  recordFile?: string;\n}\n```\n\n```typescript\nimport { probeTcp, classifyListenError, buildFailureRecord, emitListenFailure } from '@adhd/sox-listen-guard';\n\n// A call site that already has its own 'error' listener wired can still get\n// the same structured record and probe-before-bind behavior:\nif (await probeTcp('127.0.0.1', 3099)) {\n  const failure = buildFailureRecord(null, 'already-running', { port: 3099, host: '127.0.0.1' });\n  emitListenFailure(failure, { recordFile: './listen-failures.jsonl' });\n}\n\nserver.once('error', (err) => {\n  const disposition = classifyListenError(err); // EADDRINUSE -> 'already-running', else 'other'\n  const failure = buildFailureRecord(err, disposition, { port: 3099 });\n  emitListenFailure(failure, { recordFile: './listen-failures.jsonl' });\n});\n```\n\n### The failure record\n\n```typescript\ninterface ListenFailureRecord {\n  code?: string;       // errno code, e.g. 'EADDRINUSE', 'EACCES', 'ENOENT'\n  errno?: string;\n  syscall?: string;    // e.g. 'listen'\n  message: string;\n  host?: string;       // TCP\n  port?: number;       // TCP\n  socketPath?: string; // UDS\n  disposition: ListenDisposition;\n  pid: number;\n  ts: string;          // ISO-8601\n}\n```\n\n`emitListenFailure` appends one JSON line per failure to `opts.recordFile` (creating the parent\ndirectory as needed) — best-effort: a failed write to the record file is swallowed rather than\nthrown, since the record is diagnostics, not control flow.\n\n## Invariants\n\n- `classifyListenError` maps `EADDRINUSE` to `'already-running'`; every other errno maps to\n  `'other'`. That split is the intended exit-code convention: `'already-running'` means another\n  instance is already serving (safe to exit 0), `'other'` means a genuine fault (exit 1).\n- `listenGuarded` never throws for a bind failure — it always resolves a `ListenOutcome`. The\n  `'error'` listener is attached before `server.listen()` is ever called, so a bind failure cannot\n  surface as an unhandled `'error'` event.\n- `emitListenFailure` never throws, even if `recordFile`'s directory can't be created or the append\n  fails.\n","readmeFilename":"README.md"}