#90: repair stale lnw rows in place on the next chapter visit
A row keyed under the Chapter Slug its address was built from, while the page pointer names a different Series, is stale. Rewrite its key, identity and address in the cache, the retry-queue entry (if any) and the per-device last-checked map together, then sync through the queue-backed path so an offline repair parks and replays. Pure transform exported for the harness; wired before render() at both detect() call sites. A row already under the repaired key merges, keeping the farther-ahead progress, favourite OR'd and the stronger lifecycle bucket.
This commit is contained in:
@@ -47,6 +47,7 @@ const {
|
||||
lightnovelworld,
|
||||
kindOf,
|
||||
maxChapter,
|
||||
repairLnwStaleRow,
|
||||
} = require("../novel-bookmark.user.js");
|
||||
|
||||
function loc(href) {
|
||||
@@ -331,6 +332,212 @@ test("latestChapterFromAnchors returns null when nothing matches", () => {
|
||||
assert.equal(maxChapter([], /chapter-([0-9.]+)/), null);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// repairLnwStaleRow — the row migration (spec seam 3)
|
||||
// ============================================================
|
||||
|
||||
// A row as bookmarkCurrent builds it, keyed under the invented Chapter Slug
|
||||
// identity the old adapter derived from the chapter address.
|
||||
function staleRow(over) {
|
||||
return Object.assign(
|
||||
{
|
||||
key: "lightnovelworld:my-longevity-simulation",
|
||||
site: "lightnovelworld",
|
||||
kind: "novel",
|
||||
series_id: "my-longevity-simulation",
|
||||
title: "My Longevity Simulation",
|
||||
series_url: "https://lightnovelworld.net/novel/my-longevity-simulation/",
|
||||
last_chapter: "Chapter 7",
|
||||
last_chapter_num: 7,
|
||||
last_chapter_url: "https://lightnovelworld.net/my-longevity-simulation-chapter-7/",
|
||||
updated_at: 1000,
|
||||
},
|
||||
over
|
||||
);
|
||||
}
|
||||
|
||||
// The divergent novel from research §3: the address's slug is a Chapter Slug,
|
||||
// the page pointer names the real Series.
|
||||
function lnwPage(over) {
|
||||
return Object.assign(
|
||||
{
|
||||
type: "chapter",
|
||||
site: "lightnovelworld",
|
||||
seriesId: "immortality-simulator",
|
||||
chapterSlug: "my-longevity-simulation",
|
||||
seriesUrl: "https://lightnovelworld.net/novel/immortality-simulator/",
|
||||
},
|
||||
over
|
||||
);
|
||||
}
|
||||
|
||||
test("repairLnwStaleRow rewrites the row, the queue entry and the last-checked map together", () => {
|
||||
const list = [staleRow()];
|
||||
const queue = [
|
||||
{ key: "lightnovelworld:my-longevity-simulation", op: "put", sendStatus: false, attempts: 2 },
|
||||
];
|
||||
const lastChecked = { "lightnovelworld:my-longevity-simulation": 12345 };
|
||||
const out = repairLnwStaleRow(list, queue, lastChecked, lnwPage());
|
||||
assert.equal(out.list.length, 1);
|
||||
assert.equal(out.list[0].key, "lightnovelworld:immortality-simulator");
|
||||
assert.equal(out.list[0].series_id, "immortality-simulator");
|
||||
assert.equal(out.list[0].series_url, "https://lightnovelworld.net/novel/immortality-simulator/");
|
||||
assert.deepEqual(out.queue, [
|
||||
{ key: "lightnovelworld:immortality-simulator", op: "put", sendStatus: false, attempts: 2 },
|
||||
]);
|
||||
assert.deepEqual(out.lastChecked, { "lightnovelworld:immortality-simulator": 12345 });
|
||||
});
|
||||
|
||||
test("repairLnwStaleRow keeps Progress, Favourite and Lifecycle bucket on the rewritten row", () => {
|
||||
const stale = staleRow({
|
||||
last_chapter: "Chapter 42",
|
||||
last_chapter_num: 42,
|
||||
last_chapter_url: "https://lightnovelworld.net/my-longevity-simulation-chapter-42/",
|
||||
favorite: true,
|
||||
status: "archived",
|
||||
updated_at: 777,
|
||||
});
|
||||
const out = repairLnwStaleRow([stale], [], {}, lnwPage());
|
||||
const b = out.list[0];
|
||||
assert.equal(b.last_chapter, "Chapter 42");
|
||||
assert.equal(b.last_chapter_num, 42);
|
||||
assert.equal(
|
||||
b.last_chapter_url,
|
||||
"https://lightnovelworld.net/my-longevity-simulation-chapter-42/"
|
||||
);
|
||||
assert.equal(b.favorite, true);
|
||||
assert.equal(b.status, "archived");
|
||||
assert.equal(b.updated_at, 777);
|
||||
assert.equal(b.title, "My Longevity Simulation");
|
||||
});
|
||||
|
||||
test("repairLnwStaleRow changes nothing when the Chapter Slug and Series slug agree", () => {
|
||||
const list = [
|
||||
staleRow({ key: "lightnovelworld:a-will-eternal", series_id: "a-will-eternal" }),
|
||||
];
|
||||
const queue = [{ key: "lightnovelworld:a-will-eternal", op: "put", sendStatus: true, attempts: 1 }];
|
||||
const lastChecked = { "lightnovelworld:a-will-eternal": 99 };
|
||||
const out = repairLnwStaleRow(
|
||||
list,
|
||||
queue,
|
||||
lastChecked,
|
||||
lnwPage({
|
||||
chapterSlug: "a-will-eternal",
|
||||
seriesId: "a-will-eternal",
|
||||
seriesUrl: "https://lightnovelworld.net/novel/a-will-eternal/",
|
||||
})
|
||||
);
|
||||
assert.equal(out.list, list); // same references, nothing rewritten
|
||||
assert.equal(out.queue, queue);
|
||||
assert.equal(out.lastChecked, lastChecked);
|
||||
});
|
||||
|
||||
test("repairLnwStaleRow changes nothing when no row sits under the old key", () => {
|
||||
const list = [
|
||||
staleRow({
|
||||
key: "lightnovelworld:immortality-simulator",
|
||||
series_id: "immortality-simulator",
|
||||
series_url: "https://lightnovelworld.net/novel/immortality-simulator/",
|
||||
}),
|
||||
];
|
||||
const lastChecked = { "lightnovelworld:immortality-simulator": 99 };
|
||||
const out = repairLnwStaleRow(list, [], lastChecked, lnwPage());
|
||||
assert.equal(out.list, list);
|
||||
assert.equal(out.queue.length, 0);
|
||||
assert.equal(out.lastChecked, lastChecked);
|
||||
});
|
||||
|
||||
test("repairLnwStaleRow ignores a page that carries no Chapter Slug", () => {
|
||||
const list = [
|
||||
staleRow({
|
||||
key: "lightnovelworld:immortality-simulator",
|
||||
series_id: "immortality-simulator",
|
||||
}),
|
||||
];
|
||||
const lastChecked = { "lightnovelworld:immortality-simulator": 99 };
|
||||
// A series page (chapterSlug null) and another site both stay untouched.
|
||||
const series = repairLnwStaleRow(list, [], lastChecked, lnwPage({ chapterSlug: null }));
|
||||
const other = repairLnwStaleRow(list, [], lastChecked, {
|
||||
type: "chapter",
|
||||
site: "novelfull",
|
||||
seriesId: "x",
|
||||
});
|
||||
assert.equal(series.list, list);
|
||||
assert.equal(other.list, list);
|
||||
});
|
||||
|
||||
test("repairLnwStaleRow still moves the row and the last-checked map when the queue has no entry", () => {
|
||||
const out = repairLnwStaleRow(
|
||||
[staleRow()],
|
||||
[],
|
||||
{ "lightnovelworld:my-longevity-simulation": 5 },
|
||||
lnwPage()
|
||||
);
|
||||
assert.equal(out.list[0].key, "lightnovelworld:immortality-simulator");
|
||||
assert.deepEqual(out.queue, []);
|
||||
assert.deepEqual(out.lastChecked, { "lightnovelworld:immortality-simulator": 5 });
|
||||
});
|
||||
|
||||
test("repairLnwStaleRow carries a queued change to the repaired identity with its marker intact", () => {
|
||||
const queue = [
|
||||
{ key: "lightnovelworld:my-longevity-simulation", op: "put", sendStatus: true, attempts: 3 },
|
||||
];
|
||||
const out = repairLnwStaleRow([staleRow()], queue, {}, lnwPage());
|
||||
assert.deepEqual(out.queue, [
|
||||
{ key: "lightnovelworld:immortality-simulator", op: "put", sendStatus: true, attempts: 3 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("repairLnwStaleRow merges a duplicate under the repaired key, keeping the farther-ahead progress", () => {
|
||||
const canonical = staleRow({
|
||||
key: "lightnovelworld:immortality-simulator",
|
||||
series_id: "immortality-simulator",
|
||||
series_url: "https://lightnovelworld.net/novel/immortality-simulator/",
|
||||
last_chapter: "Chapter 5",
|
||||
last_chapter_num: 5,
|
||||
last_chapter_url: "https://lightnovelworld.net/immortality-simulator-chapter-5/",
|
||||
favorite: false,
|
||||
});
|
||||
const stale = staleRow({
|
||||
last_chapter: "Chapter 42",
|
||||
last_chapter_num: 42,
|
||||
last_chapter_url: "https://lightnovelworld.net/my-longevity-simulation-chapter-42/",
|
||||
favorite: true,
|
||||
status: "archived",
|
||||
});
|
||||
const queue = [
|
||||
{ key: "lightnovelworld:my-longevity-simulation", op: "put", sendStatus: true, attempts: 3 },
|
||||
{ key: "lightnovelworld:immortality-simulator", op: "put", sendStatus: false, attempts: 0 },
|
||||
];
|
||||
const out = repairLnwStaleRow([canonical, stale], queue, {}, lnwPage());
|
||||
assert.equal(out.list.length, 1);
|
||||
const merged = out.list[0];
|
||||
assert.equal(merged.key, "lightnovelworld:immortality-simulator");
|
||||
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.
|
||||
assert.deepEqual(out.queue, [
|
||||
{ key: "lightnovelworld:immortality-simulator", op: "put", sendStatus: false, attempts: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
test("repairLnwStaleRow does not regress progress when the repaired-key row is ahead", () => {
|
||||
const canonical = staleRow({
|
||||
key: "lightnovelworld:immortality-simulator",
|
||||
series_id: "immortality-simulator",
|
||||
series_url: "https://lightnovelworld.net/novel/immortality-simulator/",
|
||||
last_chapter: "Chapter 100",
|
||||
last_chapter_num: 100,
|
||||
last_chapter_url: "https://lightnovelworld.net/immortality-simulator-chapter-100/",
|
||||
});
|
||||
const stale = staleRow({ last_chapter: "Chapter 42", last_chapter_num: 42 });
|
||||
const out = repairLnwStaleRow([canonical, stale], [], {}, lnwPage());
|
||||
assert.equal(out.list.length, 1);
|
||||
assert.equal(out.list[0].last_chapter_num, 100);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// kindOf
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user