mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 02:44:02 +00:00
fix(perimeters): report a stale WFIGS snapshot as stale, and stop serving it after a day
When WFIGS was unreachable the proxy served its last snapshot, marked stale
(X-Data-Stale, body.stale), with no age limit, and the client source kept
only the rows. The layer then read healthy with a fresh update time and
get_fire_perimeters reported the perimeters as current; three days into an
outage it still said "1 mapped wildfire perimeter in the requested box."
The source now offers getSnapshotDetail ({rows, stale, fetchedAt}); the
layer reports stale with the snapshot's fetch time, the tool adds stale,
retrieved_at and "(data may be stale)", and the proxy serves a stale
snapshot for at most 24 hours, as the GNSS proxy caps its own.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
2006c035d9
commit
25973ade2c
8 files changed
+190
-9
No files matched your search
@@ -55,6 +55,8 @@ export function createFirePerimetersLayer({
|
||||
let _dataSource = null;
|
||||
let _count = 0;
|
||||
let _lastUpdate = null;
|
||||
// The proxy's last snapshot, served while WFIGS is unreachable.
|
||||
let _stale = false;
|
||||
let _lastError = null;
|
||||
let _enabled = false;
|
||||
let _clickHandler = null;
|
||||
@@ -260,6 +262,7 @@ export function createFirePerimetersLayer({
|
||||
_count = 0;
|
||||
_lastUpdate = null;
|
||||
_lastError = null;
|
||||
_stale = false;
|
||||
_enabled = false;
|
||||
console.log('[Data:FirePerimeters] Initialized');
|
||||
},
|
||||
@@ -287,9 +290,21 @@ export function createFirePerimetersLayer({
|
||||
_request = request;
|
||||
refreshInciwebIndex(request.signal);
|
||||
try {
|
||||
const rows = await source.getSnapshot({ signal: request.signal });
|
||||
// The source contract is getSnapshot; a source that can also say how
|
||||
// fresh its rows are (the WFIGS proxy) offers getSnapshotDetail.
|
||||
const snapshot =
|
||||
typeof source.getSnapshotDetail === 'function'
|
||||
? await source.getSnapshotDetail({ signal: request.signal })
|
||||
: { rows: await source.getSnapshot({ signal: request.signal }) };
|
||||
if (request.signal.aborted || _request !== request || !_enabled)
|
||||
return false;
|
||||
const { rows } = snapshot;
|
||||
_stale = snapshot.stale === true;
|
||||
// A stale snapshot is as old as its fetch, not this poll.
|
||||
const updatedAt =
|
||||
_stale && Number.isFinite(snapshot.fetchedAt)
|
||||
? snapshot.fetchedAt
|
||||
: Date.now();
|
||||
|
||||
// Incident facts may change independently of the polygon's source clock.
|
||||
const signature = JSON.stringify(
|
||||
@@ -299,7 +314,7 @@ export function createFirePerimetersLayer({
|
||||
);
|
||||
if (signature === _snapshotSignature) {
|
||||
if (_selectedId) publishSelectedCard();
|
||||
_lastUpdate = Date.now();
|
||||
_lastUpdate = updatedAt;
|
||||
_lastError = null;
|
||||
return true;
|
||||
}
|
||||
@@ -355,7 +370,7 @@ export function createFirePerimetersLayer({
|
||||
// Refresh (or drop) the selected incident's card against the new feed.
|
||||
if (_selectedId) publishSelectedCard();
|
||||
_count = rows.length;
|
||||
_lastUpdate = Date.now();
|
||||
_lastUpdate = updatedAt;
|
||||
_lastError = null;
|
||||
console.log(
|
||||
`[Data:FirePerimeters] Updated: ${_count} incidents, ${nextEntities.length} polygons`,
|
||||
@@ -366,6 +381,8 @@ export function createFirePerimetersLayer({
|
||||
return false;
|
||||
console.warn('[Data:FirePerimeters] Fetch error:', e);
|
||||
_lastError = e?.message || msg('Perimeter source unavailable');
|
||||
// A failure now outranks the stale snapshot still drawn.
|
||||
_stale = false;
|
||||
return false;
|
||||
} finally {
|
||||
if (_request === request) _request = null;
|
||||
@@ -391,6 +408,7 @@ export function createFirePerimetersLayer({
|
||||
_count = 0;
|
||||
_lastUpdate = null;
|
||||
_lastError = null;
|
||||
_stale = false;
|
||||
},
|
||||
|
||||
/** Snapshot incident facts (with card-anchor coordinates) for the analyst query engine. */
|
||||
@@ -443,6 +461,7 @@ export function createFirePerimetersLayer({
|
||||
count: _count,
|
||||
lastUpdate: _lastUpdate,
|
||||
error: _lastError,
|
||||
stale: _stale,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -552,3 +552,36 @@ for (const action of ['disable', 'destroy', 'selection']) {
|
||||
if (action !== 'destroy') h.layer.destroy();
|
||||
});
|
||||
}
|
||||
|
||||
test('a stale snapshot reads stale, dated by its own fetch, and a fresh one clears it', async () => {
|
||||
const fetchedAt = Date.now() - 3 * 60 * 60 * 1000;
|
||||
let snapshot = { rows: [row], stale: true, fetchedAt };
|
||||
const h = harness({
|
||||
getSnapshot: async () => snapshot.rows,
|
||||
getSnapshotDetail: async () => snapshot,
|
||||
});
|
||||
await h.layer.update(h.viewer);
|
||||
assert.equal(h.layer.getStats().stale, true);
|
||||
assert.equal(h.layer.getStats().lastUpdate, fetchedAt);
|
||||
snapshot = { rows: [row], stale: false, fetchedAt: Date.now() };
|
||||
await h.layer.update(h.viewer);
|
||||
assert.equal(h.layer.getStats().stale, false);
|
||||
assert.ok(h.layer.getStats().lastUpdate > fetchedAt);
|
||||
});
|
||||
|
||||
test('a failed refresh after a stale snapshot reads as the failure, not stale', async () => {
|
||||
let fail = false;
|
||||
const h = harness({
|
||||
getSnapshot: async () => [row],
|
||||
getSnapshotDetail: async () => {
|
||||
if (fail) throw new Error('WFIGS HTTP 502');
|
||||
return { rows: [row], stale: true, fetchedAt: Date.now() - 60_000 };
|
||||
},
|
||||
});
|
||||
await h.layer.update(h.viewer);
|
||||
assert.equal(h.layer.getStats().stale, true);
|
||||
fail = true;
|
||||
await h.layer.update(h.viewer);
|
||||
assert.equal(h.layer.getStats().stale, false);
|
||||
assert.equal(h.layer.getStats().error, 'WFIGS HTTP 502');
|
||||
});
|
||||
@@ -9,7 +9,17 @@ export function createWfigsPerimeterSource({
|
||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||
} = {}) {
|
||||
return {
|
||||
async getSnapshot({ signal } = {}) {
|
||||
/** The perimeter rows (see getSnapshotDetail for staleness). */
|
||||
async getSnapshot(options = {}) {
|
||||
return (await this.getSnapshotDetail(options)).rows;
|
||||
},
|
||||
|
||||
/**
|
||||
* The rows with how fresh they are: the proxy answers from its last
|
||||
* snapshot, marked stale, while WFIGS is unreachable.
|
||||
* @returns {Promise<{rows: object[], stale: boolean, fetchedAt: number|null}>}
|
||||
*/
|
||||
async getSnapshotDetail({ signal } = {}) {
|
||||
signal?.throwIfAborted();
|
||||
const response = await fetchImpl('/api/fire-perimeters', { signal });
|
||||
if (!response.ok) throw new Error(`WFIGS HTTP ${response.status}`);
|
||||
@@ -21,7 +31,13 @@ export function createWfigsPerimeterSource({
|
||||
signal?.throwIfAborted();
|
||||
if (!Array.isArray(payload?.rows))
|
||||
throw new Error(msg('Malformed perimeter snapshot'));
|
||||
return payload.rows;
|
||||
return {
|
||||
rows: payload.rows,
|
||||
stale: payload.stale === true,
|
||||
fetchedAt: Number.isFinite(payload.fetchedAt)
|
||||
? payload.fetchedAt
|
||||
: null,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -115,3 +115,60 @@ test('response-body completion honors cancellation without replacing records', a
|
||||
name: 'AbortError',
|
||||
});
|
||||
});
|
||||
|
||||
test('a failed refresh serves the last snapshot marked stale for a day, then refuses', async () => {
|
||||
let clock = 1_000_000_000;
|
||||
let up = true;
|
||||
const plugin = firePerimetersProxy({
|
||||
now: () => clock,
|
||||
fetchImpl: async () => {
|
||||
if (!up) throw new Error('WFIGS unreachable');
|
||||
return Response.json(validPayload);
|
||||
},
|
||||
});
|
||||
let handler;
|
||||
plugin.configureServer({
|
||||
middlewares: { use: (_path, callback) => (handler = callback) },
|
||||
});
|
||||
const ask = async () => {
|
||||
const reply = {};
|
||||
await handler(
|
||||
{ url: '/', method: 'GET', socket: { remoteAddress: '127.0.0.1' } },
|
||||
{
|
||||
writeHead(status, headers) {
|
||||
reply.status = status;
|
||||
reply.staleHeader = headers?.['X-Data-Stale'] === 'true';
|
||||
},
|
||||
end(body) {
|
||||
reply.body = JSON.parse(body);
|
||||
},
|
||||
},
|
||||
);
|
||||
return reply;
|
||||
};
|
||||
const fresh = await ask();
|
||||
assert.equal(fresh.status, 200);
|
||||
assert.equal(fresh.body.stale, undefined);
|
||||
up = false;
|
||||
clock += 23 * 60 * 60 * 1000;
|
||||
const stale = await ask();
|
||||
assert.equal(stale.status, 200);
|
||||
assert.equal(stale.staleHeader, true);
|
||||
assert.equal(stale.body.stale, true);
|
||||
assert.equal(stale.body.fetchedAt, fresh.body.fetchedAt);
|
||||
clock += 2 * 60 * 60 * 1000;
|
||||
assert.equal((await ask()).status, 502, 'a day-old snapshot is not served');
|
||||
});
|
||||
|
||||
test('the snapshot detail carries the proxy staleness and fetch time', async () => {
|
||||
const fetchedAt = Date.UTC(2026, 9, 25, 12);
|
||||
const source = createWfigsPerimeterSource({
|
||||
fetchImpl: async () => Response.json({ rows: [], stale: true, fetchedAt }),
|
||||
});
|
||||
assert.deepEqual(await source.getSnapshotDetail(), {
|
||||
rows: [],
|
||||
stale: true,
|
||||
fetchedAt,
|
||||
});
|
||||
assert.deepEqual(await source.getSnapshot(), []);
|
||||
});
|
||||
Reference in new issue
Block a user