From 5e9e77d8db3376b0b8fb4999f1f0e246d260da3c Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 29 Sep 2026 20:56:01 +0200 Subject: [PATCH] fix(dashboard): read M3U favorites after queued favorite writes getM3uFavoriteChannels could query SQLite before a just-toggled favorite was written: updateFavorites is serialized per playlist and can land late, while the dashboard reads as soon as the store changes and never reloads because playlistFavoritesReloadKey is unchanged. Await the playlist's write queue tail (which never rejects) before reading. Co-Authored-By: Claude Opus 5.5 --- .changes/dashboard-fresh-m3u-favorite.md | 8 ++++ .../src/lib/playlists.service.spec.ts | 43 +++++++++++++++++++ libs/services/src/lib/playlists.service.ts | 3 ++ 3 files changed, 54 insertions(+) create mode 100644 .changes/dashboard-fresh-m3u-favorite.md diff --git a/.changes/dashboard-fresh-m3u-favorite.md b/.changes/dashboard-fresh-m3u-favorite.md new file mode 100644 index 000000000..68a104a3b --- /dev/null +++ b/.changes/dashboard-fresh-m3u-favorite.md @@ -0,0 +1,8 @@ +--- +type: fix +area: dashboard +--- + +An M3U channel added to favorites right before opening the dashboard now +appears in the favorites rail immediately, instead of only after the next +reload. diff --git a/libs/services/src/lib/playlists.service.spec.ts b/libs/services/src/lib/playlists.service.spec.ts index 71a4961f6..54d5e49b4 100644 --- a/libs/services/src/lib/playlists.service.spec.ts +++ b/libs/services/src/lib/playlists.service.spec.ts @@ -345,6 +345,49 @@ describe('PlaylistsService', () => { expect(electron.dbGetAppPlaylists).not.toHaveBeenCalled(); }); + it('reads resolved M3U favorites only after a queued favorites write lands', async () => { + let storedFavorites: string[] = []; + let releaseWrite!: () => void; + const writeGate = new Promise((resolve) => { + releaseWrite = resolve; + }); + const electron = { + dbGetAppPlaylist: jest.fn(async () => { + await writeGate; + return { _id: 'playlist-1', favorites: storedFavorites }; + }), + dbGetAppPlaylistFavoriteChannels: jest.fn(async () => + storedFavorites.map((id) => ({ id })) + ), + dbGetAppPlaylists: jest.fn(async () => []), + dbGetAppState: jest.fn(async (key: string) => + key === SQLITE_PLAYLIST_MIGRATION_FLAG || + key === STALKER_PLAYLIST_METADATA_MIGRATION_FLAG + ? '1' + : null + ), + dbSetAppState: jest.fn(), + dbUpsertAppPlaylist: jest.fn(async (playlist: Playlist) => { + storedFavorites = playlist.favorites as string[]; + }), + dbUpsertAppPlaylists: jest.fn(), + }; + testWindow.electron = electron; + + const service = createService(); + const write = firstValueFrom( + service.updateFavorites('playlist-1', ['channel-1']) + ); + const read = firstValueFrom( + service.getM3uFavoriteChannels('playlist-1') + ); + await new Promise((resolve) => setTimeout(resolve)); + releaseWrite(); + + await write; + await expect(read).resolves.toEqual([{ id: 'channel-1' }]); + }); + it('falls back from resolved M3U favorites when SQLite playlist migration is incomplete', async () => { const electron = { dbGetAppPlaylist: jest.fn(), diff --git a/libs/services/src/lib/playlists.service.ts b/libs/services/src/lib/playlists.service.ts index 9719348bf..b00cbb034 100644 --- a/libs/services/src/lib/playlists.service.ts +++ b/libs/services/src/lib/playlists.service.ts @@ -1092,6 +1092,9 @@ export class PlaylistsService { return from( (async () => { + // A favorite toggled just before this read may still be queued; + // the queue tail never rejects. + await this.playlistWriteQueues.get(playlistId); const alreadyMigrated = await electron.dbGetAppState( SQLITE_PLAYLIST_MIGRATION_FLAG );