Skip to content

fix: stop the playlist cache serving stale watched state on re-entry - #686

Merged
KrX3D merged 2 commits into
mainfrom
fix/stale-playlist-cache
Sep 1, 2026
Merged

fix: stop the playlist cache serving stale watched state on re-entry#686
KrX3D merged 2 commits into
mainfrom
fix/stale-playlist-cache

Conversation

@KrX3D

@KrX3D KrX3D commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Your diagnosis was right — re-entry was being served from cache.

getWatchPercent() reads watched state primarily from data embedded in the renderer itself (thumbnailOverlayResumePlaybackRenderer, played/status overlays, watched badges), and only falls back to the live _ttVideoProgressCache when none are present. The cache holds raw renderers captured at collection time, so their overlays are frozen at that moment. Injecting them on a later visit replays the old progress and discards the fresh response that did carry the update.

So the cache is now strictly a hand-off from the pre-reload pass to the reloaded page, and is consumed on injection. Every visit runs its own collect+reload against fresh data — ~1.7s per visit instead of a 0.38s cache hit, which is the right trade for correct watched state.

Two guards prevent a reload loop, since consuming the cache mid-visit would otherwise let the reloaded page collect and reload again:

  • __ttServedFromCache marks the cache-served page and the scheduler stands down for it (it already holds the full list and has no continuation token).
  • The reload guard now clears on real navigation rather than at injection, so it survives SOFT_RELOAD_PAGE (which doesn't navigate) while still letting the next genuine visit run.

Point 1 not addressed, as you said was fine: stopping playback returns to the playlist without reprocessing, so videos watched in that session stay visible until you re-enter. With this fix, that re-entry now filters them correctly — which was the practical problem.

Reported: after watching videos and re-entering a playlist, the watched
ones came back looking unwatched. That is a regression from the full-list
cache, and the diagnosis offered with the report was right — re-entry was
being served from cache.

getWatchPercent() reads watched state primarily from data embedded in the
renderer itself (thumbnailOverlayResumePlaybackRenderer, played/status
overlays, watched badges), falling back to the live _ttVideoProgressCache
only when none of those are present. The cache holds raw renderers captured
at collection time, so their overlays are frozen at that moment. Injecting
them on a later visit replays whatever progress they had then, and
overwrites the fresh response that did carry the updated state.

So the cache is now strictly a hand-off from the pre-reload pass to the
reloaded page, consumed on injection. Every visit runs its own
collect+reload against fresh data. That costs a cycle per visit (~1.7s)
instead of a 0.38s cache hit, which is the right trade for showing correct
watched state.

Two guards keep that from looping, since consuming the cache mid-visit
would otherwise let the reloaded page collect and reload again:
- __ttServedFromCache marks the page that was served from cache, and the
  scheduler stands down for it — it already holds the full list and has no
  continuation token.
- The reload guard is cleared in _clearState (real navigation) rather than
  at injection, so it persists across SOFT_RELOAD_PAGE, which does not
  navigate, while still letting the next genuine visit run its own cycle.

Not addressed, as agreed: stopping playback returns to the playlist without
reprocessing it, so videos watched in that session stay visible until the
page is re-entered. With this fix that re-entry now filters them correctly,
which was the practical problem.
Comment thread mods/features/playlistBatchCollect.js Fixed
Valid finding — remote property injection on the cache write.

Both the full-playlist cache and the one-shot reload guard were plain
objects keyed by playlistKeyFromHash(), i.e. window.location.hash. That is
attacker-influenceable through a crafted URL, so cache[key] = ... with a
hash of #__proto__ would write through to Object.prototype and pollute
every object in the page.

Converted both to the collection types that have no prototype chain to
reach:
- __ttPlaylistFullCache: object -> Map (get/set/has/delete, plus size and
  iteration for the eviction pass, which previously used Object.keys and a
  reduce over cache[a].ts).
- __ttFullReloadDone: object -> Set, since it only ever stored true as a
  membership marker.

Both are guarded with an instanceof check on read, so a stale plain object
left on window by an earlier build is replaced rather than misused.

Set is already used in four places in adblock.js, so these types are known
to work on the target devices.

No behaviour change.
@KrX3D
KrX3D merged commit ab68f85 into main Sep 1, 2026
3 checks passed
@KrX3D
KrX3D deleted the fix/stale-playlist-cache branch September 1, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants