From 57e108b8cb9c365efb298a110740d4503d197733 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 27 Jul 2026 10:50:01 +0200 Subject: [PATCH] feat(stalker): reflect persisted connections in state --- libs/m3u-state/src/lib/actions.ts | 8 ++- .../lib/reducers/playlist.reducers.spec.ts | 61 +++++++++++++++++-- .../src/lib/reducers/playlist.reducers.ts | 18 ++++-- 3 files changed, 76 insertions(+), 11 deletions(-) diff --git a/libs/m3u-state/src/lib/actions.ts b/libs/m3u-state/src/lib/actions.ts index 9fcdaf2ca..954fb6897 100644 --- a/libs/m3u-state/src/lib/actions.ts +++ b/libs/m3u-state/src/lib/actions.ts @@ -1,5 +1,10 @@ import { createActionGroup, emptyProps, props } from '@ngrx/store'; -import { Channel, EpgProgram, Playlist, PlaylistMeta } from '@iptvnator/shared/interfaces'; +import { + Channel, + EpgProgram, + Playlist, + PlaylistMeta, +} from '@iptvnator/shared/interfaces'; export const PlaylistActions = createActionGroup({ source: 'Playlists', @@ -7,6 +12,7 @@ export const PlaylistActions = createActionGroup({ 'Load Playlists': emptyProps(), 'Load Playlists Success': props<{ playlists: PlaylistMeta[] }>(), 'Add Playlist': props<{ playlist: Playlist }>(), + 'Stalker Connection Persisted': props<{ playlist: Playlist }>(), 'Add Many Playlists': props<{ playlists: Playlist[] }>(), 'Remove Playlist': props<{ playlistId: string }>(), 'Update Playlist Meta': props<{ playlist: PlaylistMeta }>(), diff --git a/libs/m3u-state/src/lib/reducers/playlist.reducers.spec.ts b/libs/m3u-state/src/lib/reducers/playlist.reducers.spec.ts index 89b9e61a0..0a50a5c32 100644 --- a/libs/m3u-state/src/lib/reducers/playlist.reducers.spec.ts +++ b/libs/m3u-state/src/lib/reducers/playlist.reducers.spec.ts @@ -160,9 +160,9 @@ describe('playlistReducers', () => { }) ); - expect( - nextState.playlists.entities['playlist-1']?.autoRefresh - ).toBe(true); + expect(nextState.playlists.entities['playlist-1']?.autoRefresh).toBe( + true + ); }); it('keeps autoRefresh disabled on playlist refresh when the existing playlist has it disabled', () => { @@ -194,8 +194,57 @@ describe('playlistReducers', () => { }) ); - expect( - nextState.playlists.entities['playlist-1']?.autoRefresh - ).toBe(false); + expect(nextState.playlists.entities['playlist-1']?.autoRefresh).toBe( + false + ); + }); + + it('reflects an atomically persisted Stalker connection without triggering another write', () => { + const existingPlaylist = { + _id: 'stalker-1', + autoRefresh: false, + count: 0, + importDate: '2026-07-27T00:00:00.000Z', + macAddress: '00:1A:79:AA:BB:CC', + portalUrl: 'https://old.example/server/load.php', + stalkerToken: 'legacy-token', + title: 'Old Portal', + } as PlaylistMeta; + const persistedPlaylist = { + ...existingPlaylist, + portalUrl: 'https://new.example/server/load.php', + stalkerLandingUrl: 'https://new.example/c/', + stalkerRequestRecipe: 'full-session', + stalkerSourceUrl: 'https://new.example/c/', + title: 'Connected Portal', + } as Playlist; + delete persistedPlaylist.stalkerToken; + const state = { + ...initialState, + playlists: playlistsAdapter.addOne( + existingPlaylist, + initialState.playlists + ), + }; + + const nextState = reducer( + state, + PlaylistActions.stalkerConnectionPersisted({ + playlist: persistedPlaylist, + }) + ); + + expect(nextState.playlists.entities['stalker-1']).toEqual( + expect.objectContaining({ + portalUrl: 'https://new.example/server/load.php', + stalkerLandingUrl: 'https://new.example/c/', + stalkerRequestRecipe: 'full-session', + stalkerSourceUrl: 'https://new.example/c/', + title: 'Connected Portal', + }) + ); + expect(nextState.playlists.entities['stalker-1']).not.toHaveProperty( + 'stalkerToken' + ); }); }); diff --git a/libs/m3u-state/src/lib/reducers/playlist.reducers.ts b/libs/m3u-state/src/lib/reducers/playlist.reducers.ts index fb22c6be4..1ac6dc4d2 100644 --- a/libs/m3u-state/src/lib/reducers/playlist.reducers.ts +++ b/libs/m3u-state/src/lib/reducers/playlist.reducers.ts @@ -39,11 +39,9 @@ export const playlistReducers = [ detectedEpgUrls: action.playlist.detectedEpgUrls ?? currentPlaylist?.detectedEpgUrls, - enabledEpgUrls: - action.playlist.epgUrls ?? currentPlaylist?.epgUrls, + enabledEpgUrls: action.playlist.epgUrls ?? currentPlaylist?.epgUrls, manualEpgUrls: - action.playlist.manualEpgUrls ?? - currentPlaylist?.manualEpgUrls, + action.playlist.manualEpgUrls ?? currentPlaylist?.manualEpgUrls, disabledEpgUrls: action.playlist.disabledEpgUrls ?? currentPlaylist?.disabledEpgUrls, @@ -86,6 +84,18 @@ export const playlistReducers = [ ), }; }), + on( + PlaylistActions.stalkerConnectionPersisted, + (state, action): PlaylistState => { + return { + ...state, + playlists: playlistsAdapter.setOne( + action.playlist, + state.playlists + ), + }; + } + ), on(PlaylistActions.addManyPlaylists, (state, action): PlaylistState => { return { ...state,