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.
This commit is contained in:
weeco committed 2026-05-07 08:52:59 -07:00
1 parent e751e82c07
commit 505e75b67e
4 files changed
+39 -9

No files matched your search

@@ -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[] = [];
@@ -189,8 +189,10 @@ export class EpgQueueService implements OnDestroy {
}
private pruneEphemeralMaps(visibleIds: Set<number>): 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);
}
@@ -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<string, EpgItem> = {};
for (const id of ids) {
const row = rows?.[id];
@@ -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);
});
}
}