diff --git a/apps/electron-backend/src/app/services/stalker-session/stalker-session-runtime.spec.ts b/apps/electron-backend/src/app/services/stalker-session/stalker-session-runtime.spec.ts new file mode 100644 index 000000000..2a299be7a --- /dev/null +++ b/apps/electron-backend/src/app/services/stalker-session/stalker-session-runtime.spec.ts @@ -0,0 +1,68 @@ +import { + STALKER_SESSION_FAILURE_REASONS, + type StalkerSessionConnectionDescriptor, +} from '@iptvnator/shared/interfaces'; +import type { DatabaseWorkerClient } from '../database-worker-client'; +import { + createStalkerPlaylistReader, + createStalkerSessionManager, +} from './stalker-session-runtime'; + +const descriptor: StalkerSessionConnectionDescriptor = { + connectionMode: 'persisted-open', + macAddress: '00:1A:79:AA:BB:CC', + playlistRef: 'playlist-7', + profilePreset: { + id: 'mag250-public-5_1-minimal-v1', + version: 1, + }, + sourceUrl: 'https://portal.test/c/', +}; + +describe('Stalker session runtime', () => { + it('loads a playlist through the non-EPG database worker contract', async () => { + const request = jest.fn().mockResolvedValue({ _id: 'playlist-7' }); + const readPlaylist = createStalkerPlaylistReader({ + request, + } as unknown as Pick); + + await expect(readPlaylist('playlist-7')).resolves.toEqual({ + _id: 'playlist-7', + }); + expect(request).toHaveBeenCalledWith('DB_GET_APP_PLAYLIST', { + playlistId: 'playlist-7', + }); + }); + + it('consults worker-backed saved credentials before resolving a persisted connection', async () => { + const request = jest.fn().mockResolvedValue({ + _id: 'playlist-7', + macAddress: descriptor.macAddress, + password: 'saved-password', + portalUrl: descriptor.sourceUrl, + type: 'stalker', + username: 'saved-user', + }); + const resolve = jest.fn().mockResolvedValue({ + kind: 'failure', + reason: STALKER_SESSION_FAILURE_REASONS.EndpointNotFound, + retryable: false, + stage: 'resolving', + }); + const manager = createStalkerSessionManager({ + databaseWorker: { + request, + } as unknown as Pick, + resolver: { resolve }, + }); + + await expect(manager.open(1, { descriptor })).resolves.toMatchObject({ + kind: 'failure', + reason: STALKER_SESSION_FAILURE_REASONS.EndpointNotFound, + }); + expect(request).toHaveBeenCalledWith('DB_GET_APP_PLAYLIST', { + playlistId: 'playlist-7', + }); + expect(resolve).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/electron-backend/src/app/services/stalker-session/stalker-session-runtime.ts b/apps/electron-backend/src/app/services/stalker-session/stalker-session-runtime.ts new file mode 100644 index 000000000..ddeb0c98b --- /dev/null +++ b/apps/electron-backend/src/app/services/stalker-session/stalker-session-runtime.ts @@ -0,0 +1,51 @@ +import { + databaseWorkerClient, + type DatabaseWorkerClient, +} from '../database-worker-client'; +import { stalkerPlaybackContextService } from '../stalker-playback-context.service'; +import { mapRawOperation } from './stalker-operation-adapter'; +import { createStalkerSavedCredentialsLoader } from './stalker-saved-credentials'; +import { + StalkerSessionManager, + type StalkerSessionManagerDependencies, + type StalkerSessionPlaybackContextPort, +} from './stalker-session-manager'; + +type StalkerDatabaseWorker = Pick; + +export type StalkerSessionRuntimeDependencies = Omit< + StalkerSessionManagerDependencies, + 'loadSavedCredentials' | 'mapRawOperation' | 'playbackContexts' +> & { + readonly databaseWorker?: StalkerDatabaseWorker; + readonly playbackContexts?: StalkerSessionPlaybackContextPort; +}; + +export function createStalkerPlaylistReader( + databaseWorker: StalkerDatabaseWorker = databaseWorkerClient +): (playlistRef: string) => Promise { + return (playlistRef) => + databaseWorker.request('DB_GET_APP_PLAYLIST', { + playlistId: playlistRef, + }); +} + +export function createStalkerSessionManager( + dependencies: StalkerSessionRuntimeDependencies = {} +): StalkerSessionManager { + const { + databaseWorker = databaseWorkerClient, + playbackContexts = stalkerPlaybackContextService, + ...managerDependencies + } = dependencies; + return new StalkerSessionManager({ + ...managerDependencies, + loadSavedCredentials: createStalkerSavedCredentialsLoader( + createStalkerPlaylistReader(databaseWorker) + ), + mapRawOperation, + playbackContexts, + }); +} + +export const stalkerSessionManager = createStalkerSessionManager(); diff --git a/apps/electron-backend/src/main.ts b/apps/electron-backend/src/main.ts index ea9b08923..218fd70bd 100644 --- a/apps/electron-backend/src/main.ts +++ b/apps/electron-backend/src/main.ts @@ -24,10 +24,12 @@ import SettingsEvents from './app/events/settings.events'; import SharedEvents from './app/events/shared.events'; import SquirrelEvents from './app/events/squirrel.events'; import StalkerEvents from './app/events/stalker.events'; +import StalkerSessionEvents from './app/events/stalker-session.events'; import { isStartupTraceEnabled, trace } from './app/services/debug-trace'; import { registerStaticHeaderShims } from './app/services/request-header-overrides.service'; import { AppUpdateService } from './app/services/app-update.service'; import { databaseWorkerClient } from './app/services/database-worker-client'; +import { stalkerSessionManager } from './app/services/stalker-session/stalker-session-runtime'; import WindowEvents from './app/events/window.events'; import XtreamEvents from './app/events/xtream.events'; import { environment } from './environments/environment'; @@ -142,6 +144,9 @@ export default class Main { SharedEvents.bootstrapSharedEvents(); PlayerEvents.bootstrapPlayerEvents(); SettingsEvents.bootstrapSettingsEvents(); + StalkerSessionEvents.bootstrapStalkerSessionEvents( + stalkerSessionManager + ); StalkerEvents.bootstrapStalkerEvents(); XtreamEvents.bootstrapXtreamEvents(); DatabaseEvents.bootstrapDatabaseEvents(); @@ -209,6 +214,8 @@ runEmbeddedMpvRuntimeDiagnosticOrContinue(process.argv, () => { shutdownEmbeddedMpv(); shutdownMpvSession(); shutdownVlcSession(); - void databaseWorkerClient.shutdown(); + void stalkerSessionManager + .destroyAll() + .finally(() => databaseWorkerClient.shutdown()); }); });