diff --git a/server/providers/cctv/catalog.js b/server/providers/cctv/catalog.js index 396b62f8b..840ca9ce1 100644 --- a/server/providers/cctv/catalog.js +++ b/server/providers/cctv/catalog.js @@ -224,21 +224,22 @@ export function createCctvCatalog({ sourceRoot = process.cwd() } = {}) { _cctvSourceInflight = refreshCctvSources().finally(() => { _cctvSourceInflight = null; }); + // A background refresh has no caller waiting on it: report its failure + // here, once per refresh rather than once per request it serves. + if (_cctvSourceCache.length) { + _cctvSourceInflight.catch((error) => { + console.warn( + '[CCTV] background catalog refresh failed:', + error?.message || error, + ); + }); + } } // With a catalog in hand, answer with it while the refresh runs: the // refresh lasts as long as the slowest pack (Tarktee's DATEX feeds take up // to its 30 s timeout), and every /api/cctv request would otherwise wait // for it. Only the first load, with nothing to serve yet, waits. - if (_cctvSourceCache.length) { - _cctvSourceInflight.catch((error) => { - console.warn( - '[CCTV] background catalog refresh failed:', - error?.message || error, - ); - }); - return _cctvSourceCache; - } - return _cctvSourceInflight; + return _cctvSourceCache.length ? _cctvSourceCache : _cctvSourceInflight; } /** diff --git a/src/data/cctvCatalogCap.test.mjs b/src/data/cctvCatalogCap.test.mjs index 46faa43de..26775cb48 100644 --- a/src/data/cctvCatalogCap.test.mjs +++ b/src/data/cctvCatalogCap.test.mjs @@ -217,7 +217,18 @@ test('an expired catalog is served at once while a slow pack refreshes in the ba ['old-0', 'old-1', 'old-2'], ); + // Requests during the refresh share it rather than starting another. + const pause = () => new Promise((resolve) => setTimeout(resolve, 50)); + await pause(); + const inFlight = hung.length; + assert.ok(inFlight > 0, 'the refresh reached the hanging upstreams'); + await getSources(); + await getSources(); + await pause(); + assert.equal(hung.length, inFlight, 'only one refresh ran'); + // When the slow pack gives up, the refreshed catalog replaces the old one. + hang = false; for (const giveUp of hung) giveUp(); for (let i = 0; i < 50; i += 1) { const ids = (await getSources()).map((s) => s.id);