Skip to content

[episodes] Harden episode-scoped loads after the wrong-episode fix - #2205

Merged
frankrousseau merged 12 commits into
cgwire:mainfrom
frankrousseau:fix/episode-scope-followups
Sep 14, 2026
Merged

frankrousseau merged 12 commits into
cgwire:mainfrom
frankrousseau:fix/episode-scope-followups

Conversation

@frankrousseau

@frankrousseau frankrousseau commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Problem

  • A remotely deleted episode sent its detail page to another episode, and its task pages or a stale link to either got a stand-in
  • Late loadEdits, loadAssets and loadShots responses could land under the key of a newer load and keep the wrong episode on screen
  • An entity deleted while its list was loading, or during its own refresh, came back as a ghost row
  • A production switch while loadAssets awaited the episodes left its loading flag raised and froze the tab
  • The topbar treated a list of two live-added episodes as loaded and bounced a valid direct link to all
  • The playlist page rebuilt a playlist under all or main against a shot map a load had just emptied
  • Breakdown skipped the column width reset when the production switched during the episodes fetch
  • CLEAR_EDITS duplicated LOAD_EDITS_START, and the keyboard multi-select test of ComboboxOptions was dropped

Solution

  • Send the detail and task pages of a lost episode to the episodes list, on live deletion and stale links
  • Ignore edits, assets and shots responses whose recorded loading key changed meanwhile
  • Fetch a live entity once the list load settled, and skip one removed during its refresh
  • Drop an assets load whose production changed while it awaited the episodes
  • Gate the topbar episode refetch on the store's isEpisodeListLoaded flag
  • Await the shots load in flight under a pseudo-episode; keep Breakdown's production snapshot
  • Reuse LOAD_EDITS_START in CLEAR_EDITS, restore the keyboard test

frankrousseau and others added 10 commits September 12, 2026 17:55
A shot, asset, edit or sequence announced by a socket event while its
list was loading was fetched first and parked behind the list promise.
An entity deleted during that window was then re-inserted as a ghost
row: the delete handler only removes what the map holds, and the parked
payload landed after the response that dropped it.

Fetch after the list settled instead: the payload is younger than the
response by construction and a deleted entity fails the fetch. A
displayed entity is still refreshed right away.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
loadEdits only checked the production of a late response, and CLEAR_EDITS
lets a same-production load start without queueing behind the one in
flight. A slow episode response could then land under the key of a
newer production-wide load and stay on screen.

Compare the recorded loading key instead, which covers the production
switch too. CLEAR_EDITS reuses LOAD_EDITS_START rather than repeating
its assignments.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
No consumer told the shared marker from the partial one: both only
made the key differ from the one the pages rebuild. Keep a single
marker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
The episodes watcher redirected the detail page of a remotely deleted
episode to another episode's page, with no way back. Send it to the
episodes list instead, on a live deletion and on a stale direct link.

The episode refetch is gated on the store's loaded flag rather than on
the list length, which live episode:new events can fill before the
response is in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
The playlist page rebuilt a playlist under all or main against the map
a shots load had just emptied. The breakdown page rebound its
production snapshot after the episodes fetch, so a production switched
meanwhile kept the old column widths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
loadAssets awaits the episodes when a TV show has none loaded. A
production switch during that fetch let it resume with the production
left: LOAD_ASSETS_START wiped the new production's assets and raised a
loading flag that no response lowered. Every later loadAssets then
queued behind that settled load and re-dispatched in a loop of
microtasks, freezing the tab.

Give up when the production changed while waiting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
The redirect to the episodes list only covered the episode detail page
on a live deletion. A task page of that episode still got another
episode under the same URL. A direct link to either page went through
the production configuration instead, which picked a stand-in episode
as well.

Treat the episode detail page and its task pages alike, on both paths.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
loadAssets and loadShots only checked the production of a late
response, the gap loadEdits had. A production switch and back during a
slow load let the old response land under the key of the load started
on return: the Assets page kept another episode's assets under all
without refetching. On shots, the discarded response also cleared the
flag and the key of the load in flight, so live shot:new events were
dropped afterwards.

Compare the recorded loading key instead, which covers the production
switch too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
A live update refreshes a displayed shot, asset, edit or sequence
without waiting. When its deletion event landed before the refresh
response, the entity was gone from the map and the response inserted
it back as a ghost row.

Skip the insertion when the entity was displayed as the refresh
started: it was deleted since, or dropped by a list load whose own
response decides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SzHbFrhkjczjtxYnwhL5N2
@NicoPennec

Copy link
Copy Markdown
Member

There are two points to correct:

src/store/modules/assets.js · const marker = isPartial ? '#partial' : ''

The two markers were not equivalent: the in-flight dedup in loadAssets compares the whole key. Under the all or main pseudo-episode, the schedule loads without tasks and shared assets under P/all#partial, and the breakdown and concepts pages now record the same key. Opening the breakdown while the schedule load is still in flight makes it adopt that dataset, without tasks or shared assets, and nothing refetches. Before this change the keys differed, so the second load waited and then fetched its own. Narrow race, but a new one. I would keep #shared.

tests/unit/store/assets.spec.js · gives up when the production changed while it waited for the episodes

This test passes on main too: with no current episode and an empty episode list, loadAssets returns [] on the "no episode" path before it reaches the new guard. Setting rootGetters.currentEpisode in the dispatch stub, as the real loadEpisodes would, makes it fail without the guard: LOAD_ASSETS_START then gets committed for the production left.

frankrousseau and others added 2 commits September 14, 2026 16:45
The in-flight dedup in loadAssets compares the whole loading key. With a
single partial marker, the schedule under the all pseudo-episode and the
production-wide load of the breakdown and the concepts recorded the same
P/all#partial key: a breakdown opened while the schedule was loading
adopted its dataset, without tasks or shared assets, and nothing
refetched.

Restore the #shared marker so the second load waits, then fetches its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8vdEJjSK2MsTYP3TVX6a2
The test stopped on the no episode path before it reached the production
guard, so it passed without it. Resolve an episode in the loadEpisodes
stub, as the real action does, and mock getAssets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8vdEJjSK2MsTYP3TVX6a2
@frankrousseau

Copy link
Copy Markdown
Contributor Author

Both fixed: #shared restored with a regression test for the in-flight dedup (7b16332), and the guard test now resolves an episode in the stub so it fails without the guard (4c5a028).

@frankrousseau
frankrousseau merged commit 48378ee into cgwire:main Sep 14, 2026
5 checks passed
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