Stale lnw rows repair themselves on the next chapter visit #90

Closed
opened 2026-08-11 11:28:10 +07:00 by sulthan · 1 comment
Owner

Parent

#80 (spec) — implements part of #77.

What to build

Readers already hold Bookmarks created under the invented identity. Those rows keep 404ing on every Poll. Repair them in place the next time the Reader opens a chapter page of that novel — the Reader sees nothing: no second row, no lost Progress, no prompt.

No guessing is needed to spot one. A chapter page now yields both the Chapter Slug from its own address and the series slug from its pointer. If those differ and a row exists under the Chapter Slug identity, that row is the stale one.

The rewrite covers three storage sites and they must move together: the cached row's key, identity and address; the retry-queue entry, if one exists under the old key; and the per-device last-checked map. Then it syncs. The queue matters specifically because an entry under the old key can still replay successfully against the old server row, so leaving the two keys to coexist loses the write — a Reader whose repair happens while offline must find the queued change waiting under the repaired identity.

Write it as a pure transform — cached list, queue and last-checked map in, the same three rewritten out, no storage access inside. That keeps it inside the existing test harness with no new stubbing, and it is exactly the shape worth testing precisely, because the bug hides in one of the sites being missed.

A server-side migration is impossible and was rejected: the database holds no source for the correct slug and no endpoint on the Site maps one slug to the other. The repaired row leaves its old shared Series row behind; nothing deletes it, and the Poll's due-query is an inner join against Bookmarks, so a Series with no Bookmarks is never polled again — the orphan is permanently stored and permanently inert. A Reader who never opens a chapter page of an affected novel does not heal and continues exactly as today. That is the status quo for that row, not a regression.

Acceptance criteria

  • Opening a chapter page of an affected novel rewrites all three storage sites together and syncs, with nothing shown to the Reader
  • The repaired row keeps its Progress, its Favourite flag and its Lifecycle bucket
  • With no stale row present, nothing changes
  • With no queue entry under the old key, the other two sites still move
  • A queued change made before the repair survives under the repaired identity
  • Verified on device: the panel on a chapter page of a tracked affected novel says "Tracked", not "+ Bookmark this", and auto-progress on dwell still records the chapter after the repair
  • node --check userscript/novel-bookmark.user.js and node --test userscript/test/novel-logic.test.js pass

Blocked by

## Parent #80 (spec) — implements part of #77. ## What to build Readers already hold Bookmarks created under the invented identity. Those rows keep 404ing on every Poll. Repair them in place the next time the Reader opens a chapter page of that novel — the Reader sees nothing: no second row, no lost Progress, no prompt. No guessing is needed to spot one. A chapter page now yields both the Chapter Slug from its own address and the series slug from its pointer. If those differ and a row exists under the Chapter Slug identity, that row is the stale one. The rewrite covers three storage sites and they must move together: the cached row's key, identity and address; the retry-queue entry, if one exists under the old key; and the per-device last-checked map. Then it syncs. The queue matters specifically because an entry under the old key can still replay successfully against the old server row, so leaving the two keys to coexist loses the write — a Reader whose repair happens while offline must find the queued change waiting under the repaired identity. Write it as a pure transform — cached list, queue and last-checked map in, the same three rewritten out, no storage access inside. That keeps it inside the existing test harness with no new stubbing, and it is exactly the shape worth testing precisely, because the bug hides in one of the sites being missed. A server-side migration is impossible and was rejected: the database holds no source for the correct slug and no endpoint on the Site maps one slug to the other. The repaired row leaves its old shared Series row behind; nothing deletes it, and the Poll's due-query is an inner join against Bookmarks, so a Series with no Bookmarks is never polled again — the orphan is permanently stored and permanently inert. A Reader who never opens a chapter page of an affected novel does not heal and continues exactly as today. That is the status quo for that row, not a regression. ## Acceptance criteria - [ ] Opening a chapter page of an affected novel rewrites all three storage sites together and syncs, with nothing shown to the Reader - [ ] The repaired row keeps its Progress, its Favourite flag and its Lifecycle bucket - [ ] With no stale row present, nothing changes - [ ] With no queue entry under the old key, the other two sites still move - [ ] A queued change made before the repair survives under the repaired identity - [ ] Verified on device: the panel on a chapter page of a tracked affected novel says "Tracked", not "+ Bookmark this", and auto-progress on dwell still records the chapter after the repair - [ ] `node --check userscript/novel-bookmark.user.js` and `node --test userscript/test/novel-logic.test.js` pass ## Blocked by - #89
sulthan added the ready-for-agent label 2026-08-11 11:28:10 +07:00
sulthan self-assigned this 2026-08-11 11:54:20 +07:00
Author
Owner

Landed on main (merge of ticket/90-lnw-stale-row-repair; commits 8c172e3, e191155).

repairLnwStaleRow is a pure transform — cached list, queue and last-checked map in, the same three out, no storage access, no module state, no clock — exported from the Node test hook. applyLnwStaleRowRepair drives it at both detect() sites (init and onNavigate) before render(), persists all three sites, and syncs through the queue-backed path with the outcome swallowed. No toast, no prompt.

Decisions worth recording:

  • Duplicate case (spec user story 2): when a row already exists under the repaired key the two merge rather than one being dropped. The farther-ahead progress wins, favourite is OR'd, and the stronger lifecycle bucket is kept (finished > archived > reading), so a merge can never silently un-archive or un-finish a row.
  • The old server bookmark is DELETEd alongside the repaired PUT. Without it the Reader's list holds the novel twice after the next refresh, which is the duplication user story 2 exists to end. This is the Bookmark under the retired key, not the shared Series row — that one stays behind, inert, exactly as the spec says.
  • Queue collision: when both keys hold a marker they collapse to one under the repaired key, sticky sendStatus surviving from either and the worse attempts count winning.
  • Two mechanical traps handled: the queue array is spliced in place because closures capture that instance, and reindex() runs before anything reads state.byKey.

29/29 tests passing, node --check clean.

On-device: verified with Playwright on a live chapter page. The stale row was rewritten to immortality-simulator across cache, queue and last-checked; the panel showed Tracked; auto-progress on dwell recorded Chapter 1 after ~32s. Console showed only the expected API 401s (no token in that browser) and no script exceptions.

Residual: the full server round-trip could not be demonstrated without a real credential — queue park and coalesce were observed instead. The client-side latest-chapter scan is still slug-scoped; that is #91.

Landed on main (merge of ticket/90-lnw-stale-row-repair; commits 8c172e3, e191155). repairLnwStaleRow is a pure transform — cached list, queue and last-checked map in, the same three out, no storage access, no module state, no clock — exported from the Node test hook. applyLnwStaleRowRepair drives it at both detect() sites (init and onNavigate) before render(), persists all three sites, and syncs through the queue-backed path with the outcome swallowed. No toast, no prompt. Decisions worth recording: - Duplicate case (spec user story 2): when a row already exists under the repaired key the two merge rather than one being dropped. The farther-ahead progress wins, favourite is OR'd, and the stronger lifecycle bucket is kept (finished > archived > reading), so a merge can never silently un-archive or un-finish a row. - The old server bookmark is DELETEd alongside the repaired PUT. Without it the Reader's list holds the novel twice after the next refresh, which is the duplication user story 2 exists to end. This is the Bookmark under the retired key, not the shared Series row — that one stays behind, inert, exactly as the spec says. - Queue collision: when both keys hold a marker they collapse to one under the repaired key, sticky sendStatus surviving from either and the worse attempts count winning. - Two mechanical traps handled: the queue array is spliced in place because closures capture that instance, and reindex() runs before anything reads state.byKey. 29/29 tests passing, node --check clean. On-device: verified with Playwright on a live chapter page. The stale row was rewritten to immortality-simulator across cache, queue and last-checked; the panel showed Tracked; auto-progress on dwell recorded Chapter 1 after ~32s. Console showed only the expected API 401s (no token in that browser) and no script exceptions. Residual: the full server round-trip could not be demonstrated without a real credential — queue park and coalesce were observed instead. The client-side latest-chapter scan is still slug-scoped; that is #91.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#90