mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
The proxy now skips malformed upstream rows individually but counts them, capped at 10,000, in the catalog metadata and the /stations envelope (rejectedStationCount). The count never fails health policy, so an otherwise healthy generation is still accepted. The client validates the field, adds its own rejected rows after admission, and reports degraded coverage when either side skipped rows. Adds an end-to-end proxy-to-client test and aligns DATA_SOURCES.md and CURRENT-STATE.md. Co-Authored-By: Claude Opus 5.5 <[email protected]>
205 lines
7.8 KiB
JavaScript
205 lines
7.8 KiB
JavaScript
import { msg } from '../../i18n/index.js';
|
|
import {
|
|
RADIO_DIRECTORY_STALE_MS,
|
|
RADIO_DIRECTORY_FUTURE_SKEW_MS,
|
|
RADIO_REJECTED_STATION_COUNT_MAX,
|
|
} from './policy.js';
|
|
|
|
// Directory errors stay English in layer state and stats; the layer row and
|
|
// the Radio panel translate them when shown.
|
|
const ERROR_REFRESH_DEGRADED = msg(
|
|
'Directory refresh degraded; showing the previous station catalog.',
|
|
);
|
|
const ERROR_COVERAGE_DEGRADED = msg('Radio directory coverage is degraded.');
|
|
const ERROR_REFRESH_FAILED = msg(
|
|
'Directory refresh failed; showing the previous station catalog.',
|
|
);
|
|
const ERROR_DIRECTORY_UNAVAILABLE = msg(
|
|
'Radio directory is temporarily unavailable.',
|
|
);
|
|
|
|
export function createIngestion({
|
|
state: layerState,
|
|
services,
|
|
parts,
|
|
source,
|
|
}) {
|
|
const methods = {
|
|
/** Refresh directory metadata through the hardened same-origin broker. */
|
|
async update() {
|
|
if (!layerState._enabled) return;
|
|
const generation = ++layerState._requestGeneration;
|
|
const sessionGeneration = layerState._sessionGeneration;
|
|
layerState._abortController?.abort();
|
|
layerState._abortController = new AbortController();
|
|
layerState._loading = true;
|
|
layerState._error = null;
|
|
parts.presentation.emitState();
|
|
try {
|
|
const body = await source.getDirectory({
|
|
signal: layerState._abortController.signal,
|
|
});
|
|
if (
|
|
!parts.model.radioRequestIsCurrent(
|
|
generation,
|
|
layerState._requestGeneration,
|
|
layerState._enabled,
|
|
sessionGeneration,
|
|
layerState._sessionGeneration,
|
|
)
|
|
)
|
|
return;
|
|
if (!Array.isArray(body?.stations))
|
|
throw new Error('Radio directory response was malformed');
|
|
const updatedAt =
|
|
typeof body.updatedAt === 'string' &&
|
|
Number.isFinite(Date.parse(body.updatedAt))
|
|
? body.updatedAt
|
|
: null;
|
|
const updatedAtMs = updatedAt ? Date.parse(updatedAt) : NaN;
|
|
if (
|
|
!updatedAt ||
|
|
updatedAtMs < Date.now() - RADIO_DIRECTORY_STALE_MS ||
|
|
updatedAtMs > Date.now() + RADIO_DIRECTORY_FUTURE_SKEW_MS ||
|
|
typeof body.stale !== 'boolean' ||
|
|
typeof body.degraded !== 'boolean'
|
|
)
|
|
throw new Error('Radio directory freshness metadata was malformed');
|
|
// Rows the server skipped as structurally malformed. Older envelopes
|
|
// omit the field; a present value must be a bounded count.
|
|
const serverRejectedStationCount =
|
|
body.rejectedStationCount === undefined
|
|
? 0
|
|
: body.rejectedStationCount;
|
|
if (
|
|
!Number.isSafeInteger(serverRejectedStationCount) ||
|
|
serverRejectedStationCount < 0 ||
|
|
serverRejectedStationCount > RADIO_REJECTED_STATION_COUNT_MAX
|
|
)
|
|
throw new Error('Radio directory diagnostic metadata was malformed');
|
|
const rows = body.stations;
|
|
const acceptedRows = rows.filter(
|
|
parts.catalogModel.isValidRadioDirectoryStation,
|
|
);
|
|
if (!acceptedRows.length)
|
|
throw new Error('Radio directory returned no usable stations');
|
|
const acceptedGeneration = body.acceptedGeneration;
|
|
if (
|
|
acceptedGeneration !== null &&
|
|
(!Number.isSafeInteger(acceptedGeneration) || acceptedGeneration < 1)
|
|
)
|
|
throw new Error('Radio directory generation metadata was malformed');
|
|
if (!body.stale && !body.degraded && acceptedGeneration === null) {
|
|
throw new Error('Radio directory omitted its accepted generation');
|
|
}
|
|
const catalogInstance = body.catalogInstance;
|
|
if (
|
|
!body.stale &&
|
|
!body.degraded &&
|
|
(typeof catalogInstance !== 'string' || !catalogInstance)
|
|
) {
|
|
throw new Error('Radio directory omitted its catalog instance');
|
|
}
|
|
const preservingWarmCatalog =
|
|
layerState._stations.length > 0 && (body.stale || body.degraded);
|
|
// Generations are only comparable within one producer instance. A new
|
|
// instance token (server restart, different proxy process) starts a fresh
|
|
// sequence: never a repeat and never a regression.
|
|
const sameCatalogInstance =
|
|
layerState._acceptedCatalogSnapshot?.instance === catalogInstance;
|
|
const currentAcceptedGeneration = sameCatalogInstance
|
|
? layerState._acceptedCatalogSnapshot?.generation
|
|
: null;
|
|
if (
|
|
!body.stale &&
|
|
!body.degraded &&
|
|
Number.isSafeInteger(currentAcceptedGeneration) &&
|
|
acceptedGeneration < currentAcceptedGeneration
|
|
)
|
|
throw new Error('Radio directory generation regressed');
|
|
const repeatingAcceptedGeneration =
|
|
!body.stale &&
|
|
!body.degraded &&
|
|
Number.isSafeInteger(currentAcceptedGeneration) &&
|
|
acceptedGeneration === currentAcceptedGeneration;
|
|
if (!preservingWarmCatalog) {
|
|
const immutableRows = acceptedRows.map(
|
|
parts.catalogModel.freezeRadioStation,
|
|
);
|
|
if (repeatingAcceptedGeneration) {
|
|
layerState._updatedAt =
|
|
layerState._acceptedCatalogSnapshot.updatedAt;
|
|
if (
|
|
!layerState._tuningActive &&
|
|
layerState._cancelledTuningPresentationStation
|
|
) {
|
|
layerState._cancelledTuningPresentationStation = null;
|
|
parts.rendering.updateSelectionEntity();
|
|
}
|
|
} else if (!body.stale && !body.degraded) {
|
|
const acceptedSnapshot =
|
|
parts.catalogModel.createAcceptedCatalogSnapshot(
|
|
catalogInstance,
|
|
acceptedGeneration,
|
|
updatedAt,
|
|
immutableRows,
|
|
);
|
|
if (!acceptedSnapshot)
|
|
throw new Error(
|
|
'Radio directory generation metadata was malformed',
|
|
);
|
|
layerState._acceptedCatalogSnapshot = acceptedSnapshot;
|
|
parts.rendering.reconcileStations(acceptedSnapshot.stations);
|
|
} else {
|
|
parts.rendering.reconcileStations(immutableRows);
|
|
}
|
|
if (!repeatingAcceptedGeneration) layerState._updatedAt = updatedAt;
|
|
}
|
|
// Combine server-side structural rejections with rows this client
|
|
// refused. The count is diagnostic: it marks coverage degraded but does
|
|
// not undo the generation admitted above.
|
|
layerState._rejectedStationCount = Math.min(
|
|
RADIO_REJECTED_STATION_COUNT_MAX,
|
|
serverRejectedStationCount + rows.length - acceptedRows.length,
|
|
);
|
|
layerState._degraded =
|
|
body.degraded || layerState._rejectedStationCount > 0;
|
|
layerState._stale = body.stale;
|
|
layerState._error = preservingWarmCatalog
|
|
? ERROR_REFRESH_DEGRADED
|
|
: layerState._degraded
|
|
? ERROR_COVERAGE_DEGRADED
|
|
: null;
|
|
} catch (error) {
|
|
if (
|
|
error?.name === 'AbortError' ||
|
|
!parts.model.radioRequestIsCurrent(
|
|
generation,
|
|
layerState._requestGeneration,
|
|
layerState._enabled,
|
|
sessionGeneration,
|
|
layerState._sessionGeneration,
|
|
)
|
|
)
|
|
return;
|
|
layerState._error = layerState._stations.length
|
|
? ERROR_REFRESH_FAILED
|
|
: ERROR_DIRECTORY_UNAVAILABLE;
|
|
layerState._stale = layerState._stations.length > 0;
|
|
layerState._degraded = layerState._stations.length > 0;
|
|
} finally {
|
|
if (
|
|
generation === layerState._requestGeneration &&
|
|
sessionGeneration === layerState._sessionGeneration
|
|
) {
|
|
layerState._loading = false;
|
|
layerState._abortController = null;
|
|
parts.presentation.emitState();
|
|
}
|
|
}
|
|
},
|
|
};
|
|
|
|
return { methods };
|
|
}
|