feat: emit app state snapshots on all mutating workflows
This commit is contained in:
26
src/app.js
26
src/app.js
@@ -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 });
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user