mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
fix(firms): record source success after aggregation (#181)
Record FIRMS source success after appending its rows so aggregation failures do not produce contradictory source status. Cover large feeds, filtering, ordering and source failures with regression tests.
This commit is contained in:
1 parent
87a92c8bc4
commit
e7cb08f5f6
4 files changed
+90
-1
No files matched your search
@@ -15,6 +15,8 @@ of current runtime behavior, see [`docs/CURRENT-STATE.md`](docs/CURRENT-STATE.md
|
||||
|
||||
### Fixed
|
||||
|
||||
- FIRMS records a source as successful only after appending its rows, avoiding
|
||||
contradictory success/failure status if aggregation throws.
|
||||
- Radio country filtering and voice country requests now resolve common English
|
||||
names and exonyms that `Intl.DisplayNames`' primary label omits, so requests
|
||||
like "play radio in Turkey" no longer fail closed (Turkey → Türkiye, plus
|
||||
|
||||
@@ -10,6 +10,13 @@ Launch payloads with missing records now say PAYLOAD DATA UNAVAILABLE. Missing n
|
||||
|
||||
Updated: August 24, 2026
|
||||
|
||||
## FIRMS source status
|
||||
|
||||
The FIRMS proxy records source success after appending its rows. If aggregation
|
||||
throws, that source reports failure without a contradictory success entry.
|
||||
The existing per-record append continues to support large feeds; sequential
|
||||
fetching, trailing-24-hour filtering and partial-success caching are unchanged.
|
||||
|
||||
## Installations and map-source guidance
|
||||
|
||||
- On an uncached Overpass failure, mapped installations keep their existing
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { filterTrailing24h, parseFirmsCsv } from './firmsCsv.js';
|
||||
|
||||
const config = fs.readFileSync(new URL('../../vite.config.js', import.meta.url), 'utf8');
|
||||
const start = config.indexOf(' async function refreshUpstream(key) {');
|
||||
assert.notEqual(start, -1, 'FIRMS refresh function exists');
|
||||
const end = config.indexOf('\n }', start);
|
||||
assert.notEqual(end, -1, 'FIRMS refresh function closes');
|
||||
const refreshSource = config.slice(start, end + 4);
|
||||
const SOURCES = ['VIIRS_NOAA20_NRT', 'VIIRS_NOAA21_NRT', 'VIIRS_SNPP_NRT'];
|
||||
const NOW = Date.UTC(2026, 8, 11, 12);
|
||||
const recent = { acqDate: '2026-09-11', acqTime: '1100' };
|
||||
|
||||
// Exercise the production refresh without opening a server or using a MAP_KEY.
|
||||
// Inject only its upstream, clock and filter dependencies; keep its aggregation
|
||||
// and source-status code intact, including failures while consuming records.
|
||||
function createRefresh(fetchSource, filter = filterTrailing24h) {
|
||||
return new Function('SOURCES', 'fetchSource', 'filterTrailing24h', 'Date', 'console',
|
||||
`return (${refreshSource});`)(SOURCES, fetchSource, filter, { now: () => NOW }, { warn() {} });
|
||||
}
|
||||
|
||||
test('FIRMS retains large sources in order and filters expired rows', async () => {
|
||||
const header = 'latitude,longitude,acq_date,acq_time,confidence,frp\n';
|
||||
const large = parseFirmsCsv(header + '1,2,2026-09-11,1100,n,4\n'.repeat(200_000));
|
||||
const expired = { ...recent, acqDate: '2026-09-09' };
|
||||
const last = { ...recent, marker: 'last' };
|
||||
const calls = [];
|
||||
let active = 0;
|
||||
const refresh = createRefresh(async (key, source) => {
|
||||
assert.equal(active++, 0, 'sources must be fetched sequentially');
|
||||
calls.push(source);
|
||||
await Promise.resolve();
|
||||
active--;
|
||||
return source === SOURCES[0] ? [...large, expired] : source === SOURCES[1] ? [last] : [];
|
||||
});
|
||||
const result = await refresh('fixture');
|
||||
assert.deepEqual(calls, SOURCES);
|
||||
assert.equal(result.fires.length, 200_001);
|
||||
assert.equal(result.fires[0], large[0]);
|
||||
assert.equal(result.fires[199_999], large.at(-1));
|
||||
assert.equal(result.fires.at(-1), last);
|
||||
assert.deepEqual(result.sources, SOURCES.map((source, index) => ({
|
||||
source, count: [200_000, 1, 0][index], ok: true,
|
||||
})));
|
||||
});
|
||||
|
||||
test('FIRMS keeps successful sources when another upstream fails', async () => {
|
||||
const result = await createRefresh(async (key, source) => {
|
||||
if (source === SOURCES[1]) throw new Error('upstream unavailable');
|
||||
return [recent];
|
||||
})('fixture');
|
||||
assert.equal(result.fires.length, 2);
|
||||
assert.deepEqual(result.sources, SOURCES.map((source, index) => ({
|
||||
source, count: index === 1 ? 0 : 1, ok: index !== 1,
|
||||
})));
|
||||
});
|
||||
|
||||
test('FIRMS reports one failure if consuming a source throws before append', async () => {
|
||||
const failing = [];
|
||||
const result = await createRefresh(async (key, source) => source === SOURCES[0] ? failing : [recent],
|
||||
(records, now) => {
|
||||
if (records !== failing) return filterTrailing24h(records, now);
|
||||
// Fault injection for aggregation; ordinary parsed CSV returns an array.
|
||||
return { length: 1, [Symbol.iterator]() { throw new Error('aggregation failed'); } };
|
||||
})('fixture');
|
||||
assert.equal(result.fires.length, 2);
|
||||
assert.deepEqual(result.sources, SOURCES.map((source, index) => ({
|
||||
source, count: index === 0 ? 0 : 1, ok: index !== 0,
|
||||
})));
|
||||
});
|
||||
|
||||
test('FIRMS distinguishes all-source failure from successful empty sources', async () => {
|
||||
await assert.rejects(createRefresh(async () => { throw new Error('upstream unavailable'); })('fixture'),
|
||||
/all FIRMS sources failed/);
|
||||
const result = await createRefresh(async () => [])('fixture');
|
||||
assert.deepEqual(result.fires, []);
|
||||
assert.deepEqual(result.sources, SOURCES.map(source => ({ source, count: 0, ok: true })));
|
||||
});
|
||||
+1
-1
@@ -2094,11 +2094,11 @@ function firmsProxy() {
|
||||
for (const source of SOURCES) {
|
||||
try {
|
||||
const records = filterTrailing24h(await fetchSource(key, source), now);
|
||||
sources.push({ source, count: records.length, ok: true });
|
||||
// NOT fires.push(...records): spread passes each record as an argument,
|
||||
// and a world/2 VIIRS pull exceeds V8's argument limit (~125k) at
|
||||
// ~131k records — RangeError, and the whole source is silently dropped.
|
||||
for (const record of records) fires.push(record);
|
||||
sources.push({ source, count: records.length, ok: true });
|
||||
} catch (err) {
|
||||
console.warn(`[firms-proxy] ${source} fetch failed:`, err?.message || err);
|
||||
sources.push({ source, count: 0, ok: false });
|
||||
|
||||
Reference in new issue
Block a user