From 0a5d5a984fbe4fc809f3fc607187a340eb2cfeb0 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 27 Sep 2026 18:51:49 +0200 Subject: [PATCH] fix(services): trust a migration receipt written in this session; correct J1 note - Mark the IndexedDB -> SQLite receipt as confirmed after an empty-store receipt write or a committed dbMigrateAppPlaylists, not only when it was already present, so M3U favorites skip the per-playlist re-read on the first launch after an upgrade too. - The release note no longer claims a wall-clock speedup the J1 benchmark did not show. Co-Authored-By: Claude Opus 5.5 --- .changes/workspace-startup-first-card-ipc.md | 2 +- .../playlists.service.inventory-read.spec.ts | 34 ++++++++++++++++++- libs/services/src/lib/playlists.service.ts | 8 +++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/.changes/workspace-startup-first-card-ipc.md b/.changes/workspace-startup-first-card-ipc.md index a6e91feb9..fc6c7e758 100644 --- a/.changes/workspace-startup-first-card-ipc.md +++ b/.changes/workspace-startup-first-card-ipc.md @@ -3,4 +3,4 @@ type: perf area: workspace --- -The workspace shows its first source card sooner after launch (journey J1, `renderer.ipcCallsToFirstCard`): it reads the source list once instead of twice, and loads downloads, update status, recent items and favorites right after the first screen. Set `IPTVNATOR_DISABLE_STARTUP_DEFERRAL=1` to turn this off. +On launch the workspace reads the source list once instead of twice before its first source card (journey J1, `renderer.ipcCallsToFirstCard`), and M3U favorites stop re-checking the finished playlist migration for every playlist. diff --git a/libs/services/src/lib/playlists.service.inventory-read.spec.ts b/libs/services/src/lib/playlists.service.inventory-read.spec.ts index a429e7adb..6b6422e9f 100644 --- a/libs/services/src/lib/playlists.service.inventory-read.spec.ts +++ b/libs/services/src/lib/playlists.service.inventory-read.spec.ts @@ -22,7 +22,9 @@ describe('PlaylistsService inventory reads', () => { function setup() { const reads: ReturnType>[] = []; const electron = { - dbGetAppState: jest.fn(async () => '1'), + dbGetAppState: jest.fn( + async (_key: string): Promise => '1' + ), dbSetAppState: jest.fn(), dbRecoverLegacyPlaylists: jest.fn(async () => undefined), dbGetAppPlaylists: jest.fn(async () => []), @@ -117,4 +119,34 @@ describe('PlaylistsService inventory reads', () => { electron.dbGetAppPlaylistFavoriteChannels ).toHaveBeenCalledTimes(2); }); + + it.each([ + ['an empty legacy store', [], { success: true, count: 0 }, true], + ['a committed migration', [source('legacy')], { success: true, count: 1 }, true], + ['an uncommitted migration', [source('legacy')], { success: false, count: 0 }, false], + ])( + 'trusts the receipt written by %s in this session', + async (_case, legacy, result, confirmed) => { + const { electron, service, settle } = setup(); + electron.dbGetAppState.mockImplementation(async (key: string) => + key === 'm3u-playlists-indexeddb-to-sqlite-v1' ? null : '1' + ); + Object.assign(electron, { + dbMigrateAppPlaylists: jest.fn(async () => result), + }); + Object.assign(service, { + dbService: { getAll: jest.fn(() => of(legacy)) }, + }); + + const inventory = firstValueFrom(service.getAllPlaylists()); + await settle(0, []); + await inventory; + const receiptReads = electron.dbGetAppState.mock.calls.length; + await firstValueFrom(service.getM3uFavoriteChannels('one')); + + expect(electron.dbGetAppState).toHaveBeenCalledTimes( + receiptReads + (confirmed ? 0 : 1) + ); + } + ); }); diff --git a/libs/services/src/lib/playlists.service.ts b/libs/services/src/lib/playlists.service.ts index 1ccc170bd..3679c755b 100644 --- a/libs/services/src/lib/playlists.service.ts +++ b/libs/services/src/lib/playlists.service.ts @@ -79,7 +79,9 @@ type PlaylistStorageElectronApi = { playlist: Playlist, operationId?: string ) => Promise; - dbMigrateAppPlaylists: (playlists: Playlist[]) => Promise; + dbMigrateAppPlaylists: ( + playlists: Playlist[] + ) => Promise<{ success: boolean; count: number }>; dbRecoverLegacyPlaylists?: () => Promise; dbUpsertAppPlaylists: (playlists: Playlist[]) => Promise; }; @@ -211,9 +213,11 @@ export class PlaylistsService { if (playlists.length) { // The worker commits rows and the receipt atomically. Keep the // original IndexedDB as a recovery source, even after success. - await electron.dbMigrateAppPlaylists(playlists); + const result = await electron.dbMigrateAppPlaylists(playlists); + this.sqliteMigrationConfirmed = result?.success === true; } else { await electron.dbSetAppState(SQLITE_PLAYLIST_MIGRATION_FLAG, '1'); + this.sqliteMigrationConfirmed = true; } }