From 6de998094af3cf45d23a7ad0f210b134156ed94b Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 3 Oct 2026 14:37:59 +0200 Subject: [PATCH] fix(portals): a reopened series keeps its queued episode choice One settle handler per owner hands the latest queued choice to the start callback that came with it, so the page the viewer left cannot consume the choice a reopened page made. Co-Authored-By: Claude Fable 5.1 --- .../serial-details-external-launch.spec.ts | 16 ++++++---- .../serial-details-external-launch.ts | 30 +++++++++++++------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details-external-launch.spec.ts b/libs/portal/xtream/feature/src/lib/serial-details/serial-details-external-launch.spec.ts index f115c4b90..aa7fc98f3 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details-external-launch.spec.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details-external-launch.spec.ts @@ -203,17 +203,21 @@ describe('openEpisodeExternally', () => { () => new Promise((resolve) => (settleLaunch = resolve)) ); const launch = openEpisodeExternally(t.host, PLAYBACK, 'mpv'); - const start = jest.fn(); - queueEpisodeChoice('xtream-1:103', 'episode-1', start); - queueEpisodeChoice('xtream-1:103', 'episode-2', start); + // The first page queues, the viewer reopens the series and queues + // again: the reopened page's start runs, with its episode. + const startOld = jest.fn(); + const startNew = jest.fn(); + queueEpisodeChoice('xtream-1:103', 'episode-1', startOld); + queueEpisodeChoice('xtream-1:103', 'episode-2', startNew); await flush(); - expect(start).not.toHaveBeenCalled(); + expect(startOld).not.toHaveBeenCalled(); settleLaunch(); await launch; await flush(); - expect(start).toHaveBeenCalledTimes(1); - expect(start).toHaveBeenCalledWith('episode-2'); + expect(startOld).not.toHaveBeenCalled(); + expect(startNew).toHaveBeenCalledTimes(1); + expect(startNew).toHaveBeenCalledWith('episode-2'); }); it('keeps the running player when closing it fails', async () => { diff --git a/libs/portal/xtream/feature/src/lib/serial-details/serial-details-external-launch.ts b/libs/portal/xtream/feature/src/lib/serial-details/serial-details-external-launch.ts index ef7bd3730..900abcab1 100644 --- a/libs/portal/xtream/feature/src/lib/serial-details/serial-details-external-launch.ts +++ b/libs/portal/xtream/feature/src/lib/serial-details/serial-details-external-launch.ts @@ -52,26 +52,38 @@ export function whenEpisodeLaunchesSettle(owner: string | null): Promise { return tail ? tail.then(() => undefined) : Promise.resolve(); } +interface QueuedEpisodeChoice { + readonly episode: unknown; + readonly start: (episode: never) => void; +} + /** Per owner, the latest Play/episode choice made while its forced launch settled. */ -const queuedChoices = new Map(); +const queuedChoices = new Map(); /** * Keeps the latest choice made while the owner's forced launch settles and - * hands it to `start` once that launch settled; the caller decides whether - * the page still shows the owner by then. + * hands it to the `start` that came with it once that launch settled; the + * caller decides whether its page still shows the owner by then. One settle + * handler per owner: a page reopened meanwhile queues with its own `start`, + * and that one runs, not the handler of the page the viewer left. */ export function queueEpisodeChoice( owner: string, episode: TEpisode, start: (episode: TEpisode) => void ): void { - queuedChoices.set(owner, episode); + const handlerRegistered = queuedChoices.has(owner); + queuedChoices.set(owner, { + episode, + start: start as (episode: never) => void, + }); + if (handlerRegistered) { + return; + } void whenEpisodeLaunchesSettle(owner).then(() => { - const queued = queuedChoices.get(owner) as TEpisode | undefined; - if (queued !== undefined) { - queuedChoices.delete(owner); - start(queued); - } + const queued = queuedChoices.get(owner); + queuedChoices.delete(owner); + queued?.start(queued.episode as never); }); }