lightnovelworld Series identity is read from the chapter page (#80) #92
@@ -303,17 +303,29 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let queueOut = queue;
|
let queueOut = queue;
|
||||||
if (queue.some((e) => e.key === oldKey)) {
|
const oldEntry = queue.find((e) => e.key === oldKey);
|
||||||
queueOut = queue
|
if (oldEntry) {
|
||||||
.map((e) => {
|
const survivor = queue.find((x) => x.key === newKey);
|
||||||
if (e.key !== oldKey) return e;
|
if (!survivor) {
|
||||||
// An entry already under the repaired key wins: both markers read
|
queueOut = queue.map((e) => (e.key === oldKey ? Object.assign({}, e, { key: newKey }) : e));
|
||||||
// the same row from the cache at send time, so keeping both would
|
} else {
|
||||||
// replay one write twice.
|
// Both keys hold a marker and one row survives, so the two collapse
|
||||||
if (queue.some((x) => x.key === newKey)) return null;
|
// into one entry under the repaired key (the queue's one-entry-per-key
|
||||||
return Object.assign({}, e, { key: newKey });
|
// invariant). sendStatus is sticky — an archive intent from either
|
||||||
})
|
// marker survives, the queue's own rule — and the worse attempts
|
||||||
.filter(Boolean);
|
// 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;
|
let lastCheckedOut = lastChecked;
|
||||||
@@ -1327,7 +1339,8 @@
|
|||||||
// Refresh + navigation
|
// 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
|
await drain(); // push what we owe before adopting the server's view of it
|
||||||
loading = true;
|
loading = true;
|
||||||
render();
|
render();
|
||||||
@@ -1345,12 +1358,16 @@
|
|||||||
// Runs the repair on every navigation, silently: rewrites a stale
|
// Runs the repair on every navigation, silently: rewrites a stale
|
||||||
// lightnovelworld row to the page's discovered identity, persists all three
|
// lightnovelworld row to the page's discovered identity, persists all three
|
||||||
// sites, and syncs through the queue-backed path so an offline repair parks
|
// 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
|
// and replays later. No toast — the Reader never asked for this. Returns
|
||||||
// transform returns its inputs unchanged there is nothing to do.
|
// the sync promise (or null when nothing changed) so init can hold the
|
||||||
function repairStaleLnwRow() {
|
// 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 lastChecked = loadLastChecked();
|
||||||
const out = repairLnwStaleRow(state.list, queue, lastChecked, state.page);
|
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;
|
state.list = out.list;
|
||||||
reindex(); // byKey answers under the old key until rebuilt
|
reindex(); // byKey answers under the old key until rebuilt
|
||||||
saveCache(state.list);
|
saveCache(state.list);
|
||||||
@@ -1359,16 +1376,20 @@
|
|||||||
saveLastChecked(out.lastChecked);
|
saveLastChecked(out.lastChecked);
|
||||||
// Queue-backed sync, outcome swallowed: the repaired row is PUT (with its
|
// Queue-backed sync, outcome swallowed: the repaired row is PUT (with its
|
||||||
// bucket when archived — the only status a userscript write may send) and
|
// 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);
|
const key = keyOf(state.page);
|
||||||
pushBookmark(key, statusOf(state.byKey[key]) === "archived").catch(() => {});
|
return Promise.all([
|
||||||
pushDelete("lightnovelworld:" + state.page.chapterSlug).catch(() => {});
|
pushBookmark(key, statusOf(state.byKey[key]) === "archived"),
|
||||||
|
pushDelete("lightnovelworld:" + state.page.chapterSlug),
|
||||||
|
]).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastUrl = location.href;
|
let lastUrl = location.href;
|
||||||
function onNavigate() {
|
function onNavigate() {
|
||||||
state.page = detect();
|
state.page = detect();
|
||||||
repairStaleLnwRow();
|
applyLnwStaleRowRepair();
|
||||||
render();
|
render();
|
||||||
maybeAutoUpdate();
|
maybeAutoUpdate();
|
||||||
maybeCaptureLatestOnSeriesPage();
|
maybeCaptureLatestOnSeriesPage();
|
||||||
@@ -1481,14 +1502,14 @@
|
|||||||
function init() {
|
function init() {
|
||||||
buildUI();
|
buildUI();
|
||||||
state.page = detect();
|
state.page = detect();
|
||||||
repairStaleLnwRow();
|
const repair = applyLnwStaleRowRepair();
|
||||||
render();
|
render();
|
||||||
installNavWatcher();
|
installNavWatcher();
|
||||||
installLongPress();
|
installLongPress();
|
||||||
window.addEventListener("online", drain); // signal returned while the page stayed open
|
window.addEventListener("online", drain); // signal returned while the page stayed open
|
||||||
// Sync first: both auto-record and the latest-chapter checks below need to
|
// Sync first: both auto-record and the latest-chapter checks below need to
|
||||||
// know which series are bookmarked and how fresh they are.
|
// know which series are bookmarked and how fresh they are.
|
||||||
refresh().then(() => {
|
refresh(repair).then(() => {
|
||||||
maybeAutoUpdate();
|
maybeAutoUpdate();
|
||||||
maybeCaptureLatestOnSeriesPage();
|
maybeCaptureLatestOnSeriesPage();
|
||||||
backgroundRefreshLatest();
|
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.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.favorite, true); // favourite survives from either row
|
||||||
assert.equal(merged.status, "archived"); // the stronger bucket survives
|
assert.equal(merged.status, "archived"); // the stronger bucket survives
|
||||||
// One queue marker per key: the repaired-key entry wins, the stale-key one
|
// One marker under the repaired key: sendStatus is sticky (the archive
|
||||||
// is dropped rather than replaying the write twice.
|
// intent from the stale-key marker survives) and the worse attempts count
|
||||||
|
// wins — the queue's own coalescing rules.
|
||||||
assert.deepEqual(out.queue, [
|
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