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; return parsed;
} }
function buildApp({ config }) { function buildApp({ config, initialState = null, onMutation = null }) {
const engine = new XArtAudioEngine({ const engine = new XArtAudioEngine({
creditConfig: config.credit, 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) { function ensureAuth(userId, returnTo) {
if (userId) { if (userId) {
return null; return null;
@@ -92,6 +109,8 @@ function buildApp({ config }) {
}); });
} }
persistMutation();
return json(200, { return json(200, {
status: "completed", status: "completed",
deduped: result.deduped, deduped: result.deduped,
@@ -120,6 +139,7 @@ function buildApp({ config }) {
try { try {
engine.topUpCredits(payload.userId, payload.credits, `polar:${payload.eventId}`); engine.topUpCredits(payload.userId, payload.credits, `polar:${payload.eventId}`);
persistMutation();
return json(200, { status: "credited" }); return json(200, { status: "credited" });
} catch (error) { } catch (error) {
return json(400, { error: error.message }); return json(400, { error: error.message });
@@ -224,6 +244,7 @@ function buildApp({ config }) {
} }
engine.topUpCredits(userId, amount, `app-topup:${userId}:${randomUUID()}`); engine.topUpCredits(userId, amount, `app-topup:${userId}:${randomUUID()}`);
persistMutation();
return redirect(withQuery("/app", { flash: `Added ${amount} credits` })); 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" })); return redirect(withQuery("/app", { flash: "Parent post is not an article" }));
} }
persistMutation();
return redirect(withQuery(`/audio/${result.job.assetId}`, { return redirect(withQuery(`/audio/${result.job.assetId}`, {
flash: "Audiobook generated", flash: "Audiobook generated",
})); }));
@@ -277,6 +299,7 @@ function buildApp({ config }) {
try { try {
engine.unlockAudio(assetId, userId); engine.unlockAudio(assetId, userId);
persistMutation();
return redirect(withQuery(`/audio/${assetId}`, { flash: "Unlocked" })); return redirect(withQuery(`/audio/${assetId}`, { flash: "Unlocked" }));
} catch (error) { } catch (error) {
return redirect(withQuery(`/audio/${assetId}`, { flash: `Unlock failed: ${error.message}` })); 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); const assetId = path.slice("/api/audio/".length, -"/unlock".length);
try { try {
const result = engine.unlockAudio(assetId, userId); const result = engine.unlockAudio(assetId, userId);
persistMutation();
return json(200, result); return json(200, result);
} catch (error) { } catch (error) {
return json(400, { error: error.message }); return json(400, { error: error.message });

View File

@@ -5,7 +5,7 @@ const assert = require("node:assert/strict");
const { buildApp } = require("../src/app"); const { buildApp } = require("../src/app");
const { hmacSHA256Hex } = require("../src/lib/signature"); const { hmacSHA256Hex } = require("../src/lib/signature");
function createApp() { function createApp(options = {}) {
return buildApp({ return buildApp({
config: { config: {
xWebhookSecret: "x-secret", xWebhookSecret: "x-secret",
@@ -18,6 +18,7 @@ function createApp() {
maxCharsPerArticle: 120000, maxCharsPerArticle: 120000,
}, },
}, },
...options,
}); });
} }
@@ -194,3 +195,32 @@ test("X webhook valid flow processes article", () => {
assert.equal(body.status, "completed"); assert.equal(body.status, "completed");
assert.equal(body.creditsCharged, 1); 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");
});