feat(app): define trusted rewind branch flow

This commit is contained in:
Codex
2026-07-28 03:23:57 -04:00
parent cf9507a9dd
commit 497c127e87
15 changed files with 671 additions and 26 deletions
+47 -1
View File
@@ -1,4 +1,11 @@
import type { PlayerView, PresentationBeat, TurnRequest, TurnResult } from "@contracts";
import type {
ForkBranchRequest,
ForkBranchResult,
PlayerView,
PresentationBeat,
TurnRequest,
TurnResult
} from "@contracts";
const DEMO_LATENCY_MS = 120;
@@ -95,3 +102,42 @@ export async function submitDemoTurn(
}
};
}
/**
* Browser-only rewind preview. The desktop path performs the durable fork in
* SQLite; this adapter mirrors the same DTO boundary without claiming local
* persistence.
*/
export async function forkDemoBranch(
request: ForkBranchRequest,
currentView: PlayerView
): Promise<ForkBranchResult> {
await new Promise((resolve) => window.setTimeout(resolve, DEMO_LATENCY_MS));
if (
request.storyId !== currentView.storyId ||
request.currentBranchId !== currentView.branchId ||
request.expectedCurrentNodeId !== currentView.nodeId
) {
throw new Error("story branch changed; refresh and retry");
}
const sourceIndex = currentView.history.findIndex((node) => node.id === request.sourceNodeId);
if (sourceIndex < 0) throw new Error("selected story node is unavailable");
const branchId = `branch_${request.actionId.replaceAll(/[^a-zA-Z0-9_.-]/g, "_")}`;
const history = currentView.history.slice(0, sourceIndex + 1).map((node) => ({
...node,
isCurrent: node.id === request.sourceNodeId
}));
return {
branchId,
playerView: {
...currentView,
nodeId: request.sourceNodeId,
branchId,
history
}
};
}