fix(live): prevent analysis crash on large catalogs (#26)
Bound synchronous live channel enrichment and tolerate minimal projection payloads so analysis and listings do not blank the UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -21,7 +21,8 @@ test('maps projected logical channels to the frontend variant format', () => {
|
||||
isFavorite: true, isHidden: true, logicalId: 'logical:one',
|
||||
logicalFingerprint: 'news', logicalDisplayName: 'News',
|
||||
streamId: '42', tvgLogo: 'logo.png',
|
||||
url: 'https://stream.test/live', groupTitle: 'News', sourceType: 'm3u', sourceName: 'Local'
|
||||
url: 'https://stream.test/live', groupTitle: 'News', sourceType: 'm3u', sourceName: 'Local',
|
||||
language: { code: 'und', short: 'UND', label: 'Unknown', confidence: 0 }
|
||||
});
|
||||
assert.equal(mapped.favorites.has('7:42'), true);
|
||||
assert.equal(mapped.hiddenItems.has('channel:7:42'), true);
|
||||
|
||||
@ -289,3 +289,37 @@ test('search and language filters run before expensive standard and EPG enrichme
|
||||
assert.equal(standardCalls, 1);
|
||||
assert.equal(epgCalls, 1);
|
||||
});
|
||||
|
||||
test('full catalog listing skips synchronous per-channel enrichment', () => {
|
||||
const db = createProjectionDb();
|
||||
for (let index = 0; index < 500; index++) {
|
||||
seedProjectionChannel(db, `channel-${index}`, `Channel ${index} HD`);
|
||||
}
|
||||
require('../server/services/liveProjectionService').rebuild(db);
|
||||
|
||||
let standardCalls = 0;
|
||||
let epgCalls = 0;
|
||||
const started = Date.now();
|
||||
const page = getChannelPage({ languages: [], q: '', epgSources: [{ id: 99, type: 'epg', enabled: true }] }, db, {
|
||||
iptvOrgCatalog: { findMatch: () => { standardCalls += 1; return null; } },
|
||||
epgMatching: { findBestMatch: () => { epgCalls += 1; return null; } }
|
||||
});
|
||||
|
||||
assert.equal(page.channels.length, 500);
|
||||
assert.equal(standardCalls, 0);
|
||||
assert.equal(epgCalls, 0);
|
||||
assert.ok(Date.now() - started < 1000);
|
||||
});
|
||||
|
||||
test('broad searches above the enrichment guard remain bounded', () => {
|
||||
const db = createProjectionDb();
|
||||
for (let index = 0; index < 300; index++) seedProjectionChannel(db, `news-${index}`, `News ${index}`);
|
||||
require('../server/services/liveProjectionService').rebuild(db);
|
||||
let calls = 0;
|
||||
const page = getChannelPage({ q: 'news', epgSources: [] }, db, {
|
||||
iptvOrgCatalog: { findMatch: () => { calls += 1; return null; } },
|
||||
epgMatching: { findBestMatch: () => { calls += 1; return null; } }
|
||||
});
|
||||
assert.equal(page.channels.length, 300);
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
69
test/t26Frontend.test.js
Normal file
69
test/t26Frontend.test.js
Normal file
@ -0,0 +1,69 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { JSDOM } = require('jsdom');
|
||||
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const projection = require('../public/js/services/liveChannelProjection.js');
|
||||
const script = relativePath => fs.readFileSync(path.join(root, 'public', relativePath), 'utf8');
|
||||
|
||||
test('minimal non-enriched live projection payload maps and renders without an exception', () => {
|
||||
const mapped = projection.mapResponse({
|
||||
projection: { stale: false },
|
||||
filters: { availableLanguages: [null, {}] },
|
||||
channels: [
|
||||
{
|
||||
id: 'logical-news:und', logicalChannelId: 'logical-news', displayName: 'News',
|
||||
standard: null, language: {}, epg: null, preferredVariant: null,
|
||||
variants: [{
|
||||
id: 'news-raw', sourceId: 1, name: 'News Raw', tvgId: 'news.raw',
|
||||
categoryName: 'News', language: null, isHidden: false
|
||||
}]
|
||||
},
|
||||
{
|
||||
id: 'logical-sports:und', logicalChannelId: 'logical-sports', displayName: 'Sports',
|
||||
language: null, variants: null,
|
||||
preferredVariant: {
|
||||
id: 'sports-raw', sourceId: 1, name: 'Sports Raw', tvgId: 'sports.raw',
|
||||
categoryName: 'Sports', isHidden: false
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'empty', logicalChannelId: 'empty', displayName: 'Empty',
|
||||
language: null, variants: null, preferredVariant: null
|
||||
}
|
||||
]
|
||||
}, [{ id: 1, type: 'm3u', name: 'Provider' }]);
|
||||
|
||||
assert.equal(mapped.logicalChannels.length, 2);
|
||||
assert.ok(mapped.logicalChannels.every(channel => channel.language.code === 'und'));
|
||||
assert.equal(mapped.logicalChannels[0].tvgId, 'news.raw');
|
||||
assert.deepEqual(mapped.availableLanguages.map(language => language.code), ['und']);
|
||||
|
||||
const dom = new JSDOM('<div id="result"></div>', { url: 'http://nodecast.test/', runScripts: 'outside-only' });
|
||||
const { window } = dom;
|
||||
window.eval(script('js/icons.js'));
|
||||
window.eval(script('js/services/channelLanguageDetection.js'));
|
||||
window.eval(script('js/services/channelLanguageFusion.js'));
|
||||
window.eval(script('js/services/channelVariantGrouping.js'));
|
||||
window.eval(`${script('js/components/ChannelList.js')}\nwindow.ChannelList = ChannelList;`);
|
||||
window.app = { epgGuide: { getCurrentProgram: () => null, loadError: null } };
|
||||
const channelList = Object.create(window.ChannelList.prototype);
|
||||
channelList.expandedVariants = new Set();
|
||||
channelList.sources = [{ id: 1, type: 'm3u', name: 'Provider' }];
|
||||
channelList.showHidden = false;
|
||||
channelList.isFavorite = () => false;
|
||||
channelList.isHidden = () => false;
|
||||
|
||||
const result = window.document.getElementById('result');
|
||||
assert.doesNotThrow(() => {
|
||||
result.innerHTML = mapped.logicalChannels.map(channel =>
|
||||
channelList.renderLogicalChannel(channel, 'render-id', 'News')
|
||||
).join('');
|
||||
});
|
||||
assert.equal(result.querySelectorAll('.logical-channel').length, 2);
|
||||
assert.equal(result.querySelector('.channel-language-badge').getAttribute('aria-label'), 'Unknown language');
|
||||
assert.match(result.textContent, /No program information/);
|
||||
dom.window.close();
|
||||
});
|
||||
Reference in New Issue
Block a user