feat(kanban): migrate queued commands transactionally

req: sync/014
This commit is contained in:
slhx agent
2026-07-13 18:06:47 +02:00
parent 3ace17f9f1
commit a17de85b4f
5 changed files with 251 additions and 38 deletions
+31 -7
View File
@@ -1,7 +1,10 @@
const DATABASE = "hemx-kanban-v1";
const DATABASE_VERSION = 2;
const COMMANDS = "commands";
const META = "meta";
const COMMAND_SCHEMA = 1;
const COMMAND_SCHEMA = 2;
const LEGACY_COMMAND_SCHEMA = 1;
const MIGRATION_KEY = "commandSchemaMigration";
const EXPORT_SCHEMA = 1;
const MAX_REPLAY_COMMANDS = 64;
const REPLAY_BUDGET_MS = 100;
@@ -23,13 +26,31 @@ function completed(transaction) {
});
}
function migrateCommandLog(request, oldVersion) {
const database = request.result;
if (!database.objectStoreNames.contains(COMMANDS)) database.createObjectStore(COMMANDS, { keyPath: "id" });
if (!database.objectStoreNames.contains(META)) database.createObjectStore(META);
if (oldVersion === 0 || oldVersion >= DATABASE_VERSION) return;
const transaction = request.transaction;
const commands = transaction.objectStore(COMMANDS);
const meta = transaction.objectStore(META);
const all = commands.getAll();
all.addEventListener("success", () => {
const legacy = all.result;
if (legacy.some((command) => command.schemaVersion !== LEGACY_COMMAND_SCHEMA)) {
transaction.abort();
return;
}
for (const command of legacy) {
commands.put({ ...command, schemaVersion: COMMAND_SCHEMA, targetColumn: "done" });
}
meta.put({ from: LEGACY_COMMAND_SCHEMA, to: COMMAND_SCHEMA, migrated: legacy.length }, MIGRATION_KEY);
}, { once: true });
}
function openCommandLog() {
const request = indexedDB.open(DATABASE, 1);
request.addEventListener("upgradeneeded", () => {
const database = request.result;
if (!database.objectStoreNames.contains(COMMANDS)) database.createObjectStore(COMMANDS, { keyPath: "id" });
if (!database.objectStoreNames.contains(META)) database.createObjectStore(META);
});
const request = indexedDB.open(DATABASE, DATABASE_VERSION);
request.addEventListener("upgradeneeded", (event) => migrateCommandLog(request, event.oldVersion));
return result(request);
}
@@ -86,6 +107,7 @@ async function appendReorder(database, wire) {
causal,
kind: "reorder_card",
cardId: String(wire[2] || "1"),
targetColumn: "done",
eventKind: String(wire[1] || "click"),
key: wire[4] ? String(wire[4]) : null,
};
@@ -145,6 +167,7 @@ function validate(command) {
if (command.id !== `${command.actor}:${command.causal}`) invalidCommand(command, "id");
if (command.kind !== "reorder_card") invalidCommand(command, "kind");
if (typeof command.cardId !== "string" || !command.cardId) invalidCommand(command, "cardId");
if (command.targetColumn !== "done") invalidCommand(command, "targetColumn");
if (typeof command.eventKind !== "string" || !command.eventKind) invalidCommand(command, "eventKind");
if (command.key !== null && typeof command.key !== "string") invalidCommand(command, "key");
return command;
@@ -291,6 +314,7 @@ async function start() {
actor: command.actor,
session: command.session,
causal: command.causal,
targetColumn: command.targetColumn,
},
}));
try {