fix(portals): a Stalker movie launch superseded inside the IPC closes its player, and a reset holds the starts

After the launch IPC began, moving on to another movie or a newer start
could no longer stop it, and the session it returned was dropped, so
its player opened beside the new one; the session is kept and closed
when the launch is no longer current. A progress reset now counts as a
pending start of its movie, so Play, Start over and launches are held
until the write landed instead of resuming from the row being cleared.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-10-03 19:20:20 +02:00
1 parent ccac905ba0
commit bfa04001c9
2 files changed
+66 -1

No files matched your search

@@ -196,6 +196,32 @@ describe('createStalkerVodDetailActions openExternal', () => {
expect(t.openExternalPlayback).not.toHaveBeenCalled();
});
it('closes the player a launch opened after the page moved on', async () => {
let selected = 42;
const t = setup(() => selected);
const opened = {
id: 'mpv-7',
status: 'launching',
} as ExternalPlayerSession;
t.openExternalPlayback.mockResolvedValue(opened);
const launch = t.actions.openExternal({
item: MOVIE,
player: 'mpv',
positionSeconds: null,
});
t.resolveLink();
await Promise.resolve();
await Promise.resolve();
// The launch is inside the player IPC when the viewer opens another
// movie: its player must not open beside that one.
selected = 43;
await launch;
expect(t.openExternalPlayback).toHaveBeenCalledTimes(1);
expect(t.closeSession).toHaveBeenCalledWith(opened);
expect(t.open).not.toHaveBeenCalled();
});
it('drops the launch once a newer start superseded it', async () => {
const t = setup(() => 42);
const launch = t.actions.openExternal({
@@ -301,6 +327,12 @@ describe('createStalkerVodDetailActions resetProgress', () => {
const discardPendingPositionLoad = jest.fn();
const afterProgressReset = jest.fn();
const open = jest.fn();
const settlePendingStart = jest.fn();
const beginPendingStart = jest.fn().mockReturnValue({
settle: settlePendingStart,
isCurrent: () => true,
rebase: jest.fn(),
});
const actions = createStalkerVodDetailActions({
resolvePlayback: jest.fn(),
portalPlayer: { openExternalPlayback: jest.fn() },
@@ -313,6 +345,7 @@ describe('createStalkerVodDetailActions resetProgress', () => {
selectedVodId,
selectedVodPosition,
discardPendingPositionLoad,
beginPendingStart,
afterProgressReset,
snackBar: { open },
translate: { instant: (key: string) => key },
@@ -325,9 +358,29 @@ describe('createStalkerVodDetailActions resetProgress', () => {
discardPendingPositionLoad,
afterProgressReset,
open,
beginPendingStart,
settlePendingStart,
};
}
it("holds the movie's starts until the clear landed", async () => {
const t = setup(() => 42);
let finishClear: () => void = () => undefined;
t.clearPlaybackPositionOrThrow.mockImplementationOnce(
() => new Promise<void>((resolve) => (finishClear = resolve))
);
const reset = t.actions.resetProgress(MOVIE);
// A start made now would resume from the row being cleared.
expect(t.beginPendingStart).toHaveBeenCalledTimes(1);
expect(t.settlePendingStart).not.toHaveBeenCalled();
finishClear();
await reset;
expect(t.settlePendingStart).toHaveBeenCalledTimes(1);
expect(t.selectedVodPosition()).toBeNull();
});
it('clears the shown position of the movie that is still selected', async () => {
const t = setup(() => 42);
await t.actions.resetProgress(MOVIE);
@@ -171,10 +171,16 @@ export function createStalkerVodDetailActions(
// The host's inline teardown retired the request id too;
// only a start made after this point supersedes the launch.
pending?.rebase();
await deps.portalPlayer.openExternalPlayback(
const session = await deps.portalPlayer.openExternalPlayback(
playback,
event.player
);
// The page moved on, or a newer start took over, while the
// launch sat inside the player IPC: the player it opened
// must not stay beside what the viewer chose since.
if (session && (!stillSelected() || superseded())) {
await deps.externalPlayback.closeSession(session);
}
} catch (error) {
// A launch a newer start superseded fails on its own; the
// newer one reports for the movie now.
@@ -195,6 +201,10 @@ export function createStalkerVodDetailActions(
if (!playlistId || !Number.isFinite(vodId) || vodId <= 0) {
return;
}
// Counted as a pending start of this movie: its Play, Start over
// and launches are held until the write landed, or one made
// meanwhile would resume from the very row being cleared.
const pending = deps.beginPendingStart?.();
try {
await deps.playbackPositions.clearPlaybackPositionOrThrow(
playlistId,
@@ -204,6 +214,8 @@ export function createStalkerVodDetailActions(
} catch (error) {
deps.logError('Resetting the VOD position failed', error);
return;
} finally {
pending?.settle();
}
// The clear was async: only the movie still on screen loses its
// shown progress, and no older read may put it back.