feat(kanban): bound sync upload runs
req: sync/017
This commit is contained in:
@@ -8,9 +8,14 @@ const LEASE_MS = 5000;
|
||||
const LEASE_POLL_MS = 100;
|
||||
const LEASE_KEY = "uploaderLease";
|
||||
let database;
|
||||
let uploadLimit;
|
||||
let retryTimer;
|
||||
let leaseTimer;
|
||||
let synchronizing = false;
|
||||
let uploadsThisRun = 0;
|
||||
let uploadedTotal = 0;
|
||||
let inFlightUploads = 0;
|
||||
let maxObservedInFlight = 0;
|
||||
let stopped = false;
|
||||
|
||||
class UploadError extends Error {
|
||||
@@ -168,6 +173,30 @@ async function upload(command) {
|
||||
throw new Error("sync retry limit exhausted");
|
||||
}
|
||||
|
||||
function beginUpload() {
|
||||
inFlightUploads += 1;
|
||||
maxObservedInFlight = Math.max(maxObservedInFlight, inFlightUploads);
|
||||
root.setAttribute("data-sync-in-flight", String(inFlightUploads));
|
||||
root.setAttribute("data-sync-max-observed-in-flight", String(maxObservedInFlight));
|
||||
}
|
||||
|
||||
function finishUpload() {
|
||||
inFlightUploads -= 1;
|
||||
root.setAttribute("data-sync-in-flight", String(inFlightUploads));
|
||||
}
|
||||
|
||||
async function continuePendingWork() {
|
||||
const commands = await pendingCommands(database);
|
||||
root.setAttribute("data-sync-pending-count", String(commands.length));
|
||||
if (commands.length === 0) return;
|
||||
if (uploadsThisRun >= uploadLimit) {
|
||||
root.setAttribute("data-sync-manual-retry", "available");
|
||||
setPhase("backpressured", `Upload limit ${uploadLimit} reached; ${commands.length} durable command${commands.length === 1 ? " remains" : "s remain"} queued. Retry now to continue.`);
|
||||
return;
|
||||
}
|
||||
setTimeout(() => synchronize(validatePending(commands[0])).catch(failPermanently), 0);
|
||||
}
|
||||
|
||||
async function synchronize(command) {
|
||||
if (synchronizing) return;
|
||||
synchronizing = true;
|
||||
@@ -188,7 +217,13 @@ async function synchronize(command) {
|
||||
root.removeAttribute("data-sync-manual-retry");
|
||||
setOnline(navigator.onLine);
|
||||
try {
|
||||
const acknowledgement = await upload(command);
|
||||
beginUpload();
|
||||
let acknowledgement;
|
||||
try {
|
||||
acknowledgement = await upload(command);
|
||||
} finally {
|
||||
finishUpload();
|
||||
}
|
||||
setOnline(true);
|
||||
root.setAttribute("data-sync-upload-sequence", String(acknowledgement.serverSequence));
|
||||
setPhase("awaiting-ack", `Command ${command.id} uploaded; awaiting canonical acknowledgement.`);
|
||||
@@ -203,8 +238,13 @@ async function synchronize(command) {
|
||||
source.addEventListener("acknowledgement", async (event) => {
|
||||
const canonical = JSON.parse(event.data);
|
||||
if (canonical.commandId !== command.id) return;
|
||||
source.close();
|
||||
root.setAttribute("data-sync-pending-before-ack", String((await pendingCommands(database)).length));
|
||||
await removeAcknowledged(database, command.id);
|
||||
uploadsThisRun += 1;
|
||||
uploadedTotal += 1;
|
||||
root.setAttribute("data-sync-uploaded-this-run", String(uploadsThisRun));
|
||||
root.setAttribute("data-sync-uploaded-total", String(uploadedTotal));
|
||||
root.setAttribute("data-sync-pending-count", String((await pendingCommands(database)).length));
|
||||
root.setAttribute("data-sync-ack-sequence", String(canonical.serverSequence));
|
||||
root.setAttribute("data-sync-canonical-column", canonical.canonicalColumn);
|
||||
@@ -214,7 +254,7 @@ async function synchronize(command) {
|
||||
clearTimeout(leaseTimer);
|
||||
await releaseUploaderLease(database);
|
||||
root.setAttribute("data-sync-leader", "false");
|
||||
source.close();
|
||||
await continuePendingWork();
|
||||
});
|
||||
source.addEventListener("snapshot-required", async (event) => {
|
||||
const missing = JSON.parse(event.data);
|
||||
@@ -271,20 +311,32 @@ async function runLeaseLoop(command) {
|
||||
|
||||
async function start() {
|
||||
if (!root) return;
|
||||
uploadLimit = Number.parseInt(root.getAttribute("data-sync-upload-limit"), 10);
|
||||
if (!Number.isSafeInteger(uploadLimit) || uploadLimit < 1) throw new Error("data-sync-upload-limit must be a positive integer");
|
||||
database = await openLog();
|
||||
const commands = await pendingCommands(database);
|
||||
root.setAttribute("data-sync-uploaded-this-run", "0");
|
||||
root.setAttribute("data-sync-uploaded-total", "0");
|
||||
root.setAttribute("data-sync-in-flight", "0");
|
||||
root.setAttribute("data-sync-max-observed-in-flight", "0");
|
||||
root.setAttribute("data-sync-pending-count", String(commands.length));
|
||||
if (commands.length === 0) {
|
||||
setPhase("idle", "No pending commands.");
|
||||
return;
|
||||
}
|
||||
const command = validatePending(commands[0]);
|
||||
root.addEventListener("click", (event) => {
|
||||
root.addEventListener("click", async (event) => {
|
||||
if (!event.target.closest("[data-sync-retry]")) return;
|
||||
synchronize(command).catch(failPermanently);
|
||||
uploadsThisRun = 0;
|
||||
root.setAttribute("data-sync-uploaded-this-run", "0");
|
||||
root.removeAttribute("data-sync-manual-retry");
|
||||
const [next] = await pendingCommands(database);
|
||||
if (next) synchronize(validatePending(next)).catch(failPermanently);
|
||||
});
|
||||
window.addEventListener("online", () => {
|
||||
if (root.getAttribute("data-sync-phase") === "offline") synchronize(command).catch(failPermanently);
|
||||
window.addEventListener("online", async () => {
|
||||
if (root.getAttribute("data-sync-phase") !== "offline") return;
|
||||
const [next] = await pendingCommands(database);
|
||||
if (next) synchronize(validatePending(next)).catch(failPermanently);
|
||||
});
|
||||
await runLeaseLoop(command);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user