feat: emit app state snapshots on all mutating workflows

This commit is contained in:
Codex
2026-02-18 12:59:15 +00:00
parent 0c2124e4cf
commit c714c2eaec
2 changed files with 56 additions and 2 deletions

View File

@@ -48,11 +48,28 @@ function parsePositiveInt(raw, fallback = null) {
return parsed;
}
function buildApp({ config }) {
function buildApp({ config, initialState = null, onMutation = null }) {
const engine = new XArtAudioEngine({
creditConfig: config.credit,
initialState: initialState && initialState.engine ? initialState.engine : null,
});
function persistMutation() {
if (!onMutation) {
return;
}
try {
onMutation({
version: 1,
updatedAt: new Date().toISOString(),
engine: engine.exportState(),
});
} catch {
// avoid breaking request flow on persistence callback issues
}
}
function ensureAuth(userId, returnTo) {
if (userId) {
return null;
@@ -92,6 +109,8 @@ function buildApp({ config }) {
});
}
persistMutation();
return json(200, {
status: "completed",
deduped: result.deduped,
@@ -120,6 +139,7 @@ function buildApp({ config }) {
try {
engine.topUpCredits(payload.userId, payload.credits, `polar:${payload.eventId}`);
persistMutation();
return json(200, { status: "credited" });
} catch (error) {
return json(400, { error: error.message });
@@ -224,6 +244,7 @@ function buildApp({ config }) {
}
engine.topUpCredits(userId, amount, `app-topup:${userId}:${randomUUID()}`);
persistMutation();
return redirect(withQuery("/app", { flash: `Added ${amount} credits` }));
}
@@ -260,6 +281,7 @@ function buildApp({ config }) {
return redirect(withQuery("/app", { flash: "Parent post is not an article" }));
}
persistMutation();
return redirect(withQuery(`/audio/${result.job.assetId}`, {
flash: "Audiobook generated",
}));
@@ -277,6 +299,7 @@ function buildApp({ config }) {
try {
engine.unlockAudio(assetId, userId);
persistMutation();
return redirect(withQuery(`/audio/${assetId}`, { flash: "Unlocked" }));
} catch (error) {
return redirect(withQuery(`/audio/${assetId}`, { flash: `Unlock failed: ${error.message}` }));
@@ -330,6 +353,7 @@ function buildApp({ config }) {
const assetId = path.slice("/api/audio/".length, -"/unlock".length);
try {
const result = engine.unlockAudio(assetId, userId);
persistMutation();
return json(200, result);
} catch (error) {
return json(400, { error: error.message });

View File

@@ -5,7 +5,7 @@ const assert = require("node:assert/strict");
const { buildApp } = require("../src/app");
const { hmacSHA256Hex } = require("../src/lib/signature");
function createApp() {
function createApp(options = {}) {
return buildApp({
config: {
xWebhookSecret: "x-secret",
@@ -18,6 +18,7 @@ function createApp() {
maxCharsPerArticle: 120000,
},
},
...options,
});
}
@@ -194,3 +195,32 @@ test("X webhook valid flow processes article", () => {
assert.equal(body.status, "completed");
assert.equal(body.creditsCharged, 1);
});
test("emits persistence snapshots on mutating actions", () => {
const snapshots = [];
const app = createApp({
onMutation(state) {
snapshots.push(state);
},
});
call(app, {
method: "POST",
path: "/app/actions/topup",
headers: { cookie: "xartaudio_user=alice" },
body: "amount=5",
});
call(app, {
method: "POST",
path: "/app/actions/simulate-mention",
headers: { cookie: "xartaudio_user=alice" },
body: "title=Persisted&body=hello",
});
assert.equal(snapshots.length >= 2, true);
const latest = snapshots[snapshots.length - 1];
assert.equal(latest.version, 1);
assert.equal(typeof latest.updatedAt, "string");
assert.equal(typeof latest.engine, "object");
});