diff --git a/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.spec.ts b/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.spec.ts index f74052cbd..3ba7e8835 100644 --- a/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.spec.ts +++ b/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.spec.ts @@ -606,7 +606,8 @@ describe('StreamResolverService', () => { _id: 'stalker-1', portalUrl: 'https://stalker.example.com/portal.php', macAddress: '00:11:22:33:44:55', - isFullStalkerPortal: false, + isFullStalkerPortal: true, + stalkerRequestRecipe: 'stateless-mac', } satisfies Partial) ); dataService.sendIpcEvent.mockResolvedValue({ @@ -668,7 +669,8 @@ describe('StreamResolverService', () => { autoRefresh: false, portalUrl: 'https://stalker.example.com/server/load.php', macAddress: '00:11:22:33:44:55', - isFullStalkerPortal: true, + isFullStalkerPortal: false, + stalkerRequestRecipe: 'full-session', userAgent: 'renderer-user-agent', referrer: 'https://stalker.example.com/c/', origin: 'https://stalker.example.com', @@ -693,7 +695,8 @@ describe('StreamResolverService', () => { expect(stalkerSession.requestForPlaylist).toHaveBeenCalledWith( expect.objectContaining({ _id: 'stalker-1', - isFullStalkerPortal: true, + isFullStalkerPortal: false, + stalkerRequestRecipe: 'full-session', }), STALKER_SESSION_APPLICATION_OPERATIONS.CreateLink, { diff --git a/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts b/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts index 1e9fc875d..531d81f7f 100644 --- a/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts +++ b/libs/portal/shared/data-access/src/lib/collection/stream-resolver.service.ts @@ -345,9 +345,6 @@ export class StreamResolverService { ...playlist, portalUrl, macAddress, - isFullStalkerPortal: - playlist?.isFullStalkerPortal ?? - this.stalkerSession.isFullStalkerPortal(portalUrl), }; const playbackLink = await fetchStalkerPlayback( { diff --git a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts index 7d067e643..d803a6d8b 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.spec.ts @@ -28,6 +28,7 @@ const PLAYLIST = { portalUrl: 'http://demo.example/stalker_portal/server/load.php', macAddress: '00:1A:79:00:00:01', isFullStalkerPortal: false, + stalkerRequestRecipe: 'stateless-mac', } as PlaylistMeta; const TestSeriesStore = signalStore( @@ -254,7 +255,8 @@ describe('withStalkerSeries serialSeasonsResource gating', () => { }); store.setCurrentPlaylist({ ...PLAYLIST, - isFullStalkerPortal: true, + isFullStalkerPortal: false, + stalkerRequestRecipe: 'full-session', }); store.setSelectedContentType('vod'); store.setSelectedItem({ @@ -271,8 +273,9 @@ describe('withStalkerSeries serialSeasonsResource gating', () => { expect(stalkerSession.requestForPlaylist).toHaveBeenCalledWith( expect.objectContaining({ _id: PLAYLIST._id, - isFullStalkerPortal: true, + isFullStalkerPortal: false, lastUsage: '', + stalkerRequestRecipe: 'full-session', }), STALKER_SESSION_APPLICATION_OPERATIONS.SeriesSeasons, { diff --git a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.ts b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.ts index c330113e5..7d131ce7e 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/features/with-stalker-series.feature.ts @@ -29,6 +29,7 @@ import { sortVodSeriesSeasonsByNumber, StalkerRequestDeps, toStalkerSessionPlaylist, + usesTypedStalkerSession, } from '../utils'; /** @@ -83,10 +84,7 @@ async function fetchVodSeriesSeasons( playlist: PlaylistMeta, seriesId: string | number ): Promise { - if ( - playlist.isFullStalkerPortal && - supportsTypedSessions(deps.stalkerSession) - ) { + if (usesTypedStalkerSession(deps, playlist)) { const result = await deps.stalkerSession.requestForPlaylist( toStalkerSessionPlaylist(playlist), STALKER_SESSION_APPLICATION_OPERATIONS.SeriesSeasons, @@ -118,15 +116,6 @@ async function fetchVodSeriesSeasons( return extractSeriesItems(response); } -function supportsTypedSessions(stalkerSession: StalkerSessionService): boolean { - const probe = ( - stalkerSession as Partial< - Pick - > - ).supportsTypedSessions; - return typeof probe !== 'function' || probe.call(stalkerSession); -} - export function withStalkerSeries() { const logger = createLogger('withStalkerSeries'); diff --git a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts index 491641c84..f6fc8df8c 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.spec.ts @@ -22,11 +22,13 @@ const PLAYLIST = { portalUrl: 'http://demo.example/stalker_portal/server/load.php', macAddress: '00:1A:79:00:00:01', isFullStalkerPortal: false, + stalkerRequestRecipe: 'stateless-mac', } as PlaylistMeta; const FULL_PLAYLIST = { ...PLAYLIST, isFullStalkerPortal: true, + stalkerRequestRecipe: 'full-session', } as PlaylistMeta; describe('stalker-player-request.utils', () => { @@ -135,6 +137,57 @@ describe('stalker-player-request.utils', () => { }); }); + it('uses the full-session recipe when the legacy compatibility flag is stale', async () => { + stalkerSession.requestForPlaylist.mockResolvedValue({ + streamUrl: '/media/video_77.mpg', + playbackContextRef: 'playback-context-1', + }); + + await fetchStalkerPlayback( + { + dataService: dataService as never, + stalkerSession: + stalkerSession as unknown as StalkerSessionService, + }, + { + playlist: { + ...FULL_PLAYLIST, + isFullStalkerPortal: false, + }, + selectedContentType: 'itv', + cmd: '/media/source.mpg', + } + ); + + expect(stalkerSession.requestForPlaylist).toHaveBeenCalled(); + expect(dataService.sendIpcEvent).not.toHaveBeenCalled(); + }); + + it('uses the stateless recipe when the legacy compatibility flag is stale', async () => { + dataService.sendIpcEvent.mockResolvedValue({ + js: { cmd: '/media/video_77.mpg' }, + }); + + await fetchStalkerPlayback( + { + dataService: dataService as never, + stalkerSession: + stalkerSession as unknown as StalkerSessionService, + }, + { + playlist: { + ...PLAYLIST, + isFullStalkerPortal: true, + }, + selectedContentType: 'itv', + cmd: '/media/source.mpg', + } + ); + + expect(dataService.sendIpcEvent).toHaveBeenCalled(); + expect(stalkerSession.requestForPlaylist).not.toHaveBeenCalled(); + }); + it('keeps a full-portal PWA request on the exact legacy IPC payload', async () => { stalkerSession.supportsTypedSessions.mockReturnValue(false); dataService.sendIpcEvent.mockResolvedValue({ diff --git a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.ts b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.ts index 065b49b2c..58fe6f32d 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-player-request.utils.ts @@ -12,6 +12,7 @@ import { StalkerContentType } from '../stalker-store.contracts'; import { executeStalkerRequest, toStalkerSessionPlaylist, + usesTypedStalkerSession, } from './stalker-request.utils'; export interface StalkerPlayerResponse { @@ -157,10 +158,7 @@ export async function fetchStalkerPlayback( ): Promise { const contentType = options.forcedContentType ?? options.selectedContentType; - if ( - options.playlist.isFullStalkerPortal && - supportsTypedSessions(deps.stalkerSession) - ) { + if (usesTypedStalkerSession(deps, options.playlist)) { const isEpisode = options.series !== undefined; const payload = await deps.stalkerSession.requestForPlaylist( toStalkerSessionPlaylist(options.playlist), @@ -231,24 +229,12 @@ function normalizePlaybackLink( }; } -function supportsTypedSessions(stalkerSession: StalkerSessionService): boolean { - const probe = ( - stalkerSession as Partial< - Pick - > - ).supportsTypedSessions; - return typeof probe !== 'function' || probe.call(stalkerSession); -} - export async function fetchStalkerMovieFileId( deps: StalkerPlayerRequestDeps, playlist: PlaylistMeta, movieId: string ): Promise { - if ( - playlist.isFullStalkerPortal && - supportsTypedSessions(deps.stalkerSession) - ) { + if (usesTypedStalkerSession(deps, playlist)) { const result = await deps.stalkerSession.requestForPlaylist( toStalkerSessionPlaylist(playlist), STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems, diff --git a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.spec.ts b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.spec.ts index 408c98765..c5fa40a50 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.spec.ts @@ -32,6 +32,7 @@ function fullPlaylist(): PlaylistMeta { isFullStalkerPortal: true, macAddress: 'has-mac-address', portalUrl: 'https://portal.example.test/stalker_portal/server/load.php', + stalkerRequestRecipe: 'full-session', stalkerDeviceId1: 'has-device-id-1', stalkerDeviceId2: 'has-device-id-2', stalkerSerialNumber: 'has-serial', @@ -67,6 +68,7 @@ describe('executeStalkerRequest', () => { isFullStalkerPortal: false, macAddress: 'has-mac-address', portalUrl: 'https://portal.example.test/load.php', + stalkerRequestRecipe: 'stateless-mac', title: 'Basic Stalker Portal', } as PlaylistMeta; @@ -89,6 +91,52 @@ describe('executeStalkerRequest', () => { ).not.toHaveBeenCalled(); }); + it('lets the verified full-session recipe override a stale false compatibility flag', async () => { + const deps = createDeps(true); + const playlist = { + ...fullPlaylist(), + isFullStalkerPortal: false, + }; + + await executeStalkerRequest(deps, playlist, CATEGORY_PARAMS); + + expect( + deps.stalkerSession.makeAuthenticatedRequest + ).toHaveBeenCalled(); + expect(deps.dataService.sendIpcEvent).not.toHaveBeenCalled(); + }); + + it('lets the verified stateless recipe override a stale true compatibility flag', async () => { + const deps = createDeps(true); + const playlist = { + ...fullPlaylist(), + isFullStalkerPortal: true, + stalkerRequestRecipe: 'stateless-mac' as const, + }; + + await executeStalkerRequest(deps, playlist, CATEGORY_PARAMS); + + expect(deps.dataService.sendIpcEvent).toHaveBeenCalled(); + expect( + deps.stalkerSession.makeAuthenticatedRequest + ).not.toHaveBeenCalled(); + }); + + it('does not infer a typed runtime path from the legacy boolean when recipe migration is still pending', async () => { + const deps = createDeps(true); + const playlist = { + ...fullPlaylist(), + stalkerRequestRecipe: undefined, + }; + + await executeStalkerRequest(deps, playlist, CATEGORY_PARAMS); + + expect(deps.dataService.sendIpcEvent).toHaveBeenCalled(); + expect( + deps.stalkerSession.makeAuthenticatedRequest + ).not.toHaveBeenCalled(); + }); + it('keeps the PWA adapter byte-compatible on STALKER_REQUEST even for a persisted full-portal row', async () => { const deps = createDeps(false); const playlist = fullPlaylist(); diff --git a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts index 8b577ced3..895c0573f 100644 --- a/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts +++ b/libs/portal/stalker/data-access/src/lib/stores/utils/stalker-request.utils.ts @@ -42,7 +42,7 @@ export function usesTypedStalkerSession( playlist: PlaylistMeta ): boolean { return Boolean( - playlist.isFullStalkerPortal && + playlist.stalkerRequestRecipe === 'full-session' && supportsTypedSessions(deps.stalkerSession) ); } diff --git a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search-request.utils.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search-request.utils.spec.ts index 827f12caf..84f8c419e 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search-request.utils.spec.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search-request.utils.spec.ts @@ -32,17 +32,21 @@ function playlist(portalUrl: string): Playlist { } describe('executeStalkerSearchRequest', () => { - it('routes a detected full portal through the typed session facade', async () => { + it('routes a verified full-session recipe through the typed session facade despite a stale flag', async () => { const dataService = { sendIpcEvent: jest.fn(), }; const stalkerSession = { - isFullStalkerPortal: jest.fn().mockReturnValue(true), supportsTypedSessions: jest.fn().mockReturnValue(true), makeAuthenticatedRequest: jest .fn() .mockResolvedValue({ js: { data: [] } }), }; + const fullPlaylist = { + ...playlist('https://portal.example/custom/api.php'), + isFullStalkerPortal: false, + stalkerRequestRecipe: 'full-session' as const, + }; await executeStalkerSearchRequest( { @@ -50,14 +54,15 @@ describe('executeStalkerSearchRequest', () => { stalkerSession: stalkerSession as unknown as StalkerSessionService, }, - playlist('https://portal.example/server/load.php'), + fullPlaylist, REQUEST_PARAMS ); expect(stalkerSession.makeAuthenticatedRequest).toHaveBeenCalledWith( expect.objectContaining({ _id: 'stalker-1', - isFullStalkerPortal: true, + isFullStalkerPortal: false, + stalkerRequestRecipe: 'full-session', }), REQUEST_PARAMS ); @@ -69,11 +74,14 @@ describe('executeStalkerSearchRequest', () => { sendIpcEvent: jest.fn().mockResolvedValue({ js: { data: [] } }), }; const stalkerSession = { - isFullStalkerPortal: jest.fn().mockReturnValue(false), supportsTypedSessions: jest.fn().mockReturnValue(true), makeAuthenticatedRequest: jest.fn(), }; - const simplePlaylist = playlist('https://portal.example/portal.php'); + const simplePlaylist = { + ...playlist('https://portal.example/server/load.php'), + isFullStalkerPortal: true, + stalkerRequestRecipe: 'stateless-mac' as const, + }; await executeStalkerSearchRequest( { diff --git a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search-request.utils.ts b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search-request.utils.ts index 944aa364c..899ea7d7b 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search-request.utils.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search-request.utils.ts @@ -9,18 +9,5 @@ export async function executeStalkerSearchRequest( playlist: Playlist, params: Record ): Promise { - const isFullPortal = - playlist.isFullStalkerPortal ?? - deps.stalkerSession.isFullStalkerPortal(playlist.portalUrl ?? ''); - - return executeStalkerRequest( - deps, - isFullPortal - ? { - ...playlist, - isFullStalkerPortal: true, - } - : playlist, - params - ); + return executeStalkerRequest(deps, playlist, params); }