From 505e75b67e72601247de0576de37b8a3160e54f5 Mon Sep 17 00:00:00 2001 From: weeco <23424570+weeco@users.noreply.github.com> Date: Thu, 7 May 2026 08:29:28 -0700 Subject: [PATCH] fix(xtream): address review feedback on XMLTV fallback PR - pruneEphemeralMaps now uses getCached() so TTL-expired entries also drop their XMLTV preview / channel-id mapping, instead of clinging to them until the next explicit cache read. - portal-channels-list attaches a .catch() to the now-async enqueue so any future internal rejection surfaces in the console rather than bubbling silently as an unhandled promise rejection. - XtreamXmltvFallbackService binds the bridge receiver when calling the extracted function reference, in case a preload wrapper relies on `this`. Adds a regression test for the TTL-aware prune. --- .../lib/services/epg-queue.service.spec.ts | 28 +++++++++++++++++++ .../src/lib/services/epg-queue.service.ts | 6 ++-- .../services/xtream-xmltv-fallback.service.ts | 4 +-- .../portal-channels-list.component.ts | 10 +++---- 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/libs/portal/xtream/data-access/src/lib/services/epg-queue.service.spec.ts b/libs/portal/xtream/data-access/src/lib/services/epg-queue.service.spec.ts index c04eb2f3c..efbc31936 100644 --- a/libs/portal/xtream/data-access/src/lib/services/epg-queue.service.spec.ts +++ b/libs/portal/xtream/data-access/src/lib/services/epg-queue.service.spec.ts @@ -276,6 +276,34 @@ describe('EpgQueueService', () => { expect(priv().epgChannelByStreamId.has(100)).toBe(false); }); + it('prunes XMLTV preview when its cache entry has expired (TTL-aware)', async () => { + fallback.getCurrentProgramsBatch.mockResolvedValueOnce({ + 'rtl.de': makeEpgItem('rtl.de', 'Tagesschau'), + }); + xtreamApi.getShortEpg.mockResolvedValue([]); + + // Initial enqueue: stream 100 visible, gets cached. + await service.enqueue( + [{ streamId: 100, epgChannelId: 'rtl.de' }], + new Set([100]), + credentials + ); + expect(priv().xmltvPreviewByStreamId.has(100)).toBe(true); + + // Stream 100 leaves the viewport but still has a (live) cache entry, + // so prune keeps the preview alive — by design. + await service.enqueue([], new Set([]), credentials); + expect(priv().xmltvPreviewByStreamId.has(100)).toBe(true); + + // Advance past the 5-minute cache TTL. Now the cache entry is stale; + // prune must drop the preview rather than cling to an expired hit. + jest.advanceTimersByTime(5 * 60 * 1000 + 1); + await service.enqueue([], new Set([]), credentials); + + expect(priv().xmltvPreviewByStreamId.has(100)).toBe(false); + expect(priv().epgChannelByStreamId.has(100)).toBe(false); + }); + it('does not re-emit on empty→empty transitions', async () => { xtreamApi.getShortEpg.mockResolvedValue([]); const events: number[] = []; diff --git a/libs/portal/xtream/data-access/src/lib/services/epg-queue.service.ts b/libs/portal/xtream/data-access/src/lib/services/epg-queue.service.ts index 000ad9f5f..23c22ee7e 100644 --- a/libs/portal/xtream/data-access/src/lib/services/epg-queue.service.ts +++ b/libs/portal/xtream/data-access/src/lib/services/epg-queue.service.ts @@ -189,8 +189,10 @@ export class EpgQueueService implements OnDestroy { } private pruneEphemeralMaps(visibleIds: Set): void { - for (const id of this.epgChannelByStreamId.keys()) { - if (!visibleIds.has(id) && !this.cache.has(id)) { + for (const id of [...this.epgChannelByStreamId.keys()]) { + // getCached() honors TTL and lazily evicts expired entries; + // a raw cache.has() would keep stale previews alive forever. + if (!visibleIds.has(id) && this.getCached(id) === null) { this.epgChannelByStreamId.delete(id); this.xmltvPreviewByStreamId.delete(id); } diff --git a/libs/portal/xtream/data-access/src/lib/services/xtream-xmltv-fallback.service.ts b/libs/portal/xtream/data-access/src/lib/services/xtream-xmltv-fallback.service.ts index 82de909da..140e96369 100644 --- a/libs/portal/xtream/data-access/src/lib/services/xtream-xmltv-fallback.service.ts +++ b/libs/portal/xtream/data-access/src/lib/services/xtream-xmltv-fallback.service.ts @@ -43,7 +43,7 @@ export class XtreamXmltvFallbackService { if (typeof fn !== 'function') return []; try { - const programs = await fn(id); + const programs = await fn.call(this.bridge, id); return (programs ?? []).map((p) => mapEpgProgramToEpgItem(p, id)); } catch (error) { this.logger.error( @@ -70,7 +70,7 @@ export class XtreamXmltvFallbackService { if (ids.length === 0) return {}; try { - const rows = await fn(ids); + const rows = await fn.call(this.bridge, ids); const out: Record = {}; for (const id of ids) { const row = rows?.[id]; diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts index fec0f1a44..98f589205 100644 --- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts +++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts @@ -226,11 +226,11 @@ export class PortalChannelsListComponent implements AfterViewInit, OnDestroy { } if (uncachedEntries.length > 0) { - this.epgQueueService.enqueue( - uncachedEntries, - visibleIds, - credentials - ); + this.epgQueueService + .enqueue(uncachedEntries, visibleIds, credentials) + .catch((error) => { + console.warn('EPG enqueue failed', error); + }); } }