import * as fs from "node:fs"; import * as path from "node:path"; import { logInternalError } from "../utils/internal-error.ts"; const RETRYABLE_RENAME_CODES = new Set(["EPERM", "EBUSY", "EACCES"]); function sleepSync(ms: number): void { try { const buffer = new SharedArrayBuffer(4); Atomics.wait(new Int32Array(buffer), 0, 0, ms); } catch { // Fallback for environments without SharedArrayBuffer / Atomics.wait support. const deadline = Date.now() + ms; while (Date.now() < deadline) { // Busy-wait — only used as last-resort, retry counts are capped. } } } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } function isRetryableRenameError(error: unknown): boolean { return Boolean(error && typeof error === "object" && "code" in error && RETRYABLE_RENAME_CODES.has(String((error as NodeJS.ErrnoException).code))); } export function __test__renameWithRetry(tempPath: string, filePath: string, retries = 5, rename: (oldPath: string, newPath: string) => void = fs.renameSync): void { let lastError: unknown; for (let attempt = 0; attempt <= retries; attempt++) { try { rename(tempPath, filePath); return; } catch (error) { lastError = error; if (!isRetryableRenameError(error) || attempt === retries) break; sleepSync(Math.min(250, 10 * 2 ** attempt)); } } throw lastError; } export async function __test__renameWithRetryAsync(tempPath: string, filePath: string, retries = 5, rename: (oldPath: string, newPath: string) => Promise = (source, destination) => fs.promises.rename(source, destination)): Promise { let lastError: unknown; for (let attempt = 0; attempt <= retries; attempt++) { try { await rename(tempPath, filePath); return; } catch (error) { lastError = error; if (!isRetryableRenameError(error) || attempt === retries) break; await sleep(Math.min(250, 10 * 2 ** attempt)); } } throw lastError; } export function atomicWriteFile(filePath: string, content: string): void { fs.mkdirSync(path.dirname(filePath), { recursive: true }); const tempPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`; try { fs.writeFileSync(tempPath, content, "utf-8"); __test__renameWithRetry(tempPath, filePath); } catch (error) { try { fs.rmSync(tempPath, { force: true }); } catch (cleanupError) { logInternalError("atomic-write.cleanup", cleanupError, `tempPath=${tempPath}`); } throw error; } } export async function atomicWriteFileAsync(filePath: string, content: string): Promise { await fs.promises.mkdir(path.dirname(filePath), { recursive: true }); const tempPath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`; try { await fs.promises.writeFile(tempPath, content, "utf-8"); try { await __test__renameWithRetryAsync(tempPath, filePath); } catch (renameError) { let matches = false; try { const existing = await fs.promises.readFile(filePath, "utf-8"); matches = existing === content; } catch { /* ignore */ } if (matches) { try { await fs.promises.rm(tempPath, { force: true }); } catch (cleanupError) { logInternalError("atomic-write.cleanupAsync", cleanupError, `tempPath=${tempPath}`); } return; } throw renameError; } } catch (error) { try { await fs.promises.rm(tempPath, { force: true }); } catch (cleanupError) { logInternalError("atomic-write.cleanupAsync", cleanupError, `tempPath=${tempPath}`); } throw error; } } export function atomicWriteJson(filePath: string, value: T): void { atomicWriteFile(filePath, `${JSON.stringify(value, null, 2)}\n`); } export async function atomicWriteJsonAsync(filePath: string, value: T): Promise { await atomicWriteFileAsync(filePath, `${JSON.stringify(value, null, 2)}\n`); } export function readJsonFile(filePath: string): T | undefined { if (!fs.existsSync(filePath)) return undefined; return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T; }