feat(kanban): deterministically rebase snapshot
req: sync/011
This commit is contained in:
@@ -54,6 +54,24 @@ async function removeAcknowledged(database, commandId) {
|
||||
await done;
|
||||
}
|
||||
|
||||
function decideRebase(snapshot, command) {
|
||||
const canonical = snapshot.cards.find((card) => String(card.id) === command.cardId);
|
||||
if (!canonical) return { kind: "conflicted", reason: "card-missing", canonicalColumn: "missing" };
|
||||
if (command.kind === "reorder_card" && canonical.column === "done") {
|
||||
return { kind: "converged", reason: "intent-already-canonical", canonicalColumn: canonical.column };
|
||||
}
|
||||
return { kind: "conflicted", reason: "canonical-state-diverged", canonicalColumn: canonical.column };
|
||||
}
|
||||
|
||||
async function commitConvergedRebase(database, snapshot, command) {
|
||||
const transaction = database.transaction([COMMANDS, "meta"], "readwrite");
|
||||
const done = transactionDone(transaction);
|
||||
transaction.objectStore("meta").put(snapshot, "canonicalSnapshot");
|
||||
transaction.objectStore("meta").put(snapshot.serverSequence, "acknowledgementCursor");
|
||||
transaction.objectStore(COMMANDS).delete(command.id);
|
||||
await done;
|
||||
}
|
||||
|
||||
function setPhase(phase, message) {
|
||||
root.setAttribute("data-sync-phase", phase);
|
||||
root.querySelector('[role="status"]').textContent = message;
|
||||
@@ -146,13 +164,24 @@ async function synchronize(command) {
|
||||
if (!response.ok) throw new Error(`snapshot failed with ${response.status}`);
|
||||
const snapshot = await response.json();
|
||||
const queued = await pendingCommands(database);
|
||||
const decision = decideRebase(snapshot, command);
|
||||
root.setAttribute("data-sync-snapshot-sequence", String(snapshot.serverSequence));
|
||||
root.setAttribute("data-sync-snapshot-schema", String(snapshot.schemaVersion));
|
||||
root.setAttribute("data-sync-snapshot-card-count", String(snapshot.cards.length));
|
||||
root.setAttribute("data-sync-rebase-pending-count", String(queued.length));
|
||||
root.setAttribute("data-sync-canonical-column", snapshot.cards.find((card) => String(card.id) === command.cardId)?.column || "missing");
|
||||
setPhase("rebase-required", `History is unavailable; loaded snapshot ${snapshot.serverSequence} and preserved ${queued.length} pending command for rebase.`);
|
||||
root.dispatchEvent(new CustomEvent("kanban:sync-rebase-required", { detail: { snapshot, pending: queued } }));
|
||||
root.setAttribute("data-sync-rebase-decision", decision.kind);
|
||||
root.setAttribute("data-sync-rebase-reason", decision.reason);
|
||||
root.setAttribute("data-sync-canonical-column", decision.canonicalColumn);
|
||||
if (decision.kind === "converged") {
|
||||
await commitConvergedRebase(database, snapshot, command);
|
||||
root.setAttribute("data-sync-pending-count", String((await pendingCommands(database)).length));
|
||||
root.setAttribute("data-sync-ack-sequence", String(snapshot.serverSequence));
|
||||
setPhase("rebased", `Canonical snapshot ${snapshot.serverSequence} already satisfies ${command.id}; committed and removed the pending command.`);
|
||||
root.dispatchEvent(new CustomEvent("kanban:sync-rebased", { detail: { snapshot, command, decision } }));
|
||||
} else {
|
||||
setPhase("conflicted", `Canonical snapshot ${snapshot.serverSequence} conflicts with ${command.id} (${decision.reason}); the pending command remains queued.`);
|
||||
root.dispatchEvent(new CustomEvent("kanban:sync-conflicted", { detail: { snapshot, command, decision } }));
|
||||
}
|
||||
source.close();
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user