import { rename, writeFile } from 'node:fs/promises'; import { basename, dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; // Returns a temporary file // Example: for /some/file will return /some/.file.tmp function getTempFilename(file) { const f = file instanceof URL ? fileURLToPath(file) : file.toString(); return join(dirname(f), `.${basename(f)}.tmp`); } // Retries an asynchronous operation with a delay between retries and a maximum retry count async function retryAsyncOperation(fn, maxRetries, delayMs) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i < maxRetries - 1) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } else { throw error; // Rethrow the error if max retries reached } } } } export class Writer { #filename; #tempFilename; #locked = false; #prev = null; #next = null; #nextPromise = null; #nextData = null; // File is locked, add data for later #add(data) { // Only keep most recent data this.#nextData = data; // Create a singleton promise to resolve all next promises once next data is written this.#nextPromise ||= new Promise((resolve, reject) => { this.#next = [resolve, reject]; }); // Return a promise that will resolve at the same time as next promise return new Promise((resolve, reject) => { this.#nextPromise?.then(resolve).catch(reject); }); } // File isn't locked, write data async #write(data) { // Lock file this.#locked = true; try { // Atomic write await writeFile(this.#tempFilename, data, 'utf-8'); await retryAsyncOperation(async () => { await rename(this.#tempFilename, this.#filename); }, 10, 100); // Call resolve this.#prev?.[0](); } catch (err) { // Call reject if (err instanceof Error) { this.#prev?.[1](err); } throw err; } finally { // Unlock file this.#locked = false; this.#prev = this.#next; this.#next = this.#nextPromise = null; if (this.#nextData !== null) { const nextData = this.#nextData; this.#nextData = null; await this.write(nextData); } } } constructor(filename) { this.#filename = filename; this.#tempFilename = getTempFilename(filename); } async write(data) { return this.#locked ? this.#add(data) : this.#write(data); } }