diff --git a/userscript/manga-bookmark.user.js b/userscript/manga-bookmark.user.js index e5bf6a3..8a5a2fb 100644 --- a/userscript/manga-bookmark.user.js +++ b/userscript/manga-bookmark.user.js @@ -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