87 lines
2.8 KiB
JavaScript
87 lines
2.8 KiB
JavaScript
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);
|
|
}
|
|
}
|