feat(stalker): recover typed playlist requests

This commit is contained in:
4gray committed 2026-07-27 11:12:02 +02:00
1 parent 758c1cb77a
commit 2125e5a907
2 files changed
+70 -39

No files matched your search

@@ -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',
@@ -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<Operation>
): Promise<StalkerSessionOperationResult<Operation>> {
let leaseRef = await this.resolveLease(playlist);
let outcome = await this.request({
leaseRef,
operation,
parameters,
} as StalkerSessionRequest<Operation>);
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<Operation>);
}
}
if (outcome.kind !== 'success') {
throw new StalkerSessionOutcomeError(outcome);
}
return outcome.payload;
}
async control(
request: StalkerSessionControlRequest
): Promise<StalkerSessionControlOutcome> {
@@ -293,13 +328,15 @@ export class StalkerSessionService {
parameters: Readonly<Record<string, string | number>>
): Promise<T> {
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<unknown> {
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,