feat(userscript): route every write through the retry queue

This commit is contained in:
2026-07-27 17:08:24 +07:00
parent bbe04b1e81
commit 0615617ebe
+50 -29
View File
@@ -444,6 +444,45 @@
// Mutations (optimistic: update UI/cache first, then sync)
// ============================================================
// Every write goes through here, queued or not, so there is one place that
// talks to the API and one place that decides `sendStatus`. That flag is
// sticky: while an archive is pending for a key, a later progress write to the
// same key still carries the bucket — otherwise the server would answer with
// the old status and the adopted row would silently un-archive the series.
async function pushBookmark(key, sendStatus) {
const pending = queueGet(key);
const withStatus = sendStatus || (pending ? pending.sendStatus : false);
const bm = state.byKey[key];
if (!bm) {
queueDrop(key); // removed locally in the meantime — nothing left to send
return true;
}
try {
// ponytail: last-write-wins, so a replay can overwrite a newer server
// value (a poller-written latest_chapter, or progress from another
// device). Single user, self-healing on the next poll — revisit only if
// this ever runs multi-user.
upsertLocal(await apiPut(key, bm, { sendStatus: withStatus }));
queueDrop(key);
render();
return true;
} catch (e) {
queueEnqueue(key, "put", withStatus, e);
return false;
}
}
async function pushDelete(key) {
try {
await apiDelete(key);
queueDrop(key);
return true;
} catch (e) {
queueEnqueue(key, "delete", false, e);
return false;
}
}
async function bookmarkCurrent() {
const p = state.page;
if (p.type !== "series" && p.type !== "chapter") return;
@@ -502,14 +541,8 @@
async function syncUpsert(bm, okMsg) {
upsertLocal(bm); // optimistic
render();
try {
const saved = await apiPut(bm.key, bm);
upsertLocal(saved); // adopt server updated_at
render();
toast(okMsg);
} catch (e) {
toast("Offline — saved locally, will retry", true);
}
if (await pushBookmark(bm.key, false)) toast(okMsg);
else toast("Saved — will sync when online", true);
}
async function toggleFavorite(key) {
@@ -521,13 +554,7 @@
});
upsertLocal(bm); // optimistic
render();
try {
const saved = await apiPut(bm.key, bm);
upsertLocal(saved);
render();
} catch (e) {
toast("Offline — saved locally, will retry", true);
}
if (!(await pushBookmark(bm.key, false))) toast("Saved — will sync when online", true);
}
// Archive parks a series: it leaves All and Favourites but the server keeps
@@ -537,16 +564,14 @@
const existing = state.byKey[key];
if (!existing) return;
const next = statusOf(existing) === "archived" ? "reading" : "archived";
const bm = Object.assign({}, existing, { status: next });
upsertLocal(bm);
upsertLocal(Object.assign({}, existing, { status: next }));
render();
try {
const saved = await apiPut(bm.key, bm, { sendStatus: true });
upsertLocal(saved); // adopt the stored row: the server owns updated_at
render();
// The only write with an opinion about the bucket, so the only one that
// sends `status` at all.
if (await pushBookmark(key, true)) {
toast(next === "archived" ? "Archived" : "Back in your list");
} catch (e) {
toast("Archive failed — retry when online", true);
} else {
toast((next === "archived" ? "Archived" : "Restored") + " — will sync when online", true);
}
}
@@ -623,12 +648,8 @@
async function removeBookmark(key) {
removeLocal(key); // optimistic
render();
try {
await apiDelete(key);
toast("Removed");
} catch (e) {
toast("Offline — remove will retry", true);
}
if (await pushDelete(key)) toast("Removed");
else toast("Removed — will sync when online", true);
}
// Dwell timer: don't advance progress the instant a newer chapter opens (guards