81 lines
2.0 KiB
TypeScript
81 lines
2.0 KiB
TypeScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import lockfile from "proper-lockfile";
|
|
|
|
const STORE_LOCK_OPTIONS = {
|
|
retries: {
|
|
retries: 10,
|
|
factor: 2,
|
|
minTimeout: 100,
|
|
maxTimeout: 10_000,
|
|
randomize: true,
|
|
},
|
|
stale: 30_000,
|
|
} as const;
|
|
|
|
function safeParseJson<T>(raw: string): T | null {
|
|
try {
|
|
return JSON.parse(raw) as T;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function readJsonFile<T>(
|
|
filePath: string,
|
|
fallback: T,
|
|
): Promise<{ value: T; exists: boolean }> {
|
|
try {
|
|
const raw = await fs.promises.readFile(filePath, "utf-8");
|
|
const parsed = safeParseJson<T>(raw);
|
|
if (parsed == null) return { value: fallback, exists: true };
|
|
return { value: parsed, exists: true };
|
|
} catch (err) {
|
|
const code = (err as { code?: string }).code;
|
|
if (code === "ENOENT") return { value: fallback, exists: false };
|
|
return { value: fallback, exists: false };
|
|
}
|
|
}
|
|
|
|
export async function writeJsonFile(filePath: string, value: unknown): Promise<void> {
|
|
const dir = path.dirname(filePath);
|
|
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
|
|
await fs.promises.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
|
|
encoding: "utf-8",
|
|
});
|
|
await fs.promises.chmod(tmp, 0o600);
|
|
await fs.promises.rename(tmp, filePath);
|
|
}
|
|
|
|
async function ensureJsonFile(filePath: string, fallback: unknown) {
|
|
try {
|
|
await fs.promises.access(filePath);
|
|
} catch {
|
|
await writeJsonFile(filePath, fallback);
|
|
}
|
|
}
|
|
|
|
export async function withFileLock<T>(
|
|
filePath: string,
|
|
fallback: unknown,
|
|
fn: () => Promise<T>,
|
|
): Promise<T> {
|
|
await ensureJsonFile(filePath, fallback);
|
|
let release: (() => Promise<void>) | undefined;
|
|
try {
|
|
release = await lockfile.lock(filePath, STORE_LOCK_OPTIONS);
|
|
return await fn();
|
|
} finally {
|
|
if (release) {
|
|
try {
|
|
await release();
|
|
} catch {
|
|
// ignore unlock errors
|
|
}
|
|
}
|
|
}
|
|
}
|