mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
fix(playback): claim radio header ownership before awaiting the IPC
Codex round-2 P2: leaving the radio route while the header IPC was still in flight left the portal cookie/token installed — ngOnDestroy saw a null scope URL (it was recorded only after the await) and could not clear the override. Ownership is now claimed synchronously before awaiting, destroy invalidates the pending playback continuation, and the apply's stillCurrent verdict is honored. Regression test covers destroy-during-pending-IPC. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c2d1bff89d
commit
4c0be3fde5
2 files changed
+60
-4
No files matched your search
+46
@@ -1168,6 +1168,52 @@ describe('StalkerLiveStreamLayoutComponent', () => {
|
||||
).componentInstance as StubAudioPlayerComponent;
|
||||
expect(audioPlayer.url()).toBe('http://portal.example/radio_2.mpg');
|
||||
});
|
||||
|
||||
it('clears the radio header override when destroyed while the apply IPC is pending', async () => {
|
||||
// Leaving the radio route mid-apply must not leave the portal
|
||||
// cookie/token installed: ownership is claimed before awaiting the
|
||||
// IPC, so ngOnDestroy can always name the stream to clear.
|
||||
stalkerStore.selectedContentType.set('radio');
|
||||
selectedCategoryId.set('radio-all');
|
||||
selectedItem.set(null);
|
||||
selectedItvId.set(undefined);
|
||||
fixture.detectChanges();
|
||||
let resolveApply: (() => void) | undefined;
|
||||
let signalApplyIssued!: () => void;
|
||||
const applyIssued = new Promise<void>(
|
||||
(resolve) => (signalApplyIssued = resolve)
|
||||
);
|
||||
// Once: only the apply call gets the pending promise — the destroy
|
||||
// clear below also calls the bridge and must not steal the resolver.
|
||||
(
|
||||
window.electron?.setUserAgent as jest.Mock
|
||||
).mockImplementationOnce(() => {
|
||||
signalApplyIssued();
|
||||
return new Promise<boolean>((resolve) => {
|
||||
resolveApply = () => resolve(true);
|
||||
});
|
||||
});
|
||||
resolveRadioPlayback.mockResolvedValue({
|
||||
streamUrl: 'http://portal.example/radio_2.mpg',
|
||||
title: 'Portal FM',
|
||||
headers: { Cookie: 'mac=00:1A:79:00:00:01' },
|
||||
});
|
||||
|
||||
const playPromise = component.playChannel(radioChannels()[0]);
|
||||
// The header IPC has been issued but is still pending.
|
||||
await applyIssued;
|
||||
|
||||
fixture.destroy();
|
||||
resolveApply?.();
|
||||
await playPromise;
|
||||
|
||||
expect(window.electron?.setUserAgent).toHaveBeenLastCalledWith(
|
||||
undefined,
|
||||
undefined,
|
||||
'http://portal.example/radio_2.mpg'
|
||||
);
|
||||
expect(component.activePlayback()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
function buildProgram(channelId: string, title: string): EpgProgram {
|
||||
|
||||
+14
-4
@@ -560,8 +560,11 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
this.unsubscribeRemoteChannelChange?.();
|
||||
this.unsubscribeRemoteCommand?.();
|
||||
this.removeScrollListener();
|
||||
// Radio credentials must not outlive this layout; the service no-ops
|
||||
// when a newer playback already owns the override slot.
|
||||
// Invalidate any playback continuation still awaiting its header
|
||||
// IPC, then drop the radio credentials — they must not outlive this
|
||||
// layout. The service no-ops when a newer playback already owns the
|
||||
// override slot.
|
||||
this.playbackRequestId += 1;
|
||||
this.streamHeaders.clear(this.radioHeaderScopeUrl);
|
||||
}
|
||||
|
||||
@@ -598,14 +601,21 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
// WebPlayerViewComponent, so the scoped Electron header
|
||||
// override (portal cookie/token for auth-gated streams) must
|
||||
// be configured here BEFORE the audio element gets the URL.
|
||||
await this.streamHeaders.apply(playback);
|
||||
// Ownership is claimed synchronously, before awaiting the
|
||||
// IPC: if this layout is destroyed while the apply is still
|
||||
// in flight, ngOnDestroy must already know which stream's
|
||||
// override to clear — otherwise the credentials would
|
||||
// outlive the route.
|
||||
const headerSync = this.streamHeaders.apply(playback);
|
||||
this.radioHeaderScopeUrl = playback.streamUrl;
|
||||
const stillCurrent = headerSync ? await headerSync : true;
|
||||
if (
|
||||
!stillCurrent ||
|
||||
requestId !== this.playbackRequestId ||
|
||||
this.selectedChannelId() !== channelId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.radioHeaderScopeUrl = playback.streamUrl;
|
||||
this.activePlayback.set(playback);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user