feat: restore/persist app state in server runtime with queued writes

This commit is contained in:
Codex
2026-02-18 13:00:51 +00:00
parent fc7ded3f38
commit 6bd238ad0e
2 changed files with 91 additions and 4 deletions

View File

@@ -3,6 +3,7 @@
const http = require("node:http");
const { buildApp } = require("./app");
const { config } = require("./config");
const { JsonFileStateStore } = require("./lib/state-store");
function readBody(req) {
return new Promise((resolve, reject) => {
@@ -59,10 +60,72 @@ function createHttpServer({ app }) {
});
}
function start() {
const app = buildApp({ config });
function createMutationPersister({ stateStore, logger = console }) {
let queue = Promise.resolve();
return {
enqueue(state) {
queue = queue
.then(() => stateStore.save(state))
.catch((error) => {
logger.error("failed to persist state", error);
});
return queue;
},
flush() {
return queue;
},
};
}
async function createRuntime({ runtimeConfig = config, logger = console } = {}) {
const stateStore = new JsonFileStateStore(runtimeConfig.stateFilePath);
const initialState = await stateStore.load();
const persister = createMutationPersister({ stateStore, logger });
const app = buildApp({
config: runtimeConfig,
initialState,
onMutation(state) {
void persister.enqueue(state);
},
});
const server = createHttpServer({ app });
return {
app,
server,
persister,
};
}
async function start() {
const runtime = await createRuntime({ runtimeConfig: config });
const { server, persister } = runtime;
let shuttingDown = false;
async function shutdown(signal) {
if (shuttingDown) {
return;
}
shuttingDown = true;
// eslint-disable-next-line no-console
console.log(`received ${signal}, shutting down`);
server.close();
await persister.flush();
}
process.on("SIGTERM", () => {
void shutdown("SIGTERM");
});
process.on("SIGINT", () => {
void shutdown("SIGINT");
});
server.listen(config.port, () => {
// eslint-disable-next-line no-console
console.log(`xartaudio server listening on :${config.port}`);
@@ -70,12 +133,18 @@ function start() {
}
if (require.main === module) {
start();
start().catch((error) => {
// eslint-disable-next-line no-console
console.error("failed to start server", error);
process.exitCode = 1;
});
}
module.exports = {
mapToAppRequest,
normalizeHeaders,
createHttpServer,
createMutationPersister,
createRuntime,
start,
};

View File

@@ -2,7 +2,7 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { mapToAppRequest, normalizeHeaders } = require("../src/server");
const { mapToAppRequest, normalizeHeaders, createMutationPersister } = require("../src/server");
test("normalizeHeaders lowercases and joins array values", () => {
const headers = normalizeHeaders({
@@ -31,3 +31,21 @@ test("mapToAppRequest extracts method/path/headers/body correctly", () => {
assert.equal(request.headers["x-signature"], "sha256=abc");
assert.equal(request.rawBody, "{\"ok\":true}");
});
test("createMutationPersister writes sequentially and flush waits", async () => {
const saved = [];
const stateStore = {
async save(state) {
await new Promise((resolve) => setTimeout(resolve, 5));
saved.push(state.id);
},
};
const persister = createMutationPersister({ stateStore, logger: { error() {} } });
persister.enqueue({ id: "s1" });
persister.enqueue({ id: "s2" });
await persister.flush();
assert.deepEqual(saved, ["s1", "s2"]);
});