fix: Preserve offline error state

This commit is contained in:
Chmouel Boudjnah 2026-09-07 23:52:13 +02:00
parent 461deae9ff
commit bcbf0b382e
3 changed files with 34 additions and 5 deletions

View file

@ -58,10 +58,12 @@ if (buttons.length) {
button.disabled = false;
const controller = new AbortController();
active = controller;
let operationAccount = "";
button.textContent = "Cancel download";
say(button, "Preparing offline copy…");
try {
const identity = await auth.acquire();
operationAccount = identity.account;
const partition = storagePartition();
const context = await accountContext(partition, identity.account);
const authorize = async current => {
@ -119,9 +121,21 @@ if (buttons.length) {
}
} catch (error) {
if (error.name === "AbortError") say(button, "Download cancelled");
else if (error.code === "quota") say(button, error.message, true);
else say(button, error.message || "Offline download failed", true);
button.textContent = "Save offline";
else {
const message = error.code === "quota"
? error.message : error.message || "Offline download failed";
let restored = false;
if (operationAccount) {
try {
await updateReadyState(button, operationAccount);
restored = true;
} catch (stateError) {
console.warn("offline download state could not be restored", stateError);
}
}
if (!restored) button.textContent = "Save offline";
say(button, message, true);
}
} finally {
active = null;
}

View file

@ -24,7 +24,9 @@ export async function drainOfflineOutbox(context, request) {
responseCurrent: () => true,
accepted: done,
refused: (_, code) => failed("failed", code || "session rejected"),
deferred: () => {},
deferred: (status, code) => failed(
"pending", code || `HTTP ${status || "unknown"}`,
),
});
continue;
}
@ -49,11 +51,15 @@ export async function drainOfflineOutbox(context, request) {
if (response.status === 409) await failed("conflict", "revision conflict", body?.server);
else if ([400, 403, 404, 413, 422].includes(response.status))
await failed("failed", body?.error || `HTTP ${response.status}`);
else await failed("pending", body?.error || `HTTP ${response.status}`);
continue;
}
const result = body?.results?.find(value => (annotation ? value.id : value.op_id) ===
(annotation ? record.annotationID : record.id));
if (!result) continue;
if (!result) {
await failed("pending", "missing acknowledgement");
continue;
}
if (["applied", "duplicate"].includes(result.status)) {
if (annotation) {
const server = { ...record.payload.annotation, rev: result.rev, seq: result.seq };

View file

@ -188,6 +188,15 @@ async function storageChecks() {
"permanent annotation error does not stop sessions or other annotations");
await rejected(() => s.finishOfflineSession({ ...context, bookID, payload: { ...payload, active_ms: 2000 } }),
"delivered session retains immutable identity");
await queue("retryable", "retryable", "retryable");
await drainOfflineOutbox(context, async () => reply({ error: "temporary failure" }, "", 500));
const retryable = (await pending()).find(row => row.id === "retryable");
check(retryable?.state === "pending" && retryable.error === "temporary failure",
"unexpected server errors stay retryable with an error");
await drainOfflineOutbox(context, async () =>
reply({ results: [{ id: "retryable", status: "applied", rev: 1 }] }));
check(!(await pending()).some(row => row.id === "retryable"),
"retryable outbox records recover after a later success");
await s.saveOfflineSessionCheckpoint({ ...context, bookID, checkpoint: { id: "session" } });
check(!await s.getOfflineSessionCheckpoint({ ...context, bookID }), "late checkpoint cannot recreate finalized session");
await s.saveOfflineSessionCheckpoint({ ...context, bookID, checkpoint: { id: "next-session" } });