perf(workspace): share the startup inventory read and defer non-first-card IPC (J1)

Journey J1 / renderer.ipcCallsToFirstCard: 12 -> 7.

- PlaylistsService.getAllPlaylists() shares one in-flight SQLite read
  between concurrent callers (the playlist effect and the XMLTV source
  reconciliation at startup). Settled reads are never reused and every
  SQLite write detaches the pending read.
- getM3uFavoriteChannels() stops re-reading the write-once IndexedDB ->
  SQLite migration receipt once it has been seen.
- StartupDeferralService holds the download list, app update status and
  the dashboard's recent items and favorites until one task after the
  render that reveals the routed content (5 s safety timeout).
  IPTVNATOR_DISABLE_STARTUP_DEFERRAL=1 is the kill switch.
- reconcileEpgSources and the two distinct migration-flag reads stay on
  the critical path: the first is the #1548 revision fence, the second
  are different keys, not duplicates.

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 76dd8c099e
commit e5ee064407
24 files changed
+648 -23

No files matched your search

@@ -7,7 +7,11 @@ import {
selectPlaylistsLoadingFlag,
} from '@iptvnator/m3u-state';
import { of, Subject } from 'rxjs';
import { DatabaseService, PlaylistsService } from '@iptvnator/services';
import {
DatabaseService,
PlaylistsService,
StartupDeferralService,
} from '@iptvnator/services';
import {
PlaybackPositionData,
Playlist,
@@ -270,6 +274,30 @@ describe('DashboardDataService', () => {
expect(service.dashboardReady()).toBe(false);
});
it('holds the page-entry reload until the first screen rendered', async () => {
let openGate!: () => void;
jest.spyOn(
TestBed.inject(StartupDeferralService),
'whenFirstContentRendered'
).mockReturnValue(new Promise<void>((resolve) => (openGate = resolve)));
const recent = jest
.spyOn(service, 'reloadGlobalRecentItems')
.mockResolvedValue(undefined);
const favorites = jest
.spyOn(service, 'reloadGlobalFavorites')
.mockResolvedValue(undefined);
const reload = service.reloadForPageEntry();
await Promise.resolve();
expect(recent).not.toHaveBeenCalled();
expect(favorites).not.toHaveBeenCalled();
openGate();
await reload;
expect(recent).toHaveBeenCalledTimes(1);
expect(favorites).toHaveBeenCalledTimes(1);
});
it('keeps dashboardReady false until xtream recently added finishes its first load', async () => {
expect(service.dashboardReady()).toBe(false);
@@ -20,6 +20,7 @@ import {
GlobalRecentlyAddedKind,
PlaylistsService,
RuntimeCapabilitiesService,
StartupDeferralService,
} from '@iptvnator/services';
import {
XTREAM_DATA_SOURCE,
@@ -116,6 +117,7 @@ export class DashboardDataService {
private readonly xtreamDataSource = inject(XTREAM_DATA_SOURCE);
private readonly playlistsService = inject(PlaylistsService);
private readonly runtime = inject(RuntimeCapabilitiesService);
private readonly startupDeferral = inject(StartupDeferralService);
private readonly ngZone = inject(NgZone);
private readonly translate = inject(TranslateService);
private readonly playbackPositions = inject(PORTAL_PLAYBACK_POSITIONS);
@@ -499,6 +501,19 @@ export class DashboardDataService {
readonly quickRecent = computed(() => this.recentPlaylists().slice(0, 4));
/**
* Page-entry reload of recent items and favorites. On launch it waits for
* the first rendered screen (journey J1) and the rails keep their
* skeletons meanwhile; later entries find the gate already open.
*/
async reloadForPageEntry(): Promise<void> {
await this.startupDeferral.whenFirstContentRendered();
await Promise.all([
this.reloadGlobalRecentItems(),
this.reloadGlobalFavorites(),
]);
}
async reloadGlobalRecentItems(): Promise<void> {
if (!this.globalRecentLoaded()) {
this.globalRecentLoadingState.set(true);
@@ -304,8 +304,7 @@ export class WorkspaceDashboardRailsComponent {
// Re-entering the dashboard should pick up any DB-backed recent/favorite
// changes made while viewing details, including newly backfilled
// backdrops that do not change recency ordering.
void this.data.reloadGlobalRecentItems();
void this.data.reloadGlobalFavorites();
void this.data.reloadForPageEntry();
this.liveEpg.connect(this.enabledLiveCards);