mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
fix(portals): a reopened Stalker movie is not held by a start its earlier visit left pending
Backing out of a movie while its portal request still resolved and reopening it found Play, Start over and the launches held: the pending start was keyed by the movie alone, so the new visit matched it until the request settled, if ever. The pending start of a movie the page left is retired on every owner change, in the catalog detail, the search detail and the collection detail. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8a34570a3
commit
2408eaa6c5
9 files changed
+104
-1
No files matched your search
@@ -16,6 +16,23 @@ describe('createPendingPlaybackStart', () => {
|
||||
expect(pendingForA()).toBe(false);
|
||||
});
|
||||
|
||||
it('forgets a start whose owner the page left, so a return is not held by it', () => {
|
||||
const tracker = createPendingPlaybackStart<string>();
|
||||
const first = tracker.begin('a');
|
||||
|
||||
// Back out of the movie while its request still resolves: whatever
|
||||
// it settles with no longer applies, so reopening it starts afresh.
|
||||
tracker.retire('a');
|
||||
expect(tracker.isPendingFor('a')).toBe(false);
|
||||
|
||||
// The retired start settling late must not disturb a newer one.
|
||||
const newer = tracker.begin('a');
|
||||
tracker.settle(first);
|
||||
expect(tracker.isPendingFor('a')).toBe(true);
|
||||
tracker.settle(newer);
|
||||
expect(tracker.isPendingFor('a')).toBe(false);
|
||||
});
|
||||
|
||||
it('lets only the latest start clear the flag', () => {
|
||||
const tracker = createPendingPlaybackStart<string>();
|
||||
const older = tracker.begin('a');
|
||||
|
||||
@@ -17,6 +17,12 @@ export interface PendingPlaybackStart<TOwner> {
|
||||
settle(startId: number): void;
|
||||
/** Reactive: whether the latest start belongs to this owner. */
|
||||
isPendingFor(owner: TOwner): boolean;
|
||||
/**
|
||||
* Forgets the start of `owner` once the page left it: whatever it still
|
||||
* settles with no longer applies, and a return to the same owner must
|
||||
* not find its actions held by a request that may never settle.
|
||||
*/
|
||||
retire(owner: TOwner): void;
|
||||
}
|
||||
|
||||
export function createPendingPlaybackStart<
|
||||
@@ -40,5 +46,10 @@ export function createPendingPlaybackStart<
|
||||
const current = pending();
|
||||
return current !== null && current.owner === owner;
|
||||
},
|
||||
retire(owner) {
|
||||
if (pending()?.owner === owner) {
|
||||
pending.set(null);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
+35
@@ -380,6 +380,41 @@ describe('StalkerCatalogDetailComponent provider presentation', () => {
|
||||
expect(fixture.componentInstance.playbackStartPending()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not hold a reopened movie with a start its earlier visit left pending', async () => {
|
||||
const original = selectedItem();
|
||||
let resolve: (value: unknown) => void = () => undefined;
|
||||
resolveVodPlayback.mockReturnValueOnce(
|
||||
new Promise((resolvePromise) => {
|
||||
resolve = resolvePromise;
|
||||
})
|
||||
);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
fixture.componentInstance.onVodPlay({
|
||||
type: 'stalker',
|
||||
cmd: '/media/42',
|
||||
data: original,
|
||||
} as never);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.playbackStartPending()).toBe(true);
|
||||
|
||||
// Back out to another movie while the portal request hangs, then
|
||||
// reopen the first one: its Play must not wait for that request.
|
||||
selectedItem.set({
|
||||
id: '99',
|
||||
cmd: '/media/99',
|
||||
info: { name: 'Replacement movie' },
|
||||
});
|
||||
fixture.detectChanges();
|
||||
selectedItem.set(original);
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.playbackStartPending()).toBe(false);
|
||||
|
||||
resolve({ streamUrl: 'https://stale.example/movie.mpg' });
|
||||
await fixture.whenStable();
|
||||
expect(fixture.componentInstance.playbackStartPending()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not mount a VOD resolution after the catalog owner changes', async () => {
|
||||
let resolve!: (value: { streamUrl: string }) => void;
|
||||
resolveVodPlayback.mockReturnValueOnce(
|
||||
|
||||
+3
@@ -216,6 +216,9 @@ export class StalkerCatalogDetailComponent implements OnDestroy {
|
||||
effect(() => {
|
||||
const ownerKey = this.playbackOwnerKey();
|
||||
if (ownerKey === this.currentPlaybackOwnerKey) return;
|
||||
// A start the left movie still resolves no longer applies; a
|
||||
// return to it must not find Play held by a hung request.
|
||||
this.pendingStart.retire(this.currentPlaybackOwnerKey);
|
||||
this.currentPlaybackOwnerKey = ownerKey;
|
||||
this.closeInlinePlayer();
|
||||
});
|
||||
|
||||
@@ -213,6 +213,11 @@ export class StalkerCollectionDetailComponent implements ViewInPortalHandoff {
|
||||
const playbackOwnerKey = this.playbackSessionKey();
|
||||
untracked(() => {
|
||||
if (playbackOwnerKey !== this.currentPlaybackOwnerKey) {
|
||||
// A start the left item still resolves no longer
|
||||
// applies; a return to it must not find Play held.
|
||||
this.playback.retirePendingStart(
|
||||
this.currentPlaybackOwnerKey
|
||||
);
|
||||
this.currentPlaybackOwnerKey = playbackOwnerKey;
|
||||
this.closeInlinePlayer();
|
||||
}
|
||||
|
||||
@@ -175,6 +175,11 @@ export class StalkerCollectionPlaybackController {
|
||||
this.vodPlayback.handleInlineTimeUpdate(event);
|
||||
}
|
||||
|
||||
/** The page left `owner`: a start it still resolves no longer holds a return to it. */
|
||||
retirePendingStart(owner: string): void {
|
||||
this.vodPlayback.retirePendingStart(owner);
|
||||
}
|
||||
|
||||
closeInlinePlayer(): void {
|
||||
this.vodPlayback.closeInlinePlayer();
|
||||
}
|
||||
|
||||
@@ -856,6 +856,9 @@ export class StalkerSearchComponent {
|
||||
|
||||
private syncPlaybackOwner(ownerKey: string): void {
|
||||
if (ownerKey === this.currentPlaybackOwnerKey) return;
|
||||
// A start the left movie still resolves no longer applies; a return
|
||||
// to it must not find Play held by a hung request.
|
||||
this.vodPlayback.retirePendingStart(this.currentPlaybackOwnerKey);
|
||||
this.currentPlaybackOwnerKey = ownerKey;
|
||||
this.closeInlinePlayer();
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ function createPosition(
|
||||
};
|
||||
}
|
||||
|
||||
function createController() {
|
||||
function createController(playbackOwnerKey?: () => string) {
|
||||
const inlinePlayback = signal(null);
|
||||
const selectedVodPosition = signal<PlaybackPositionData | null>(null);
|
||||
const playbackPositions = {
|
||||
@@ -79,6 +79,7 @@ function createController() {
|
||||
translateService,
|
||||
logger,
|
||||
playbackErrorLogMessage: 'Playback failed',
|
||||
playbackOwnerKey,
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -120,6 +121,24 @@ describe('StalkerVodPlaybackController', () => {
|
||||
expect(selectedVodPosition()?.positionSeconds).toBe(20);
|
||||
});
|
||||
|
||||
it('does not hold a reopened movie with a start its earlier visit left pending', () => {
|
||||
let owner = 'movie-a';
|
||||
const { controller } = createController(() => owner);
|
||||
const pending = controller.beginPendingStart();
|
||||
expect(controller.playbackStartPending()).toBe(true);
|
||||
|
||||
// Back out of the movie while the portal request hangs, then
|
||||
// reopen it: the new visit must be playable.
|
||||
owner = '';
|
||||
controller.retirePendingStart('movie-a');
|
||||
owner = 'movie-a';
|
||||
expect(controller.playbackStartPending()).toBe(false);
|
||||
expect(pending.isCurrent()).toBe(true);
|
||||
|
||||
pending.settle();
|
||||
expect(controller.playbackStartPending()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not mount playback that resolves after the detail closes', async () => {
|
||||
const { controller, inlinePlayback } = createController();
|
||||
const pending = createDeferred<ResolvedPortalPlayback>();
|
||||
|
||||
@@ -38,6 +38,11 @@ export class StalkerVodPlaybackController {
|
||||
readonly playbackStartPending = computed(() =>
|
||||
this.pendingStart.isPendingFor(this.config.playbackOwnerKey?.())
|
||||
);
|
||||
|
||||
/** The page left `owner`: a start it still resolves no longer holds a return to it. */
|
||||
retirePendingStart(owner: string | undefined): void {
|
||||
this.pendingStart.retire(owner);
|
||||
}
|
||||
private lastInlineSaveTime = 0;
|
||||
private loadSelectedVodPositionRequestId = 0;
|
||||
private playbackRequestId = 0;
|
||||
|
||||
Reference in new issue
Block a user