feat: add atomic JSON file state persistence for production runtime
This commit is contained in:
37
src/lib/state-store.js
Normal file
37
src/lib/state-store.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const fs = require("node:fs/promises");
|
||||||
|
const path = require("node:path");
|
||||||
|
|
||||||
|
class JsonFileStateStore {
|
||||||
|
constructor(filePath) {
|
||||||
|
this.filePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
const raw = await fs.readFile(this.filePath, "utf8");
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (error) {
|
||||||
|
if (error && error.code === "ENOENT") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(state) {
|
||||||
|
const dir = path.dirname(this.filePath);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
|
||||||
|
const tempPath = `${this.filePath}.tmp`;
|
||||||
|
const payload = JSON.stringify(state, null, 2);
|
||||||
|
|
||||||
|
await fs.writeFile(tempPath, payload, "utf8");
|
||||||
|
await fs.rename(tempPath, this.filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
JsonFileStateStore,
|
||||||
|
};
|
||||||
35
test/state-store.test.js
Normal file
35
test/state-store.test.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert/strict");
|
||||||
|
const os = require("node:os");
|
||||||
|
const path = require("node:path");
|
||||||
|
const fs = require("node:fs/promises");
|
||||||
|
const { JsonFileStateStore } = require("../src/lib/state-store");
|
||||||
|
|
||||||
|
test("load returns null when state file does not exist", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "xartaudio-state-test-"));
|
||||||
|
const store = new JsonFileStateStore(path.join(tempDir, "missing.json"));
|
||||||
|
|
||||||
|
const data = await store.load();
|
||||||
|
assert.equal(data, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save and load roundtrip state", async () => {
|
||||||
|
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "xartaudio-state-test-"));
|
||||||
|
const filePath = path.join(tempDir, "state.json");
|
||||||
|
const store = new JsonFileStateStore(filePath);
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
version: 1,
|
||||||
|
engine: {
|
||||||
|
jobs: { "1": { id: "1", status: "completed" } },
|
||||||
|
nextJobId: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await store.save(expected);
|
||||||
|
const actual = await store.load();
|
||||||
|
|
||||||
|
assert.deepEqual(actual, expected);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user