fix(stalker): correlate session operation contracts

This commit is contained in:
4gray committed 2026-07-27 03:16:16 +02:00
1 parent 29fa4d0f87
commit f8a544d287
3 files changed
+324 -46

No files matched your search

@@ -11,10 +11,13 @@ import {
} from './ipc-command.class';
import {
STALKER_SESSION_APPLICATION_OPERATIONS,
type StalkerSessionApplicationOperation,
type StalkerSessionContinueRequest,
type StalkerSessionControlRequest,
type StalkerSessionOpenRequest,
type StalkerSessionOperationContractMap,
type StalkerSessionRequest,
type StalkerSessionRequestOutcome,
} from './stalker-session.interface';
describe('typed IPC commands', () => {
@@ -96,7 +99,7 @@ describe('typed IPC commands', () => {
const response: IpcCommandResponse<typeof STALKER_SESSION_REQUEST> = {
kind: 'success',
operation: STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems,
payload: { js: { data: [] } },
payload: { items: [], page: 1, total: 0 },
requestId: 'request-id',
};
@@ -104,6 +107,41 @@ describe('typed IPC commands', () => {
expect(response.kind).toBe('success');
});
it('correlates each operation with its request and sanitized result', () => {
type MissingOperations = Exclude<
StalkerSessionApplicationOperation,
keyof StalkerSessionOperationContractMap
>;
type ExtraOperations = Exclude<
keyof StalkerSessionOperationContractMap,
StalkerSessionApplicationOperation
>;
const mapIsExhaustive: [
MissingOperations,
ExtraOperations,
] extends [never, never]
? true
: false = true;
const mismatchedRequest: StalkerSessionRequest = {
leaseRef: 'opaque-lease',
operation: STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems,
// @ts-expect-error Catalog requests cannot use EPG parameters.
parameters: { channelId: 'channel-1' },
};
const mismatchedResult: StalkerSessionRequestOutcome = {
kind: 'success',
operation: STALKER_SESSION_APPLICATION_OPERATIONS.CreateLink,
// @ts-expect-error create-link cannot return a catalog collection.
payload: { items: [] },
requestId: 'request-id',
};
expect(mapIsExhaustive).toBe(true);
expect(mismatchedRequest.operation).toBe('get-ordered-list');
expect(mismatchedResult.kind).toBe('success');
});
it('maps STALKER_SESSION_CONTROL to exactly one request and response type', () => {
const request: IpcCommandRequest<typeof STALKER_SESSION_CONTROL> = {
action: 'commit',
@@ -0,0 +1,259 @@
export const STALKER_SESSION_APPLICATION_OPERATIONS = {
CatalogCategories: 'get-categories',
CatalogGenres: 'get-genres',
CatalogItems: 'get-ordered-list',
CatalogAllChannels: 'get-all-channels',
CatalogSearch: 'search',
SeriesSeasons: 'get-series-seasons',
SeriesEpisodes: 'get-series-episodes',
ShortEpg: 'get-short-epg',
EpgInfo: 'get-epg-info',
CreateLink: 'create-link',
Favorites: 'favorites',
MainInfo: 'get-main-info',
} as const;
export type StalkerSessionApplicationOperation =
(typeof STALKER_SESSION_APPLICATION_OPERATIONS)[keyof typeof STALKER_SESSION_APPLICATION_OPERATIONS];
export const STALKER_SESSION_CONTENT_TYPES = {
Itv: 'itv',
Radio: 'radio',
Series: 'series',
Vod: 'vod',
} as const;
export type StalkerSessionContentType =
(typeof STALKER_SESSION_CONTENT_TYPES)[keyof typeof STALKER_SESSION_CONTENT_TYPES];
export type StalkerSessionEntityId = string | number;
export type StalkerSessionNoParameters = Readonly<Record<string, never>>;
export interface StalkerSessionCategoryParameters {
contentType: 'radio' | 'series' | 'vod';
}
export type StalkerSessionGenreParameters = StalkerSessionNoParameters;
export interface StalkerSessionCatalogParameters {
contentType?: StalkerSessionContentType;
category?: StalkerSessionEntityId;
genre?: StalkerSessionEntityId;
page?: number;
query?: string;
sortBy?: string;
itemId?: StalkerSessionEntityId;
seasonId?: StalkerSessionEntityId;
}
export type StalkerSessionAllChannelsParameters = StalkerSessionNoParameters;
export interface StalkerSessionSearchParameters {
contentType: StalkerSessionContentType;
query: string;
category?: StalkerSessionEntityId;
page?: number;
}
export interface StalkerSessionSeriesSeasonsParameters {
seriesId: StalkerSessionEntityId;
}
export interface StalkerSessionSeriesEpisodesParameters {
seriesId: StalkerSessionEntityId;
seasonId: StalkerSessionEntityId;
}
export interface StalkerSessionShortEpgParameters {
channelId: StalkerSessionEntityId;
limit?: number;
}
export interface StalkerSessionEpgInfoParameters {
fromTimestamp?: number;
toTimestamp?: number;
}
export interface StalkerSessionCreateLinkParameters {
contentType: StalkerSessionContentType | 'episode';
command: string;
itemId?: StalkerSessionEntityId;
seriesId?: StalkerSessionEntityId;
seasonNumber?: number;
episodeNumber?: number;
}
export interface StalkerSessionFavoritesParameters {
contentType: StalkerSessionContentType;
itemId: StalkerSessionEntityId;
favorite: boolean;
}
export type StalkerSessionMainInfoParameters = StalkerSessionNoParameters;
export interface StalkerSessionCategory {
id: StalkerSessionEntityId;
name: string;
alias?: string;
parentId?: StalkerSessionEntityId;
}
export interface StalkerSessionCatalogItem {
id: StalkerSessionEntityId;
contentType: StalkerSessionContentType;
name?: string;
title?: string;
description?: string;
command?: string;
posterUrl?: string;
categoryId?: StalkerSessionEntityId;
genreId?: StalkerSessionEntityId;
year?: number;
rating?: number;
durationSeconds?: number;
isSeries?: boolean;
}
export interface StalkerSessionCatalogResult {
items: readonly StalkerSessionCatalogItem[];
page?: number;
total?: number;
hasMore?: boolean;
}
export interface StalkerSessionCategoriesResult {
items: readonly StalkerSessionCategory[];
}
export interface StalkerSessionSeason {
id: StalkerSessionEntityId;
number: number;
title?: string;
}
export interface StalkerSessionSeasonsResult {
seasons: readonly StalkerSessionSeason[];
}
export interface StalkerSessionEpisode {
id: StalkerSessionEntityId;
seriesId: StalkerSessionEntityId;
seasonId: StalkerSessionEntityId;
seasonNumber: number;
episodeNumber: number;
title?: string;
command?: string;
thumbnailUrl?: string;
}
export interface StalkerSessionEpisodesResult {
episodes: readonly StalkerSessionEpisode[];
}
export interface StalkerSessionEpgProgram {
id?: StalkerSessionEntityId;
channelId: StalkerSessionEntityId;
title: string;
description?: string;
startsAt: number;
endsAt: number;
}
export interface StalkerSessionEpgResult {
programs: readonly StalkerSessionEpgProgram[];
}
export interface StalkerSessionCreateLinkResult {
streamUrl: string;
playbackContextRef?: string;
title?: string;
thumbnail?: string | null;
isLive?: boolean;
userAgent?: string;
referer?: string;
origin?: string;
}
export interface StalkerSessionFavoritesResult {
success: boolean;
favorite: boolean;
}
export interface StalkerSessionAccountSummary {
name?: string;
status?: string;
tariffPlan?: string;
accountBalance?: string;
expiresAt?: string;
}
export interface StalkerSessionMainInfoResult {
account: StalkerSessionAccountSummary;
}
export interface StalkerSessionOperationContract<
Parameters,
Result,
> {
parameters: Parameters;
result: Result;
}
export interface StalkerSessionOperationContractMap {
[STALKER_SESSION_APPLICATION_OPERATIONS.CatalogCategories]: StalkerSessionOperationContract<
StalkerSessionCategoryParameters,
StalkerSessionCategoriesResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.CatalogGenres]: StalkerSessionOperationContract<
StalkerSessionGenreParameters,
StalkerSessionCategoriesResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.CatalogItems]: StalkerSessionOperationContract<
StalkerSessionCatalogParameters,
StalkerSessionCatalogResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.CatalogAllChannels]: StalkerSessionOperationContract<
StalkerSessionAllChannelsParameters,
StalkerSessionCatalogResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.CatalogSearch]: StalkerSessionOperationContract<
StalkerSessionSearchParameters,
StalkerSessionCatalogResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.SeriesSeasons]: StalkerSessionOperationContract<
StalkerSessionSeriesSeasonsParameters,
StalkerSessionSeasonsResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.SeriesEpisodes]: StalkerSessionOperationContract<
StalkerSessionSeriesEpisodesParameters,
StalkerSessionEpisodesResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.ShortEpg]: StalkerSessionOperationContract<
StalkerSessionShortEpgParameters,
StalkerSessionEpgResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.EpgInfo]: StalkerSessionOperationContract<
StalkerSessionEpgInfoParameters,
StalkerSessionEpgResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.CreateLink]: StalkerSessionOperationContract<
StalkerSessionCreateLinkParameters,
StalkerSessionCreateLinkResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.Favorites]: StalkerSessionOperationContract<
StalkerSessionFavoritesParameters,
StalkerSessionFavoritesResult
>;
[STALKER_SESSION_APPLICATION_OPERATIONS.MainInfo]: StalkerSessionOperationContract<
StalkerSessionMainInfoParameters,
StalkerSessionMainInfoResult
>;
}
export type StalkerSessionOperationParameters<
Operation extends StalkerSessionApplicationOperation,
> = StalkerSessionOperationContractMap[Operation]['parameters'];
export type StalkerSessionOperationResult<
Operation extends StalkerSessionApplicationOperation,
> = StalkerSessionOperationContractMap[Operation]['result'];
@@ -1,3 +1,12 @@
import type {
StalkerSessionAccountSummary,
StalkerSessionApplicationOperation,
StalkerSessionOperationParameters,
StalkerSessionOperationResult,
} from './stalker-session-operations.interface';
export * from './stalker-session-operations.interface';
export const STALKER_SESSION_CONNECTION_MODES = {
PersistedOpen: 'persisted-open',
Provisional: 'provisional',
@@ -23,24 +32,6 @@ export const STALKER_SESSION_REQUEST_RECIPES = {
export type StalkerSessionRequestRecipe =
(typeof STALKER_SESSION_REQUEST_RECIPES)[keyof typeof STALKER_SESSION_REQUEST_RECIPES];
export const STALKER_SESSION_APPLICATION_OPERATIONS = {
CatalogCategories: 'get-categories',
CatalogGenres: 'get-genres',
CatalogItems: 'get-ordered-list',
CatalogAllChannels: 'get-all-channels',
CatalogSearch: 'search',
SeriesSeasons: 'get-series-seasons',
SeriesEpisodes: 'get-series-episodes',
ShortEpg: 'get-short-epg',
EpgInfo: 'get-epg-info',
CreateLink: 'create-link',
Favorites: 'favorites',
MainInfo: 'get-main-info',
} as const;
export type StalkerSessionApplicationOperation =
(typeof STALKER_SESSION_APPLICATION_OPERATIONS)[keyof typeof STALKER_SESSION_APPLICATION_OPERATIONS];
export const STALKER_SESSION_CONTROL_ACTIONS = {
Activate: 'activate',
Deactivate: 'deactivate',
@@ -178,14 +169,6 @@ export interface StalkerSessionCapabilities {
playbackContext: boolean;
}
export interface StalkerSessionAccountSummary {
name?: string;
status?: string;
tariffPlan?: string;
accountBalance?: string;
expiresAt?: string;
}
export interface StalkerSessionPersistenceDraft {
stalkerSourceUrl?: string;
portalUrl: string;
@@ -251,26 +234,24 @@ export type StalkerSessionConnectionOutcome =
| StalkerSessionCredentialsRequiredOutcome
| StalkerSessionFailureOutcome;
export type StalkerSessionParameterValue =
| string
| number
| boolean
| null;
export type StalkerSessionRequest = {
[Operation in StalkerSessionApplicationOperation]: {
leaseRef: StalkerSessionLeaseRef;
operation: Operation;
parameters: StalkerSessionOperationParameters<Operation>;
requestId?: string;
};
}[StalkerSessionApplicationOperation];
export interface StalkerSessionRequest {
leaseRef: StalkerSessionLeaseRef;
operation: StalkerSessionApplicationOperation;
parameters: Readonly<Record<string, StalkerSessionParameterValue>>;
requestId?: string;
}
export interface StalkerSessionRequestSuccess {
kind: 'success';
requestId: string;
operation: StalkerSessionApplicationOperation;
payload: unknown;
playbackContextRef?: StalkerPlaybackContextRef;
}
export type StalkerSessionRequestSuccess = {
[Operation in StalkerSessionApplicationOperation]: {
kind: 'success';
requestId: string;
operation: Operation;
payload: StalkerSessionOperationResult<Operation>;
playbackContextRef?: StalkerPlaybackContextRef;
};
}[StalkerSessionApplicationOperation];
export type StalkerSessionRequestOutcome =
| StalkerSessionRequestSuccess