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)
|
// 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() {
|
async function bookmarkCurrent() {
|
||||||
const p = state.page;
|
const p = state.page;
|
||||||
if (p.type !== "series" && p.type !== "chapter") return;
|
if (p.type !== "series" && p.type !== "chapter") return;
|
||||||
@@ -502,14 +541,8 @@
|
|||||||
async function syncUpsert(bm, okMsg) {
|
async function syncUpsert(bm, okMsg) {
|
||||||
upsertLocal(bm); // optimistic
|
upsertLocal(bm); // optimistic
|
||||||
render();
|
render();
|
||||||
try {
|
if (await pushBookmark(bm.key, false)) toast(okMsg);
|
||||||
const saved = await apiPut(bm.key, bm);
|
else toast("Saved — will sync when online", true);
|
||||||
upsertLocal(saved); // adopt server updated_at
|
|
||||||
render();
|
|
||||||
toast(okMsg);
|
|
||||||
} catch (e) {
|
|
||||||
toast("Offline — saved locally, will retry", true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleFavorite(key) {
|
async function toggleFavorite(key) {
|
||||||
@@ -521,13 +554,7 @@
|
|||||||
});
|
});
|
||||||
upsertLocal(bm); // optimistic
|
upsertLocal(bm); // optimistic
|
||||||
render();
|
render();
|
||||||
try {
|
if (!(await pushBookmark(bm.key, false))) toast("Saved — will sync when online", true);
|
||||||
const saved = await apiPut(bm.key, bm);
|
|
||||||
upsertLocal(saved);
|
|
||||||
render();
|
|
||||||
} catch (e) {
|
|
||||||
toast("Offline — saved locally, will retry", true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Archive parks a series: it leaves All and Favourites but the server keeps
|
// Archive parks a series: it leaves All and Favourites but the server keeps
|
||||||
@@ -537,16 +564,14 @@
|
|||||||
const existing = state.byKey[key];
|
const existing = state.byKey[key];
|
||||||
if (!existing) return;
|
if (!existing) return;
|
||||||
const next = statusOf(existing) === "archived" ? "reading" : "archived";
|
const next = statusOf(existing) === "archived" ? "reading" : "archived";
|
||||||
const bm = Object.assign({}, existing, { status: next });
|
upsertLocal(Object.assign({}, existing, { status: next }));
|
||||||
upsertLocal(bm);
|
|
||||||
render();
|
render();
|
||||||
try {
|
// The only write with an opinion about the bucket, so the only one that
|
||||||
const saved = await apiPut(bm.key, bm, { sendStatus: true });
|
// sends `status` at all.
|
||||||
upsertLocal(saved); // adopt the stored row: the server owns updated_at
|
if (await pushBookmark(key, true)) {
|
||||||
render();
|
|
||||||
toast(next === "archived" ? "Archived" : "Back in your list");
|
toast(next === "archived" ? "Archived" : "Back in your list");
|
||||||
} catch (e) {
|
} else {
|
||||||
toast("Archive failed — retry when online", true);
|
toast((next === "archived" ? "Archived" : "Restored") + " — will sync when online", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,12 +648,8 @@
|
|||||||
async function removeBookmark(key) {
|
async function removeBookmark(key) {
|
||||||
removeLocal(key); // optimistic
|
removeLocal(key); // optimistic
|
||||||
render();
|
render();
|
||||||
try {
|
if (await pushDelete(key)) toast("Removed");
|
||||||
await apiDelete(key);
|
else toast("Removed — will sync when online", true);
|
||||||
toast("Removed");
|
|
||||||
} catch (e) {
|
|
||||||
toast("Offline — remove will retry", true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dwell timer: don't advance progress the instant a newer chapter opens (guards
|
// Dwell timer: don't advance progress the instant a newer chapter opens (guards
|
||||||
|
|||||||
Reference in New Issue
Block a user