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;
|
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 });
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user