{"version":3,"file":"atomicJson-D2t0CDh9.mjs","names":[],"sources":["../src/services/fileSystemRetry/index.ts","../src/services/atomicJson/index.ts"],"sourcesContent":["/**\n * Retry helpers for transient filesystem failures.\n *\n * Windows holds brief directory and rename locks (antivirus, indexers, another\n * process reading the file), surfacing as EACCES, EBUSY or EPERM. Those clear on\n * their own, so the operation is retried on a backoff instead of failing a run.\n *\n * DESIGN PATTERNS:\n * - Two variants with the same policy: a synchronous one for the atomic writers\n *   that must not yield mid-write, and an async one for everything else\n *\n * AVOID:\n * - Reaching for the synchronous variant on a hot path; it blocks the event\n *   loop, and every millisecond of backoff is a millisecond the parent cannot\n *   service a child. Prefer `runFileSystemOperationWithRetryAsync`\n * - Retrying ENOENT or ENOTDIR; those are real errors, not contention\n */\n\nconst WAIT_BUFFER = typeof SharedArrayBuffer !== 'undefined' ? new SharedArrayBuffer(4) : undefined;\nconst WAIT_VIEW = WAIT_BUFFER ? new Int32Array(WAIT_BUFFER) : undefined;\nconst RETRYABLE_FILE_SYSTEM_ERROR_CODES = new Set(['EACCES', 'EBUSY', 'EPERM']);\n\nexport const DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS = [10, 25, 50, 100, 200, 500, 1000, 2000, 4000] as const;\n\nexport type FileSystemRetryOptions = {\n  retryDelaysMs?: readonly number[];\n  wait?: (delayMs: number) => void;\n};\n\nexport type AsyncFileSystemRetryOptions = {\n  retryDelaysMs?: readonly number[];\n  wait?: (delayMs: number) => Promise<void>;\n};\n\n/**\n * Block the current thread for `delayMs`.\n *\n * Only for synchronous callers that cannot yield. `Atomics.wait` parks the\n * thread rather than burning CPU; the loop below is a portable fallback for\n * runtimes without SharedArrayBuffer and does spin, which is why async callers\n * must use `delayForFileSystemRetry` instead.\n */\nexport function waitForFileSystemRetry(delayMs: number): void {\n  if (delayMs <= 0) return;\n  if (WAIT_VIEW) {\n    try {\n      Atomics.wait(WAIT_VIEW, 0, 0, delayMs);\n      return;\n    } catch {\n      // Fall through to the portable busy wait below.\n    }\n  }\n  const end = Date.now() + delayMs;\n  while (Date.now() < end) {\n    // Portable fallback for runtimes where Atomics.wait is unavailable.\n  }\n}\n\n/** Yield for `delayMs` without blocking the event loop. */\nexport function delayForFileSystemRetry(delayMs: number): Promise<void> {\n  if (delayMs <= 0) return Promise.resolve();\n  return new Promise((resolve) => {\n    setTimeout(resolve, delayMs);\n  });\n}\n\nexport function isRetryableFileSystemError(error: unknown): boolean {\n  const code = (error as NodeJS.ErrnoException | undefined)?.code;\n  return typeof code === 'string' && RETRYABLE_FILE_SYSTEM_ERROR_CODES.has(code);\n}\n\nexport function runFileSystemOperationWithRetry<T>(operation: () => T, options: FileSystemRetryOptions = {}): T {\n  const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;\n  const wait = options.wait ?? waitForFileSystemRetry;\n  for (let attempt = 0; ; attempt++) {\n    try {\n      return operation();\n    } catch (error) {\n      const delayMs = retryDelaysMs[attempt];\n      if (delayMs === undefined || !isRetryableFileSystemError(error)) throw error;\n      wait(delayMs);\n    }\n  }\n}\n\nexport async function runFileSystemOperationWithRetryAsync<T>(\n  operation: () => T | Promise<T>,\n  options: AsyncFileSystemRetryOptions = {},\n): Promise<T> {\n  const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;\n  const wait = options.wait ?? delayForFileSystemRetry;\n  for (let attempt = 0; ; attempt++) {\n    try {\n      return await operation();\n    } catch (error) {\n      const delayMs = retryDelaysMs[attempt];\n      if (delayMs === undefined || !isRetryableFileSystemError(error)) throw error;\n      await wait(delayMs);\n    }\n  }\n}\n","/**\n * Atomic JSON writes.\n *\n * DESIGN PATTERNS:\n * - Write to a uniquely named temp file in the destination directory, then\n *   rename onto the target. Rename is atomic within a filesystem, so a reader\n *   sees either the previous file or the complete new one, never a partial write\n * - The temp name carries pid, timestamp and a random suffix so two processes\n *   writing the same target never collide on the temp file\n *\n * WHAT THIS DOES NOT DO:\n * This prevents torn reads. It does NOT prevent lost updates: two writers doing\n * read-modify-write still clobber each other, because the rename only publishes\n * whatever the caller already computed. Anything needing mutual exclusion has to\n * take a lease or claim, not rely on atomic writes.\n *\n * AVOID:\n * - Treating this as a lock\n * - Using the plain writer for anything sensitive; prefer writePrivateAtomicJson,\n *   which creates the file 0600 so a shared temp directory does not leak prompts\n */\n\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport {\n  DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS,\n  runFileSystemOperationWithRetry,\n  runFileSystemOperationWithRetryAsync,\n  waitForFileSystemRetry,\n} from '../fileSystemRetry';\n\nconst PRIVATE_FILE_MODE = 0o600;\nconst JSON_INDENT = 2;\n\ntype AtomicJsonFs = Pick<typeof fs, 'mkdirSync' | 'writeFileSync' | 'renameSync' | 'rmSync'>;\n\ntype AtomicJsonWriterOptions = {\n  fs?: AtomicJsonFs;\n  now?: () => number;\n  pid?: number;\n  random?: () => number;\n  mode?: number;\n  retryRenameErrors?: boolean;\n  retryDirectoryErrors?: boolean;\n  retryDelaysMs?: readonly number[];\n  wait?: (delayMs: number) => void;\n};\n\nexport function createAtomicJsonWriter(\n  options: AtomicJsonWriterOptions = {},\n): (filePath: string, payload: object) => void {\n  const fsImpl = options.fs ?? fs;\n  const now = options.now ?? Date.now;\n  const pid = options.pid ?? process.pid;\n  const random = options.random ?? Math.random;\n  const mode = options.mode;\n  // Retries exist for Windows directory and rename locks; other platforms fail fast.\n  const retryRenameErrors = options.retryRenameErrors ?? process.platform === 'win32';\n  const retryDirectoryErrors = options.retryDirectoryErrors ?? retryRenameErrors;\n  const retryDelaysMs = options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS;\n  const renameRetryDelaysMs = retryRenameErrors ? retryDelaysMs : [];\n  const directoryRetryDelaysMs = retryDirectoryErrors ? retryDelaysMs : [];\n  const wait = options.wait ?? waitForFileSystemRetry;\n\n  return (filePath: string, payload: object): void => {\n    runFileSystemOperationWithRetry(\n      () => {\n        fsImpl.mkdirSync(path.dirname(filePath), { recursive: true });\n      },\n      { retryDelaysMs: directoryRetryDelaysMs, wait },\n    );\n    const tempPath = path.join(\n      path.dirname(filePath),\n      `.${path.basename(filePath)}.${pid}.${now()}.${random().toString(36).slice(2)}.tmp`,\n    );\n    try {\n      fsImpl.writeFileSync(\n        tempPath,\n        JSON.stringify(payload, null, JSON_INDENT),\n        mode === undefined ? 'utf-8' : { encoding: 'utf-8', mode },\n      );\n      runFileSystemOperationWithRetry(\n        () => {\n          fsImpl.renameSync(tempPath, filePath);\n        },\n        { retryDelaysMs: renameRetryDelaysMs, wait },\n      );\n    } finally {\n      // Best effort: on the happy path the rename already consumed the temp file.\n      fsImpl.rmSync(tempPath, { force: true });\n    }\n  };\n}\n\nexport const writeAtomicJson = createAtomicJsonWriter();\nexport const writePrivateAtomicJson = createAtomicJsonWriter({ mode: PRIVATE_FILE_MODE });\n\n/** Promise-based atomic writer for startup, polling, and other non-critical paths. */\nexport async function writeAtomicJsonAsync(filePath: string, payload: object, mode?: number): Promise<void> {\n  const retryDelaysMs = process.platform === 'win32' ? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS : [];\n  await runFileSystemOperationWithRetryAsync(() => fs.promises.mkdir(path.dirname(filePath), { recursive: true }), {\n    retryDelaysMs,\n  });\n  const tempPath = path.join(\n    path.dirname(filePath),\n    `.${path.basename(filePath)}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`,\n  );\n  try {\n    await fs.promises.writeFile(\n      tempPath,\n      JSON.stringify(payload, null, JSON_INDENT),\n      mode === undefined ? 'utf-8' : { encoding: 'utf-8', mode },\n    );\n    await runFileSystemOperationWithRetryAsync(() => fs.promises.rename(tempPath, filePath), { retryDelaysMs });\n  } finally {\n    await fs.promises.rm(tempPath, { force: true });\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAkBA,MAAM,cAAc,OAAO,sBAAsB,cAAc,IAAI,kBAAkB,CAAC,IAAI,KAAA;AAC1F,MAAM,YAAY,cAAc,IAAI,WAAW,WAAW,IAAI,KAAA;AAC9D,MAAM,oDAAoC,IAAI,IAAI;CAAC;CAAU;CAAS;AAAO,CAAC;AAE9E,MAAa,sCAAsC;CAAC;CAAI;CAAI;CAAI;CAAK;CAAK;CAAK;CAAM;CAAM;AAAI;;;;;;;;;AAoB/F,SAAgB,uBAAuB,SAAuB;CAC5D,IAAI,WAAW,GAAG;CAClB,IAAI,WACF,IAAI;EACF,QAAQ,KAAK,WAAW,GAAG,GAAG,OAAO;EACrC;CACF,QAAQ,CAER;CAEF,MAAM,MAAM,KAAK,IAAI,IAAI;CACzB,OAAO,KAAK,IAAI,IAAI;AAGtB;AAUA,SAAgB,2BAA2B,OAAyB;CAClE,MAAM,OAAQ,OAA6C;CAC3D,OAAO,OAAO,SAAS,YAAY,kCAAkC,IAAI,IAAI;AAC/E;AAEA,SAAgB,gCAAmC,WAAoB,UAAkC,CAAC,GAAM;CAC9G,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,MAAM,OAAO,QAAQ,QAAQ;CAC7B,KAAK,IAAI,UAAU,IAAK,WACtB,IAAI;EACF,OAAO,UAAU;CACnB,SAAS,OAAO;EACd,MAAM,UAAU,cAAc;EAC9B,IAAI,YAAY,KAAA,KAAa,CAAC,2BAA2B,KAAK,GAAG,MAAM;EACvE,KAAK,OAAO;CACd;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;ACnDA,MAAM,oBAAoB;AAC1B,MAAM,cAAc;AAgBpB,SAAgB,uBACd,UAAmC,CAAC,GACS;CAC7C,MAAM,SAAS,QAAQ,MAAM;CAC7B,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,MAAM,QAAQ,OAAO,QAAQ;CACnC,MAAM,SAAS,QAAQ,UAAU,KAAK;CACtC,MAAM,OAAO,QAAQ;CAErB,MAAM,oBAAoB,QAAQ,qBAAqB,QAAQ,aAAa;CAC5E,MAAM,uBAAuB,QAAQ,wBAAwB;CAC7D,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,MAAM,sBAAsB,oBAAoB,gBAAgB,CAAC;CACjE,MAAM,yBAAyB,uBAAuB,gBAAgB,CAAC;CACvE,MAAM,OAAO,QAAQ,QAAQ;CAE7B,QAAQ,UAAkB,YAA0B;EAClD,sCACQ;GACJ,OAAO,UAAU,KAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;EAC9D,GACA;GAAE,eAAe;GAAwB;EAAK,CAChD;EACA,MAAM,WAAW,KAAK,KACpB,KAAK,QAAQ,QAAQ,GACrB,IAAI,KAAK,SAAS,QAAQ,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,KAChF;EACA,IAAI;GACF,OAAO,cACL,UACA,KAAK,UAAU,SAAS,MAAM,WAAW,GACzC,SAAS,KAAA,IAAY,UAAU;IAAE,UAAU;IAAS;GAAK,CAC3D;GACA,sCACQ;IACJ,OAAO,WAAW,UAAU,QAAQ;GACtC,GACA;IAAE,eAAe;IAAqB;GAAK,CAC7C;EACF,UAAU;GAER,OAAO,OAAO,UAAU,EAAE,OAAO,KAAK,CAAC;EACzC;CACF;AACF;AAEA,MAAa,kBAAkB,uBAAuB;AACtD,MAAa,yBAAyB,uBAAuB,EAAE,MAAM,kBAAkB,CAAC"}