@aurios/jason
Version:
A simple, lightweight, and embeddable JSON document database built on Bun.
103 lines (102 loc) • 3.54 kB
JavaScript
import { rename, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { retryAsyncOperation } from "../utils/utils.js";
function randomString() {
return `${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
}
function getTempPath(path) {
return join(path, `$.tmp_${randomString()}`);
}
function getFilePath(basePath, fileName) {
return join(basePath, `${fileName}.json`);
}
export default class Writer {
#basePath;
#queue = new Map();
/**
* Constructs a new Writer instance.
* @param basePath The path to the file to write to.
*/
constructor(basePath) {
this.#basePath = basePath;
}
async #atomicWrite(tempPath, filePath, data) {
await writeFile(tempPath, data, "utf-8");
await retryAsyncOperation(() => rename(tempPath, filePath));
}
async #write(filename, data) {
const queue = this.#queue;
// biome-ignore lint/style/noNonNullAssertion: ignored for now
const state = queue.get(filename);
state.locked = true;
const filePath = getFilePath(this.#basePath, filename);
const tempPath = getTempPath(this.#basePath);
try {
await this.#atomicWrite(tempPath, filePath, data);
state.nextResolve?.();
return true;
}
catch (error) {
state.nextReject?.(error);
throw error;
}
finally {
state.locked = false;
if (state.nextData !== null) {
const nextData = state.nextData;
state.nextData = null;
await this.write(filename, nextData);
}
else {
state.nextPromise = null;
state.nextResolve = null;
state.nextReject = null;
queue.delete(filename);
}
}
}
/**
* Writes data to a file with the given filename.
*
* If the file is currently being written to, the data is queued to be written
* once the current write operation completes. Ensures that writes are
* performed atomically and handles concurrent write requests by queuing them.
*
* @param fileName - The name of the file to write to.
* @param data - The data to be written to the file.
* @returns A promise that resolves to true when the write operation is complete.
*/
async write(fileName, data) {
const queue = this.#queue;
if (!queue.has(fileName)) {
queue.set(fileName, {
locked: false,
nextData: null,
nextPromise: null,
nextResolve: null,
nextReject: null,
});
}
const state = queue.get(fileName);
if (!state) {
throw new Error(`File state not found for ${fileName}`);
}
if (!state.locked) {
return this.#write(fileName, data);
}
if (state.nextPromise) {
state.nextData = data;
return new Promise((resolve, reject) => {
state.nextPromise?.then(() => resolve(true)).catch(reject);
});
}
state.nextData = data;
state.nextPromise = new Promise((resolve, reject) => {
state.nextResolve = resolve;
state.nextReject = reject;
});
return new Promise((resolve, reject) => {
state.nextPromise?.then(() => resolve(true)).catch(reject);
});
}
}