#90: address review — await repair sync before refresh, merge colliding queue markers

This commit is contained in:
2026-08-11 13:06:29 +07:00
parent 8c172e3579
commit e1911554ec
2 changed files with 47 additions and 25 deletions
+43 -22
View File
@@ -303,17 +303,29 @@
}
let queueOut = queue;
if (queue.some((e) => e.key === oldKey)) {
queueOut = queue
.map((e) => {
if (e.key !== oldKey) return e;
// An entry already under the repaired key wins: both markers read
// the same row from the cache at send time, so keeping both would
// replay one write twice.
if (queue.some((x) => x.key === newKey)) return null;
return Object.assign({}, e, { key: newKey });
})
.filter(Boolean);
const oldEntry = queue.find((e) => e.key === oldKey);
if (oldEntry) {
const survivor = queue.find((x) => x.key === newKey);
if (!survivor) {
queueOut = queue.map((e) => (e.key === oldKey ? Object.assign({}, e, { key: newKey }) : e));
} else {
// Both keys hold a marker and one row survives, so the two collapse
// into one entry under the repaired key (the queue's one-entry-per-key
// invariant). sendStatus is sticky — an archive intent from either
// marker survives, the queue's own rule — and the worse attempts
// count wins. The stale-key marker's op is dropped: the row it
// described is retired by the merge itself.
queueOut = queue
.filter((e) => e.key !== oldKey && e.key !== newKey)
.concat([
{
key: newKey,
op: survivor.op,
sendStatus: survivor.sendStatus || oldEntry.sendStatus,
attempts: Math.max(survivor.attempts || 0, oldEntry.attempts || 0),
},
]);
}
}
let lastCheckedOut = lastChecked;
@@ -1327,7 +1339,8 @@
// Refresh + navigation
// ============================================================
async function refresh() {
async function refresh(awaitFirst) {
if (awaitFirst) await awaitFirst; // repair sync lands before we adopt the server's view of it
await drain(); // push what we owe before adopting the server's view of it
loading = true;
render();
@@ -1345,12 +1358,16 @@
// Runs the repair on every navigation, silently: rewrites a stale
// lightnovelworld row to the page's discovered identity, persists all three
// sites, and syncs through the queue-backed path so an offline repair parks
// and replays later. No toast — the Reader never asked for this. When the
// transform returns its inputs unchanged there is nothing to do.
function repairStaleLnwRow() {
// and replays later. No toast — the Reader never asked for this. Returns
// the sync promise (or null when nothing changed) so init can hold the
// server view until the repair has landed. When the transform returns its
// inputs unchanged there is nothing to do.
function applyLnwStaleRowRepair() {
const lastChecked = loadLastChecked();
const out = repairLnwStaleRow(state.list, queue, lastChecked, state.page);
if (out.list === state.list && out.queue === queue && out.lastChecked === lastChecked) return;
if (out.list === state.list && out.queue === queue && out.lastChecked === lastChecked) {
return null;
}
state.list = out.list;
reindex(); // byKey answers under the old key until rebuilt
saveCache(state.list);
@@ -1359,16 +1376,20 @@
saveLastChecked(out.lastChecked);
// Queue-backed sync, outcome swallowed: the repaired row is PUT (with its
// bucket when archived — the only status a userscript write may send) and
// the old server row is deleted so no duplicate survives on the wire.
// the old server bookmark is deleted so no duplicate survives on the
// wire. The delete parks under the retired key while offline — the
// teardown of the old identity, not a new record of the Chapter Slug.
const key = keyOf(state.page);
pushBookmark(key, statusOf(state.byKey[key]) === "archived").catch(() => {});
pushDelete("lightnovelworld:" + state.page.chapterSlug).catch(() => {});
return Promise.all([
pushBookmark(key, statusOf(state.byKey[key]) === "archived"),
pushDelete("lightnovelworld:" + state.page.chapterSlug),
]).catch(() => {});
}
let lastUrl = location.href;
function onNavigate() {
state.page = detect();
repairStaleLnwRow();
applyLnwStaleRowRepair();
render();
maybeAutoUpdate();
maybeCaptureLatestOnSeriesPage();
@@ -1481,14 +1502,14 @@
function init() {
buildUI();
state.page = detect();
repairStaleLnwRow();
const repair = applyLnwStaleRowRepair();
render();
installNavWatcher();
installLongPress();
window.addEventListener("online", drain); // signal returned while the page stayed open
// Sync first: both auto-record and the latest-chapter checks below need to
// know which series are bookmarked and how fresh they are.
refresh().then(() => {
refresh(repair).then(() => {
maybeAutoUpdate();
maybeCaptureLatestOnSeriesPage();
backgroundRefreshLatest();