From 2125e5a90732ac817c3df64373743aafa442da5d Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 27 Jul 2026 11:12:02 +0200 Subject: [PATCH] feat(stalker): recover typed playlist requests --- .../src/lib/stalker-session.service.spec.ts | 27 ++++++ .../src/lib/stalker-session.service.ts | 82 ++++++++++--------- 2 files changed, 70 insertions(+), 39 deletions(-) diff --git a/libs/portal/stalker/data-access/src/lib/stalker-session.service.spec.ts b/libs/portal/stalker/data-access/src/lib/stalker-session.service.spec.ts index c7702e001..29bf677a2 100644 --- a/libs/portal/stalker/data-access/src/lib/stalker-session.service.spec.ts +++ b/libs/portal/stalker/data-access/src/lib/stalker-session.service.spec.ts @@ -434,6 +434,33 @@ describe('StalkerSessionService', () => { }); }); + it('returns the typed create-link payload without dropping its opaque playback context', async () => { + bridge.stalkerSessionOpen.mockResolvedValue(READY); + bridge.stalkerSessionRequest.mockResolvedValue({ + kind: 'success', + operation: STALKER_SESSION_APPLICATION_OPERATIONS.CreateLink, + payload: { + playbackContextRef: 'opaque-playback-context-1', + streamUrl: 'https://stream.example/live/1', + }, + requestId: 'request-create-link-1', + }); + + await expect( + service.requestForPlaylist( + PLAYLIST, + STALKER_SESSION_APPLICATION_OPERATIONS.CreateLink, + { + command: 'ffmpeg https://portal.example/live/1', + contentType: 'itv', + } + ) + ).resolves.toEqual({ + playbackContextRef: 'opaque-playback-context-1', + streamUrl: 'https://stream.example/live/1', + }); + }); + it('joins concurrent route recovery while preserving one retry per original operation', async () => { const transition: StalkerSessionRequestOutcome = { kind: 'failure', diff --git a/libs/portal/stalker/data-access/src/lib/stalker-session.service.ts b/libs/portal/stalker/data-access/src/lib/stalker-session.service.ts index 8188b3a4b..15e8f01eb 100644 --- a/libs/portal/stalker/data-access/src/lib/stalker-session.service.ts +++ b/libs/portal/stalker/data-access/src/lib/stalker-session.service.ts @@ -10,11 +10,12 @@ import { type StalkerSessionControlOutcome, type StalkerSessionControlRequest, type StalkerSessionLeaseRef, + type StalkerSessionOperationParameters, + type StalkerSessionOperationResult, type StalkerSessionRequest, type StalkerSessionRequestOutcome, } from '@iptvnator/shared/interfaces'; import { - type StalkerAdaptedRequest, adaptLegacyStalkerRequest, mapStalkerSessionResultToLegacyResponse, } from './stalker-request-adapter'; @@ -192,6 +193,40 @@ export class StalkerSessionService { return this.requireBridge().stalkerSessionRequest(request); } + async requestForPlaylist< + Operation extends StalkerSessionApplicationOperation, + >( + playlist: Playlist, + operation: Operation, + parameters: StalkerSessionOperationParameters + ): Promise> { + let leaseRef = await this.resolveLease(playlist); + let outcome = await this.request({ + leaseRef, + operation, + parameters, + } as StalkerSessionRequest); + if ( + outcome.kind !== 'success' && + this.isRecoverable(outcome) && + (await this.recover(playlist, outcome, 'request')) + ) { + const recoveredLease = this.getLeaseRef(playlist._id); + if (recoveredLease !== undefined) { + leaseRef = recoveredLease; + outcome = await this.request({ + leaseRef, + operation, + parameters, + } as StalkerSessionRequest); + } + } + if (outcome.kind !== 'success') { + throw new StalkerSessionOutcomeError(outcome); + } + return outcome.payload; + } + async control( request: StalkerSessionControlRequest ): Promise { @@ -293,13 +328,15 @@ export class StalkerSessionService { parameters: Readonly> ): Promise { const adapted = adaptLegacyStalkerRequest(parameters); - const leaseRef = await this.resolveLease(playlist); - const result = await this.executeAdaptedRequest( + const result = await this.requestForPlaylist( playlist, - leaseRef, - adapted + adapted.operation, + adapted.parameters ); - return result as T; + return mapStalkerSessionResultToLegacyResponse( + adapted.operation, + result + ) as T; } private async resolveLease( @@ -349,39 +386,6 @@ export class StalkerSessionService { }); } - private async executeAdaptedRequest( - playlist: Playlist, - leaseRef: StalkerSessionLeaseRef, - adaptedRequest: StalkerAdaptedRequest - ): Promise { - let outcome = await this.request({ - leaseRef, - operation: adaptedRequest.operation, - parameters: adaptedRequest.parameters, - } as StalkerSessionRequest); - if ( - outcome.kind !== 'success' && - this.isRecoverable(outcome) && - (await this.recover(playlist, outcome, 'request')) - ) { - const recoveredLease = this.getLeaseRef(playlist._id); - if (recoveredLease !== undefined) { - outcome = await this.request({ - leaseRef: recoveredLease, - operation: adaptedRequest.operation, - parameters: adaptedRequest.parameters, - } as StalkerSessionRequest); - } - } - if (outcome.kind !== 'success') { - throw new StalkerSessionOutcomeError(outcome); - } - return mapStalkerSessionResultToLegacyResponse( - outcome.operation, - outcome.payload - ); - } - private recover( playlist: Playlist, outcome: StalkerSessionNonSuccessOutcome,