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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-03 14:37:59 +02:00
1 parent 39ec848110
commit 6de998094a
2 files changed
+31 -15

No files matched your search

@@ -203,17 +203,21 @@ describe('openEpisodeExternally', () => {
() => new Promise<void>((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 () => {
@@ -52,26 +52,38 @@ export function whenEpisodeLaunchesSettle(owner: string | null): Promise<void> {
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<string, unknown>();
const queuedChoices = new Map<string, QueuedEpisodeChoice>();
/**
* 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<TEpisode>(
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);
});
}