From 1936b45d0ff0b92a5c8730d3fc392a9a5a5e7fec Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 27 Jul 2026 11:21:21 +0200 Subject: [PATCH] fix(portals): invalidate the session while the movie identity is empty MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The staleness guard added in 4db3a2fd bumped the session only inside `load()`, which leaves a window the guard does not cover: route navigation empties the movie identity first, and `load()` for the replacement runs only once a title is knowable again. A resolution completing in that interval still carried a session number that matched, so it passed the check and started the previous movie's source over the page the user was navigating to. The binding effect now bumps the session as soon as the identity goes null, so anything already in flight is invalidated at the moment the old movie stops being the one on screen rather than when the next one finishes loading. `lastMovieKey` is deliberately left alone: returning to the same movie should not re-run discovery, and the controller's state is still correct — only the in-flight operations needed invalidating. Regression test added and mutation-checked: removing the bump fails exactly that test. Co-Authored-By: Claude Opus 5 --- .../vod-multi-source-host-races.spec.ts | 21 +++++++++++++++++++ .../vod-multi-source-host.service.ts | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-multi-source-host-races.spec.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-multi-source-host-races.spec.ts index eee98109c..f405b9aa6 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-multi-source-host-races.spec.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-multi-source-host-races.spec.ts @@ -86,6 +86,27 @@ describe('VodMultiSourceHostService — stale resolutions', () => { ); }); + it('drops a switch once the movie identity goes null mid-navigation', async () => { + await loadMovie([ALT_TWO]); + + const slow = createDeferred>(); + resolver.resolve.mockReturnValueOnce(slow.promise); + + const pending = service.play(ALT_TWO.id); + + // Route navigation empties the identity BEFORE the next movie's load() + // runs. Without invalidating the session here, the guard would still + // pass and the old movie's source would start over the new page. + movie.set(null); + TestBed.tick(); + + slow.resolve(resolvedFor(ALT_TWO, 0)); + await expect(pending).resolves.toBe(false); + + expect(startPlayback).not.toHaveBeenCalled(); + expect(rowFor(ALT_TWO.id)?.isActive).toBe(false); + }); + it('drops a slower switch that a newer selection already superseded', async () => { await loadMovie([ALT_TWO, ALT_THREE]); diff --git a/libs/portal/xtream/feature/src/lib/vod-details/vod-multi-source-host.service.ts b/libs/portal/xtream/feature/src/lib/vod-details/vod-multi-source-host.service.ts index 9c7553568..239f53340 100644 --- a/libs/portal/xtream/feature/src/lib/vod-details/vod-multi-source-host.service.ts +++ b/libs/portal/xtream/feature/src/lib/vod-details/vod-multi-source-host.service.ts @@ -146,6 +146,12 @@ export class VodMultiSourceHostService { effect(() => { const movie = bindings.movie(); if (!movie) { + // Navigating away empties the identity before the next movie's + // `load()` runs. Bumping the session here — not only in + // `load()` — closes the window in which a resolution still in + // flight for the PREVIOUS movie would pass the staleness guard + // and start its playback over the page the user is leaving. + this.discoveryToken++; return; }