feat(kanban): bound durable replay

req: sync/014\nreq: performance/005
This commit is contained in:
slhx agent
2026-07-13 15:20:00 +02:00
parent 54b706b706
commit bf10d1e3ce
3 changed files with 174 additions and 3 deletions
+17
View File
@@ -3,6 +3,8 @@ const COMMANDS = "commands";
const META = "meta";
const COMMAND_SCHEMA = 1;
const EXPORT_SCHEMA = 1;
const MAX_REPLAY_COMMANDS = 64;
const REPLAY_BUDGET_MS = 100;
const SESSION = "hemx-kanban-session-v1";
const ROOT = '[data-hemx-root][data-hemx-client-module="/kanban_client.js"]';
@@ -110,6 +112,13 @@ async function storedCommands(database) {
return commands.sort((left, right) => left.causal - right.causal);
}
class ReplayLimitError extends Error {
constructor(actual) {
super(`durable replay limit exceeded: ${actual} > ${MAX_REPLAY_COMMANDS}`);
this.name = "ReplayLimitError";
}
}
function invalidCommand(command, field) {
const id = command && typeof command.id === "string" && command.id ? command.id : "record";
throw new Error(`invalid durable command ${id}: ${field}`);
@@ -279,9 +288,17 @@ async function start() {
if (typeof wasmHandler !== "function") throw new Error("reorder_card WASM handler is not registered");
const database = await databasePromise;
installRecoveryControls(root, database);
root.setAttribute("data-kanban-replay-limit", String(MAX_REPLAY_COMMANDS));
try {
const commands = await storedCommands(database);
if (commands.length > MAX_REPLAY_COMMANDS) throw new ReplayLimitError(commands.length);
commands.forEach(validate);
const replayStarted = performance.now();
for (const command of commands) window.hemx.applyBatch(await project(root, wasmHandler, command), root);
const replayMs = performance.now() - replayStarted;
root.setAttribute("data-kanban-replay-ms", replayMs.toFixed(3));
root.setAttribute("data-kanban-replay-budget-ms", String(REPLAY_BUDGET_MS));
root.toggleAttribute("data-kanban-replay-over-budget", replayMs > REPLAY_BUDGET_MS);
root.setAttribute("data-kanban-command-count", String(commands.length));
root.setAttribute("data-kanban-command-ready", "");
await offlineReady;