server/atomic.ts
Rama main · réplica de lectura del repositorio original.
// Temp file, fsync, rename: rename(2) is atomic on one filesystem, so a reader sees the whole old or the whole new file.
// The fsync (F_FULLFSYNC on macOS, ~4 ms) is what `durable: false` drops: a crash still never tears the file, only a power
// loss can leave it empty.
import { randomUUID } from "node:crypto";
import {
closeSync,
constants,
fstatSync,
fsyncSync,
lstatSync,
openSync,
readFileSync,
renameSync,
unlinkSync,
writeFileSync,
} from "node:fs";
export type AtomicWriteOptions = {
mode?: number;
/** false skips the fsync. Default true. */
durable?: boolean;
};
/** Windows refuses a rename onto a path while anything (a virus scanner, the indexer) holds a handle, as EPERM or EACCES
* that would succeed a moment later, so those are retried (~155 ms worst case); everything else throws. */
const RENAME_RETRY_DELAYS_MS = [5, 10, 20, 40, 80];
const RETRYABLE_RENAME_CODES = new Set(["EPERM", "EACCES", "EBUSY"]);
function sleepSync(ms: number): void {
// Node has no synchronous sleep: Atomics.wait on a shared buffer blocks without spinning the CPU.
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
export function renameWithRetry(
tmp: string,
path: string,
rename: (from: string, to: string) => void = renameSync,
): void {
for (let attempt = 0; ; attempt += 1) {
try {
rename(tmp, path);
return;
} catch (e) {
const code = (e as { code?: string }).code;
if (!code || !RETRYABLE_RENAME_CODES.has(code) || attempt >= RENAME_RETRY_DELAYS_MS.length) throw e;
sleepSync(RENAME_RETRY_DELAYS_MS[attempt]!);
}
}
}
export function writeFileAtomic(path: string, data: string | Uint8Array, options: AtomicWriteOptions = {}): void {
const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
let fd: number | null = null;
try {
// Permissions go on the temp inode itself, so a broader-permission file is never visible before a chmod.
fd = openSync(tmp, "w", options.mode);
writeFileSync(fd, data);
if (options.durable !== false) fsyncSync(fd);
closeSync(fd);
fd = null;
renameWithRetry(tmp, path);
} catch (e) {
if (fd !== null) {
try {
closeSync(fd);
} catch {
}
}
try {
unlinkSync(tmp);
} catch {
}
throw e;
}
}
/** Any doubt answers false, so this check is never why a save fails; it reads the disk, since a restore or hand edit may have changed it. */
function holdsExactly(path: string, data: string, mode: number | undefined): boolean {
let fd: number | null = null;
try {
if (process.platform === "win32") {
// No O_NOFOLLOW on Windows; a reparse point is replaced, not read.
if (!lstatSync(path).isFile()) return false;
fd = openSync(path, "r");
} else {
// Inspect the inode the open landed on, so a swap cannot pass as a match; O_NONBLOCK keeps a planted FIFO from hanging the open.
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
}
const stat = fstatSync(fd);
if (!stat.isFile() || stat.size !== Buffer.byteLength(data)) return false;
const uid = process.getuid?.();
if (uid !== undefined && stat.uid !== uid) return false;
// Windows reports 0o666 for any writable file; its mode bits mean nothing.
if (mode !== undefined && process.platform !== "win32" && (stat.mode & 0o777 & ~mode) !== 0) return false;
return readFileSync(fd).equals(Buffer.from(data));
} catch {
return false;
} finally {
if (fd !== null) {
try {
closeSync(fd);
} catch {
}
}
}
}
export function writeFileAtomicIfChanged(path: string, data: string, options: AtomicWriteOptions = {}): boolean {
if (holdsExactly(path, data, options.mode)) return false;
writeFileAtomic(path, data, options);
return true;
}
/** A file only this process writes while it runs: a save whose text equals the last one written (or read at load) is
* skipped without reading the disk back. */
export class OwnedFile {
private last: string | undefined;
readonly path: string;
private readonly options: AtomicWriteOptions;
constructor(path: string, options: AtomicWriteOptions = {}) {
this.path = path;
this.options = options;
}
loaded(text: string): void {
this.last = text;
}
write(text: string): boolean {
if (text === this.last) return false;
writeFileAtomic(this.path, text, this.options);
this.last = text;
return true;
}
}