@wrongstack/persistence 0.295.0 → 0.295.1

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.
@@ -1 +1 @@
1
- {"version":3,"file":"atomic-write.d.ts","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,sBAAsB,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,sBAAsB,KAAK,KAAK,CAAC,GAAG,SAAS,CAAC;CACnF;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,CACT,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC/F;AAED,4EAA4E;AAC5E,qBAAa,kBAAmB,SAAQ,KAAK;IAClC,IAAI,SAAa;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAEvD,YAAY,IAAI,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC9C,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;KAC7B,EAKA;CACF;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,2BAAgC,GACxC,qBAAqB,CAyHvB;AAgED,eAAO,MAAM,WAAW,eA5NR,MAAM,WACT,MAAM,GAAG,UAAU,SACrB,kBAAkB,KACxB,OAAO,CAAC,IAAI,CAyNuC,CAAC;AACzD,eAAO,MAAM,SAAS,QAzNL,MAAM,KAAG,OAAO,CAAC,IAAI,CAyNc,CAAC;AACrD,eAAO,MAAM,YAAY,GAzNV,CAAC,cAAc,MAAM,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,SAAS,eAAe,KAAG,OAAO,CAAC,CAAC,CAyNpC,CAAC"}
1
+ {"version":3,"file":"atomic-write.d.ts","sourceRoot":"","sources":["../src/atomic-write.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC9B;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,2BAA2B;IAC1C,sBAAsB,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,sBAAsB,KAAK,KAAK,CAAC,GAAG,SAAS,CAAC;CACnF;AAED,MAAM,WAAW,qBAAqB;IACpC,WAAW,CACT,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,UAAU,EAC5B,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,YAAY,CAAC,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC/F;AAED,4EAA4E;AAC5E,qBAAa,kBAAmB,SAAQ,KAAK;IAClC,IAAI,SAAa;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAEvD,YAAY,IAAI,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;QAC1B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;QAC9C,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;KAC7B,EAKA;CACF;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,OAAO,GAAE,2BAAgC,GACxC,qBAAqB,CAmJvB;AAiFD,eAAO,MAAM,WAAW,eAvQR,MAAM,WACT,MAAM,GAAG,UAAU,SACrB,kBAAkB,KACxB,OAAO,CAAC,IAAI,CAoQuC,CAAC;AACzD,eAAO,MAAM,SAAS,QApQL,MAAM,KAAG,OAAO,CAAC,IAAI,CAoQc,CAAC;AACrD,eAAO,MAAM,YAAY,GApQV,CAAC,cAAc,MAAM,MAAM,MAAM,OAAO,CAAC,CAAC,CAAC,SAAS,eAAe,KAAG,OAAO,CAAC,CAAC,CAoQpC,CAAC"}
package/dist/index.js CHANGED
@@ -91,7 +91,10 @@ function createPersistencePrimitives(options = {}) {
91
91
  try {
92
92
  const stat2 = await fs.stat(lockPath);
93
93
  if (Date.now() - stat2.mtimeMs > staleMs) {
94
- await fs.unlink(lockPath);
94
+ const recheck = await fs.stat(lockPath).catch(() => void 0);
95
+ if (recheck && recheck.mtimeMs === stat2.mtimeMs && Date.now() - recheck.mtimeMs > staleMs) {
96
+ await fs.unlink(lockPath).catch(() => void 0);
97
+ }
95
98
  continue;
96
99
  }
97
100
  } catch {
@@ -104,9 +107,16 @@ function createPersistencePrimitives(options = {}) {
104
107
  await waitForLockRelease(lockPath, timeoutMs - elapsed);
105
108
  }
106
109
  }
110
+ const refreshMs = Math.max(50, Math.floor(staleMs / 2));
111
+ const heartbeat = setInterval(() => {
112
+ const now = /* @__PURE__ */ new Date();
113
+ void fs.utimes(lockPath, now, now).catch(() => void 0);
114
+ }, refreshMs);
115
+ heartbeat.unref?.();
107
116
  try {
108
117
  return await fn();
109
118
  } finally {
119
+ clearInterval(heartbeat);
110
120
  await handle?.close().catch(() => void 0);
111
121
  await fs.unlink(lockPath).catch(() => void 0);
112
122
  }
@@ -151,20 +161,27 @@ async function renameWithRetry(from, to) {
151
161
  await fs.rename(from, to);
152
162
  return;
153
163
  }
154
- const delays = [10, 25, 60, 120, 250];
155
- let lastError;
156
- for (let attempt = 0; attempt <= delays.length; attempt++) {
164
+ const delays = [10, 25, 60, 120, 250, 500, 1e3, 2e3];
165
+ let attempt = 0;
166
+ for (; ; ) {
157
167
  try {
158
168
  await fs.rename(from, to);
159
169
  return;
160
170
  } catch (error) {
161
- lastError = error;
162
171
  const code = error.code;
163
- if (!code || !TRANSIENT_RENAME_CODES.has(code) || attempt === delays.length) throw error;
172
+ if (!code || !TRANSIENT_RENAME_CODES.has(code) || attempt === delays.length) {
173
+ if (attempt === delays.length) {
174
+ process.emitWarning(
175
+ `Windows rename retries exhausted for '${from}' \u2192 '${to}' (code=${code}). Write skipped \u2014 the caller will retry on the next cycle.`,
176
+ { code: "WRONGSTACK_WIN32_RENAME_EXHAUSTED" }
177
+ );
178
+ }
179
+ throw error;
180
+ }
164
181
  await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
182
+ attempt++;
165
183
  }
166
184
  }
167
- throw lastError;
168
185
  }
169
186
  var defaultPrimitives = createPersistencePrimitives();
170
187
  var atomicWrite = defaultPrimitives.atomicWrite;
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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 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 async function atomicWrite(\n targetPath: string,\n content: string | Uint8Array,\n opts: AtomicWriteOptions = {},\n ): Promise<void> {\n const dir = path.dirname(targetPath);\n await fs.mkdir(dir, { recursive: true });\n const tmp = path.join(\n dir,\n `.${path.basename(targetPath)}.${randomBytes(6).toString('hex')}.tmp`,\n );\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 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 } 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 await fs.unlink(lockPath);\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 try {\n return await fn();\n } finally {\n await handle?.close().catch(() => undefined);\n await fs.unlink(lockPath).catch(() => undefined);\n }\n }\n\n return { atomicWrite, 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 const delays = [10, 25, 60, 120, 250];\n let lastError: unknown;\n for (let attempt = 0; attempt <= delays.length; attempt++) {\n try {\n await fs.rename(from, to);\n return;\n } catch (error) {\n lastError = error;\n const code = (error as NodeJS.ErrnoException).code;\n if (!code || !TRANSIENT_RENAME_CODES.has(code) || attempt === delays.length) throw error;\n await new Promise((resolve) => setTimeout(resolve, delays[attempt]));\n }\n }\n throw lastError;\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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",
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 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 async function atomicWrite(\n targetPath: string,\n content: string | Uint8Array,\n opts: AtomicWriteOptions = {},\n ): Promise<void> {\n const dir = path.dirname(targetPath);\n await fs.mkdir(dir, { recursive: true });\n const tmp = path.join(\n dir,\n `.${path.basename(targetPath)}.${randomBytes(6).toString('hex')}.tmp`,\n );\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 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 } 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, 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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",
6
6
  "names": ["atomicWrite", "stat", "ensureDir", "withFileLock"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/persistence",
3
- "version": "0.295.0",
3
+ "version": "0.295.1",
4
4
  "license": "MIT",
5
5
  "description": "Dependency-free filesystem persistence primitives shared across WrongStack packages.",
6
6
  "repository": {