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 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-29 08:59:35 +02:00
1 parent e5ee064407
commit 0a5d5a984f
3 files changed
+40 -4

No files matched your search

+1 -1
View File
@@ -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.
@@ -22,7 +22,9 @@ describe('PlaylistsService inventory reads', () => {
function setup() {
const reads: ReturnType<typeof deferred<Playlist[]>>[] = [];
const electron = {
dbGetAppState: jest.fn(async () => '1'),
dbGetAppState: jest.fn(
async (_key: string): Promise<string | null> => '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)
);
}
);
});
+6 -2
View File
@@ -79,7 +79,9 @@ type PlaylistStorageElectronApi = {
playlist: Playlist,
operationId?: string
) => Promise<unknown>;
dbMigrateAppPlaylists: (playlists: Playlist[]) => Promise<unknown>;
dbMigrateAppPlaylists: (
playlists: Playlist[]
) => Promise<{ success: boolean; count: number }>;
dbRecoverLegacyPlaylists?: () => Promise<void>;
dbUpsertAppPlaylists: (playlists: Playlist[]) => Promise<unknown>;
};
@@ -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;
}
}