@wrongstack/persistence 0.296.3 → 0.297.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -0
- package/dist/index.js.map +3 -3
- package/dist/socket-path.d.ts +36 -0
- package/dist/socket-path.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -210,11 +210,35 @@ var defaultPrimitives = createPersistencePrimitives();
|
|
|
210
210
|
var atomicWrite = defaultPrimitives.atomicWrite;
|
|
211
211
|
var ensureDir = defaultPrimitives.ensureDir;
|
|
212
212
|
var withFileLock = defaultPrimitives.withFileLock;
|
|
213
|
+
|
|
214
|
+
// src/socket-path.ts
|
|
215
|
+
function unixSocketPathLimit(platform = process.platform) {
|
|
216
|
+
if (platform === "darwin" || platform === "freebsd" || platform === "openbsd") return 103;
|
|
217
|
+
return 107;
|
|
218
|
+
}
|
|
219
|
+
function checkUnixSocketPath(socketPath, platform = process.platform) {
|
|
220
|
+
const byteLength = Buffer.byteLength(socketPath, "utf8");
|
|
221
|
+
if (platform === "win32") {
|
|
222
|
+
return { ok: true, byteLength, maxBytes: Number.MAX_SAFE_INTEGER };
|
|
223
|
+
}
|
|
224
|
+
const maxBytes = unixSocketPathLimit(platform);
|
|
225
|
+
return { ok: byteLength <= maxBytes, byteLength, maxBytes };
|
|
226
|
+
}
|
|
227
|
+
function assertUnixSocketPathWithinLimit(socketPath, service, platform = process.platform) {
|
|
228
|
+
const check = checkUnixSocketPath(socketPath, platform);
|
|
229
|
+
if (check.ok) return;
|
|
230
|
+
throw new Error(
|
|
231
|
+
`${service} IPC socket path is ${check.byteLength} bytes, over the ${platform} sun_path limit of ${check.maxBytes} usable bytes: ${socketPath}. Set a shorter TMPDIR to relocate WrongStack IPC sockets.`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
213
234
|
export {
|
|
214
235
|
PersistenceFsError,
|
|
236
|
+
assertUnixSocketPathWithinLimit,
|
|
215
237
|
atomicWrite,
|
|
238
|
+
checkUnixSocketPath,
|
|
216
239
|
createPersistencePrimitives,
|
|
217
240
|
ensureDir,
|
|
241
|
+
unixSocketPathLimit,
|
|
218
242
|
withFileLock
|
|
219
243
|
};
|
|
220
244
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/atomic-write.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomBytes } from 'node:crypto';\nimport type { FSWatcher } from 'node:fs';\nimport { watch as watchDir } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\n\nexport interface AtomicWriteOptions {\n mode?: number | undefined;\n encoding?: BufferEncoding | undefined;\n}\n\nexport interface FileLockOptions {\n timeoutMs?: number | undefined;\n staleMs?: number | undefined;\n}\n\nexport interface FileLockTimeoutDetails {\n targetPath: string;\n timeoutMs: number;\n}\n\nexport interface PersistencePrimitiveOptions {\n createLockTimeoutError?: ((details: FileLockTimeoutDetails) => Error) | undefined;\n}\n\nexport interface PersistencePrimitives {\n atomicWrite(\n targetPath: string,\n content: string | Uint8Array,\n opts?: AtomicWriteOptions,\n ): Promise<void>;\n atomicReplaceWithWriter<T>(\n targetPath: string,\n write: (handle: fs.FileHandle) => Promise<T>,\n opts?: AtomicWriteOptions,\n ): Promise<T>;\n ensureDir(dir: string): Promise<void>;\n withFileLock<T>(targetPath: string, fn: () => Promise<T>, opts?: FileLockOptions): Promise<T>;\n}\n\n/** A dependency-free structured error for persistence boundary failures. */\nexport class PersistenceFsError extends Error {\n override name = 'FsError';\n readonly code: string;\n readonly path?: string | undefined;\n readonly context?: Record<string, unknown> | undefined;\n\n constructor(opts: {\n message: string;\n code: string;\n path?: string | undefined;\n context?: Record<string, unknown> | undefined;\n cause?: unknown | undefined;\n }) {\n super(opts.message, { cause: opts.cause });\n this.code = opts.code;\n this.path = opts.path;\n this.context = opts.context;\n }\n}\n\n/**\n * Create an isolated primitive set. Hosts may inject their own structured\n * timeout error without making this low-level package depend on that host.\n */\nexport function createPersistencePrimitives(\n options: PersistencePrimitiveOptions = {},\n): PersistencePrimitives {\n const createLockTimeoutError =\n options.createLockTimeoutError ??\n (({ targetPath, timeoutMs }: FileLockTimeoutDetails) =>\n new PersistenceFsError({\n message: `Timed out waiting for file lock: ${targetPath}`,\n code: 'FS_ATOMIC_WRITE_FAILED',\n path: targetPath,\n context: { timeoutMs },\n }));\n\n /**\n * Shared tail of every atomic replace: fsync the temp file, carry the\n * target's permission bits over, and swap it in with the Windows-hardened\n * rename. Split out so `atomicWrite` (whole-buffer) and\n * `atomicReplaceWithWriter` (streaming) cannot drift apart.\n */\n async function commitTemp(tmp: string, targetPath: string, opts: AtomicWriteOptions) {\n try {\n const fileHandle = await fs.open(tmp, 'r+');\n try {\n await fileHandle.sync();\n } finally {\n await fileHandle.close();\n }\n } catch {\n // fsync is best-effort; the atomic rename still protects readers.\n }\n\n let mode: number | undefined;\n try {\n const stat = await fs.stat(targetPath);\n mode = stat.mode & 0o777;\n } catch {\n mode = opts.mode;\n }\n if (mode !== undefined) await fs.chmod(tmp, mode);\n\n await renameWithRetry(tmp, targetPath);\n if (mode !== undefined && process.platform === 'win32') {\n await fs.chmod(targetPath, mode).catch(() => undefined);\n }\n }\n\n function tempPathFor(targetPath: string): string {\n return path.join(\n path.dirname(targetPath),\n `.${path.basename(targetPath)}.${randomBytes(6).toString('hex')}.tmp`,\n );\n }\n\n async function atomicWrite(\n targetPath: string,\n content: string | Uint8Array,\n opts: AtomicWriteOptions = {},\n ): Promise<void> {\n await fs.mkdir(path.dirname(targetPath), { recursive: true });\n const tmp = tempPathFor(targetPath);\n\n try {\n if (typeof content === 'string') {\n await fs.writeFile(tmp, content, { flag: 'wx', encoding: opts.encoding ?? 'utf8' });\n } else {\n await fs.writeFile(tmp, content, { flag: 'wx' });\n }\n await commitTemp(tmp, targetPath, opts);\n } catch (error) {\n await fs.unlink(tmp).catch(() => undefined);\n throw error;\n }\n }\n\n /**\n * Atomically replace `targetPath` with whatever `write` streams into the\n * handle it is given. Same durability and rename semantics as\n * {@link atomicWrite}, but the caller never has to materialize the new\n * contents in memory \u2014 the point of this variant. Used by log rotation,\n * where holding the retained tail as one buffer is exactly the allocation\n * spike being avoided.\n *\n * Returns whatever `write` returns. The temp file is removed if `write`\n * throws, so a failed rotation leaves the original file untouched.\n */\n async function atomicReplaceWithWriter<T>(\n targetPath: string,\n write: (handle: fs.FileHandle) => Promise<T>,\n opts: AtomicWriteOptions = {},\n ): Promise<T> {\n await fs.mkdir(path.dirname(targetPath), { recursive: true });\n const tmp = tempPathFor(targetPath);\n\n try {\n const handle = await fs.open(tmp, 'wx');\n let result: T;\n try {\n result = await write(handle);\n } finally {\n await handle.close().catch(() => undefined);\n }\n await commitTemp(tmp, targetPath, opts);\n return result;\n } catch (error) {\n await fs.unlink(tmp).catch(() => undefined);\n throw error;\n }\n }\n\n async function ensureDir(dir: string): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n }\n\n async function withFileLock<T>(\n targetPath: string,\n fn: () => Promise<T>,\n opts: FileLockOptions = {},\n ): Promise<T> {\n const dir = path.dirname(targetPath);\n const lockPath = path.join(dir, `.${path.basename(targetPath)}.lock`);\n const timeoutMs = opts.timeoutMs ?? 15_000;\n const staleMs = opts.staleMs ?? 30_000;\n const started = Date.now();\n let handle: fs.FileHandle | undefined;\n\n for (;;) {\n try {\n handle = await fs.open(lockPath, 'wx');\n await handle.writeFile(`${process.pid}:${Date.now()}`);\n break;\n } catch (error) {\n if (handle) {\n await handle.close().catch(() => undefined);\n await fs.unlink(lockPath).catch(() => undefined);\n handle = undefined;\n }\n\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') {\n await fs.mkdir(dir, { recursive: true });\n continue;\n }\n if (code !== 'EEXIST' && code !== 'EPERM') throw error;\n\n try {\n const stat = await fs.stat(lockPath);\n if (Date.now() - stat.mtimeMs > staleMs) {\n // Re-stat right before removing. A live holder's heartbeat (or a\n // fresh holder that just acquired) changes mtimeMs; only delete\n // when the lock is STILL the same stale file we observed, so we\n // never unlink another actor's fresh lock in the stat\u2192unlink gap.\n const recheck = await fs.stat(lockPath).catch(() => undefined);\n if (\n recheck &&\n recheck.mtimeMs === stat.mtimeMs &&\n Date.now() - recheck.mtimeMs > staleMs\n ) {\n await fs.unlink(lockPath).catch(() => undefined);\n }\n continue;\n }\n } catch {\n continue;\n }\n\n const elapsed = Date.now() - started;\n if (elapsed >= timeoutMs) {\n throw createLockTimeoutError({ targetPath, timeoutMs });\n }\n await waitForLockRelease(lockPath, timeoutMs - elapsed);\n }\n }\n\n // Heartbeat: refresh the lock's mtime while fn() runs so a legitimately\n // long critical section (e.g. index compaction under a slow disk/AV) is\n // never mistaken for a stale lock and stolen by another process \u2014 which\n // would put two holders in the section and drop writes. A live holder now\n // stays fresh, so the stale-break path only fires for a crashed holder.\n // Refresh twice per stale window so a live holder never crosses staleMs.\n // The small floor only guards against a pathologically tiny staleMs.\n const refreshMs = Math.max(50, Math.floor(staleMs / 2));\n const heartbeat = setInterval(() => {\n const now = new Date();\n void fs.utimes(lockPath, now, now).catch(() => undefined);\n }, refreshMs);\n heartbeat.unref?.();\n\n try {\n return await fn();\n } finally {\n clearInterval(heartbeat);\n await handle?.close().catch(() => undefined);\n await fs.unlink(lockPath).catch(() => undefined);\n }\n }\n\n return { atomicWrite, atomicReplaceWithWriter, ensureDir, withFileLock };\n}\n\nasync function waitForLockRelease(lockPath: string, remainingMs: number): Promise<void> {\n const parentDir = path.dirname(lockPath);\n const lockName = path.basename(lockPath);\n const intervalMs = Math.min(remainingMs, 100);\n\n return new Promise<void>((resolve) => {\n let settled = false;\n let watcher: FSWatcher | null = null;\n const settle = (): void => {\n if (settled) return;\n settled = true;\n watcher?.close();\n resolve();\n };\n const timer = setTimeout(settle, intervalMs);\n\n try {\n watcher = watchDir(parentDir, (eventType, filename) => {\n if (filename === lockName && (eventType === 'rename' || eventType === 'change')) {\n clearTimeout(timer);\n settle();\n }\n });\n } catch {\n clearTimeout(timer);\n setTimeout(settle, Math.min(remainingMs, 25));\n return;\n }\n\n void fs.access(lockPath).catch(() => {\n clearTimeout(timer);\n settle();\n });\n });\n}\n\nconst TRANSIENT_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES', 'ENOTEMPTY']);\n\nasync function renameWithRetry(from: string, to: string): Promise<void> {\n if (process.platform !== 'win32') {\n await fs.rename(from, to);\n return;\n }\n\n // Windows readers (fs.open on the destination) can hold the file through\n // several rename attempts; ~4s total gives concurrent-process readers and\n // antivirus scanners time to release without failing the write. When all\n // retries are exhausted, the caller receives the original error and self-\n // heals on the next cycle \u2014 intentionally NOT falling back to copyFile,\n // which would break the atomic-write contract (a concurrent reader could\n // observe a torn destination during the copy).\n const delays = [10, 25, 60, 120, 250, 500, 1000, 2000];\n let attempt = 0;\n for (;;) {\n try {\n await fs.rename(from, to);\n return;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (!code || !TRANSIENT_RENAME_CODES.has(code) || attempt === delays.length) {\n // All retries exhausted \u2014 emit a diagnostic warning so operators\n // know the atomic write was skipped (the caller self-heals).\n if (attempt === delays.length) {\n process.emitWarning(\n `Windows rename retries exhausted for '${from}' \u2192 '${to}' (code=${code}). ` +\n 'Write skipped \u2014 the caller will retry on the next cycle.',\n { code: 'WRONGSTACK_WIN32_RENAME_EXHAUSTED' },\n );\n }\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, delays[attempt]));\n attempt++;\n }\n }\n}\n\nconst defaultPrimitives = createPersistencePrimitives();\n\nexport const atomicWrite = defaultPrimitives.atomicWrite;\nexport const ensureDir = defaultPrimitives.ensureDir;\nexport const withFileLock = defaultPrimitives.withFileLock;\n"],
|
|
5
|
-
"mappings": "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",
|
|
3
|
+
"sources": ["../src/atomic-write.ts", "../src/socket-path.ts"],
|
|
4
|
+
"sourcesContent": ["import { randomBytes } from 'node:crypto';\nimport type { FSWatcher } from 'node:fs';\nimport { watch as watchDir } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\n\nexport interface AtomicWriteOptions {\n mode?: number | undefined;\n encoding?: BufferEncoding | undefined;\n}\n\nexport interface FileLockOptions {\n timeoutMs?: number | undefined;\n staleMs?: number | undefined;\n}\n\nexport interface FileLockTimeoutDetails {\n targetPath: string;\n timeoutMs: number;\n}\n\nexport interface PersistencePrimitiveOptions {\n createLockTimeoutError?: ((details: FileLockTimeoutDetails) => Error) | undefined;\n}\n\nexport interface PersistencePrimitives {\n atomicWrite(\n targetPath: string,\n content: string | Uint8Array,\n opts?: AtomicWriteOptions,\n ): Promise<void>;\n atomicReplaceWithWriter<T>(\n targetPath: string,\n write: (handle: fs.FileHandle) => Promise<T>,\n opts?: AtomicWriteOptions,\n ): Promise<T>;\n ensureDir(dir: string): Promise<void>;\n withFileLock<T>(targetPath: string, fn: () => Promise<T>, opts?: FileLockOptions): Promise<T>;\n}\n\n/** A dependency-free structured error for persistence boundary failures. */\nexport class PersistenceFsError extends Error {\n override name = 'FsError';\n readonly code: string;\n readonly path?: string | undefined;\n readonly context?: Record<string, unknown> | undefined;\n\n constructor(opts: {\n message: string;\n code: string;\n path?: string | undefined;\n context?: Record<string, unknown> | undefined;\n cause?: unknown | undefined;\n }) {\n super(opts.message, { cause: opts.cause });\n this.code = opts.code;\n this.path = opts.path;\n this.context = opts.context;\n }\n}\n\n/**\n * Create an isolated primitive set. Hosts may inject their own structured\n * timeout error without making this low-level package depend on that host.\n */\nexport function createPersistencePrimitives(\n options: PersistencePrimitiveOptions = {},\n): PersistencePrimitives {\n const createLockTimeoutError =\n options.createLockTimeoutError ??\n (({ targetPath, timeoutMs }: FileLockTimeoutDetails) =>\n new PersistenceFsError({\n message: `Timed out waiting for file lock: ${targetPath}`,\n code: 'FS_ATOMIC_WRITE_FAILED',\n path: targetPath,\n context: { timeoutMs },\n }));\n\n /**\n * Shared tail of every atomic replace: fsync the temp file, carry the\n * target's permission bits over, and swap it in with the Windows-hardened\n * rename. Split out so `atomicWrite` (whole-buffer) and\n * `atomicReplaceWithWriter` (streaming) cannot drift apart.\n */\n async function commitTemp(tmp: string, targetPath: string, opts: AtomicWriteOptions) {\n try {\n const fileHandle = await fs.open(tmp, 'r+');\n try {\n await fileHandle.sync();\n } finally {\n await fileHandle.close();\n }\n } catch {\n // fsync is best-effort; the atomic rename still protects readers.\n }\n\n let mode: number | undefined;\n try {\n const stat = await fs.stat(targetPath);\n mode = stat.mode & 0o777;\n } catch {\n mode = opts.mode;\n }\n if (mode !== undefined) await fs.chmod(tmp, mode);\n\n await renameWithRetry(tmp, targetPath);\n if (mode !== undefined && process.platform === 'win32') {\n await fs.chmod(targetPath, mode).catch(() => undefined);\n }\n }\n\n function tempPathFor(targetPath: string): string {\n return path.join(\n path.dirname(targetPath),\n `.${path.basename(targetPath)}.${randomBytes(6).toString('hex')}.tmp`,\n );\n }\n\n async function atomicWrite(\n targetPath: string,\n content: string | Uint8Array,\n opts: AtomicWriteOptions = {},\n ): Promise<void> {\n await fs.mkdir(path.dirname(targetPath), { recursive: true });\n const tmp = tempPathFor(targetPath);\n\n try {\n if (typeof content === 'string') {\n await fs.writeFile(tmp, content, { flag: 'wx', encoding: opts.encoding ?? 'utf8' });\n } else {\n await fs.writeFile(tmp, content, { flag: 'wx' });\n }\n await commitTemp(tmp, targetPath, opts);\n } catch (error) {\n await fs.unlink(tmp).catch(() => undefined);\n throw error;\n }\n }\n\n /**\n * Atomically replace `targetPath` with whatever `write` streams into the\n * handle it is given. Same durability and rename semantics as\n * {@link atomicWrite}, but the caller never has to materialize the new\n * contents in memory \u2014 the point of this variant. Used by log rotation,\n * where holding the retained tail as one buffer is exactly the allocation\n * spike being avoided.\n *\n * Returns whatever `write` returns. The temp file is removed if `write`\n * throws, so a failed rotation leaves the original file untouched.\n */\n async function atomicReplaceWithWriter<T>(\n targetPath: string,\n write: (handle: fs.FileHandle) => Promise<T>,\n opts: AtomicWriteOptions = {},\n ): Promise<T> {\n await fs.mkdir(path.dirname(targetPath), { recursive: true });\n const tmp = tempPathFor(targetPath);\n\n try {\n const handle = await fs.open(tmp, 'wx');\n let result: T;\n try {\n result = await write(handle);\n } finally {\n await handle.close().catch(() => undefined);\n }\n await commitTemp(tmp, targetPath, opts);\n return result;\n } catch (error) {\n await fs.unlink(tmp).catch(() => undefined);\n throw error;\n }\n }\n\n async function ensureDir(dir: string): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n }\n\n async function withFileLock<T>(\n targetPath: string,\n fn: () => Promise<T>,\n opts: FileLockOptions = {},\n ): Promise<T> {\n const dir = path.dirname(targetPath);\n const lockPath = path.join(dir, `.${path.basename(targetPath)}.lock`);\n const timeoutMs = opts.timeoutMs ?? 15_000;\n const staleMs = opts.staleMs ?? 30_000;\n const started = Date.now();\n let handle: fs.FileHandle | undefined;\n\n for (;;) {\n try {\n handle = await fs.open(lockPath, 'wx');\n await handle.writeFile(`${process.pid}:${Date.now()}`);\n break;\n } catch (error) {\n if (handle) {\n await handle.close().catch(() => undefined);\n await fs.unlink(lockPath).catch(() => undefined);\n handle = undefined;\n }\n\n const code = (error as NodeJS.ErrnoException).code;\n if (code === 'ENOENT') {\n await fs.mkdir(dir, { recursive: true });\n continue;\n }\n if (code !== 'EEXIST' && code !== 'EPERM') throw error;\n\n try {\n const stat = await fs.stat(lockPath);\n if (Date.now() - stat.mtimeMs > staleMs) {\n // Re-stat right before removing. A live holder's heartbeat (or a\n // fresh holder that just acquired) changes mtimeMs; only delete\n // when the lock is STILL the same stale file we observed, so we\n // never unlink another actor's fresh lock in the stat\u2192unlink gap.\n const recheck = await fs.stat(lockPath).catch(() => undefined);\n if (\n recheck &&\n recheck.mtimeMs === stat.mtimeMs &&\n Date.now() - recheck.mtimeMs > staleMs\n ) {\n await fs.unlink(lockPath).catch(() => undefined);\n }\n continue;\n }\n } catch {\n continue;\n }\n\n const elapsed = Date.now() - started;\n if (elapsed >= timeoutMs) {\n throw createLockTimeoutError({ targetPath, timeoutMs });\n }\n await waitForLockRelease(lockPath, timeoutMs - elapsed);\n }\n }\n\n // Heartbeat: refresh the lock's mtime while fn() runs so a legitimately\n // long critical section (e.g. index compaction under a slow disk/AV) is\n // never mistaken for a stale lock and stolen by another process \u2014 which\n // would put two holders in the section and drop writes. A live holder now\n // stays fresh, so the stale-break path only fires for a crashed holder.\n // Refresh twice per stale window so a live holder never crosses staleMs.\n // The small floor only guards against a pathologically tiny staleMs.\n const refreshMs = Math.max(50, Math.floor(staleMs / 2));\n const heartbeat = setInterval(() => {\n const now = new Date();\n void fs.utimes(lockPath, now, now).catch(() => undefined);\n }, refreshMs);\n heartbeat.unref?.();\n\n try {\n return await fn();\n } finally {\n clearInterval(heartbeat);\n await handle?.close().catch(() => undefined);\n await fs.unlink(lockPath).catch(() => undefined);\n }\n }\n\n return { atomicWrite, atomicReplaceWithWriter, ensureDir, withFileLock };\n}\n\nasync function waitForLockRelease(lockPath: string, remainingMs: number): Promise<void> {\n const parentDir = path.dirname(lockPath);\n const lockName = path.basename(lockPath);\n const intervalMs = Math.min(remainingMs, 100);\n\n return new Promise<void>((resolve) => {\n let settled = false;\n let watcher: FSWatcher | null = null;\n const settle = (): void => {\n if (settled) return;\n settled = true;\n watcher?.close();\n resolve();\n };\n const timer = setTimeout(settle, intervalMs);\n\n try {\n watcher = watchDir(parentDir, (eventType, filename) => {\n if (filename === lockName && (eventType === 'rename' || eventType === 'change')) {\n clearTimeout(timer);\n settle();\n }\n });\n } catch {\n clearTimeout(timer);\n setTimeout(settle, Math.min(remainingMs, 25));\n return;\n }\n\n void fs.access(lockPath).catch(() => {\n clearTimeout(timer);\n settle();\n });\n });\n}\n\nconst TRANSIENT_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES', 'ENOTEMPTY']);\n\nasync function renameWithRetry(from: string, to: string): Promise<void> {\n if (process.platform !== 'win32') {\n await fs.rename(from, to);\n return;\n }\n\n // Windows readers (fs.open on the destination) can hold the file through\n // several rename attempts; ~4s total gives concurrent-process readers and\n // antivirus scanners time to release without failing the write. When all\n // retries are exhausted, the caller receives the original error and self-\n // heals on the next cycle \u2014 intentionally NOT falling back to copyFile,\n // which would break the atomic-write contract (a concurrent reader could\n // observe a torn destination during the copy).\n const delays = [10, 25, 60, 120, 250, 500, 1000, 2000];\n let attempt = 0;\n for (;;) {\n try {\n await fs.rename(from, to);\n return;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (!code || !TRANSIENT_RENAME_CODES.has(code) || attempt === delays.length) {\n // All retries exhausted \u2014 emit a diagnostic warning so operators\n // know the atomic write was skipped (the caller self-heals).\n if (attempt === delays.length) {\n process.emitWarning(\n `Windows rename retries exhausted for '${from}' \u2192 '${to}' (code=${code}). ` +\n 'Write skipped \u2014 the caller will retry on the next cycle.',\n { code: 'WRONGSTACK_WIN32_RENAME_EXHAUSTED' },\n );\n }\n throw error;\n }\n await new Promise((resolve) => setTimeout(resolve, delays[attempt]));\n attempt++;\n }\n }\n}\n\nconst defaultPrimitives = createPersistencePrimitives();\n\nexport const atomicWrite = defaultPrimitives.atomicWrite;\nexport const ensureDir = defaultPrimitives.ensureDir;\nexport const withFileLock = defaultPrimitives.withFileLock;\n", "/**\n * Unix domain socket path length validation.\n *\n * POSIX `sockaddr_un.sun_path` is a fixed-size buffer that includes the\n * terminating NUL. macOS (and other BSDs) allow 104 bytes; Linux allows 108.\n * A `bind()`/`connect()` on a longer path fails with EINVAL/ENAMETOOLONG \u2014\n * and when the failing process is a detached daemon with `stdio: 'ignore'`,\n * the error is invisible and clients only see a connect timeout.\n *\n * Every WrongStack project daemon (codebase-index, SAGE, Kanban, mailbox,\n * chronicle) derives a deterministic socket path under the platform temp\n * directory; on macOS that directory is the ~48-character per-user\n * `/var/folders/<xx>/<30 chars>/T`, which leaves little headroom. Validate\n * endpoints at derivation time so an over-long path fails loudly (or degrades\n * explicitly) instead of timing out silently.\n */\n\n/** Usable `sun_path` bytes (capacity minus the terminating NUL) per platform. */\nexport function unixSocketPathLimit(platform: NodeJS.Platform = process.platform): number {\n // macOS and the BSDs: sizeof(sun_path) == 104 including NUL.\n if (platform === 'darwin' || platform === 'freebsd' || platform === 'openbsd') return 103;\n // Linux and the rest of the POSIX family: 108 including NUL.\n return 107;\n}\n\nexport interface UnixSocketPathCheck {\n readonly ok: boolean;\n readonly byteLength: number;\n /** Usable byte budget on this platform (NUL excluded). */\n readonly maxBytes: number;\n}\n\n/**\n * Non-throwing length check. Windows named pipes are not filesystem paths and\n * have no `sun_path` limit, so they always pass.\n */\nexport function checkUnixSocketPath(\n socketPath: string,\n platform: NodeJS.Platform = process.platform,\n): UnixSocketPathCheck {\n const byteLength = Buffer.byteLength(socketPath, 'utf8');\n if (platform === 'win32') {\n return { ok: true, byteLength, maxBytes: Number.MAX_SAFE_INTEGER };\n }\n const maxBytes = unixSocketPathLimit(platform);\n return { ok: byteLength <= maxBytes, byteLength, maxBytes };\n}\n\n/**\n * Throwing variant for daemons whose endpoints are expected to always fit.\n * The error names the service and the concrete byte counts so the operator\n * can act (usually: shorten TMPDIR) instead of debugging a silent timeout.\n */\nexport function assertUnixSocketPathWithinLimit(\n socketPath: string,\n service: string,\n platform: NodeJS.Platform = process.platform,\n): void {\n const check = checkUnixSocketPath(socketPath, platform);\n if (check.ok) return;\n throw new Error(\n `${service} IPC socket path is ${check.byteLength} bytes, over the ${platform} ` +\n `sun_path limit of ${check.maxBytes} usable bytes: ${socketPath}. ` +\n `Set a shorter TMPDIR to relocate WrongStack IPC sockets.`,\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["stat", "atomicWrite", "ensureDir", "withFileLock"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unix domain socket path length validation.
|
|
3
|
+
*
|
|
4
|
+
* POSIX `sockaddr_un.sun_path` is a fixed-size buffer that includes the
|
|
5
|
+
* terminating NUL. macOS (and other BSDs) allow 104 bytes; Linux allows 108.
|
|
6
|
+
* A `bind()`/`connect()` on a longer path fails with EINVAL/ENAMETOOLONG —
|
|
7
|
+
* and when the failing process is a detached daemon with `stdio: 'ignore'`,
|
|
8
|
+
* the error is invisible and clients only see a connect timeout.
|
|
9
|
+
*
|
|
10
|
+
* Every WrongStack project daemon (codebase-index, SAGE, Kanban, mailbox,
|
|
11
|
+
* chronicle) derives a deterministic socket path under the platform temp
|
|
12
|
+
* directory; on macOS that directory is the ~48-character per-user
|
|
13
|
+
* `/var/folders/<xx>/<30 chars>/T`, which leaves little headroom. Validate
|
|
14
|
+
* endpoints at derivation time so an over-long path fails loudly (or degrades
|
|
15
|
+
* explicitly) instead of timing out silently.
|
|
16
|
+
*/
|
|
17
|
+
/** Usable `sun_path` bytes (capacity minus the terminating NUL) per platform. */
|
|
18
|
+
export declare function unixSocketPathLimit(platform?: NodeJS.Platform): number;
|
|
19
|
+
export interface UnixSocketPathCheck {
|
|
20
|
+
readonly ok: boolean;
|
|
21
|
+
readonly byteLength: number;
|
|
22
|
+
/** Usable byte budget on this platform (NUL excluded). */
|
|
23
|
+
readonly maxBytes: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Non-throwing length check. Windows named pipes are not filesystem paths and
|
|
27
|
+
* have no `sun_path` limit, so they always pass.
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkUnixSocketPath(socketPath: string, platform?: NodeJS.Platform): UnixSocketPathCheck;
|
|
30
|
+
/**
|
|
31
|
+
* Throwing variant for daemons whose endpoints are expected to always fit.
|
|
32
|
+
* The error names the service and the concrete byte counts so the operator
|
|
33
|
+
* can act (usually: shorten TMPDIR) instead of debugging a silent timeout.
|
|
34
|
+
*/
|
|
35
|
+
export declare function assertUnixSocketPathWithinLimit(socketPath: string, service: string, platform?: NodeJS.Platform): void;
|
|
36
|
+
//# sourceMappingURL=socket-path.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-path.d.ts","sourceRoot":"","sources":["../src/socket-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,CAKxF;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,mBAAmB,CAOrB;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,IAAI,CAQN"}
|