#90: address review — await repair sync before refresh, merge colliding queue markers
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -516,10 +516,11 @@ test("repairLnwStaleRow merges a duplicate under the repaired key, keeping the f
|
||||
assert.equal(merged.last_chapter_num, 42); // the stale row is ahead — progress must not be lost
|
||||
assert.equal(merged.favorite, true); // favourite survives from either row
|
||||
assert.equal(merged.status, "archived"); // the stronger bucket survives
|
||||
// One queue marker per key: the repaired-key entry wins, the stale-key one
|
||||
// is dropped rather than replaying the write twice.
|
||||
// One marker under the repaired key: sendStatus is sticky (the archive
|
||||
// intent from the stale-key marker survives) and the worse attempts count
|
||||
// wins — the queue's own coalescing rules.
|
||||
assert.deepEqual(out.queue, [
|
||||
{ key: "lightnovelworld:immortality-simulator", op: "put", sendStatus: false, attempts: 0 },
|
||||
{ key: "lightnovelworld:immortality-simulator", op: "put", sendStatus: true, attempts: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user