feat(userscript): route every write through the retry queue
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user