From c885825f2b1df83e280e96ac4e2626c61bcbaaa7 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Tue, 10 Nov 2026 13:36:18 -0800 Subject: [PATCH] fix(shipwrecks): rebuild unless the snapshot has a version string; a failed page refresh marks the wrecks stale and clears them after seven days - A source without a version (undefined, not null) compared as unchanged, so an injected source's new snapshots were never drawn. - When the page could not refresh (the proxy answers 502 once both sources are past seven days), it kept showing the old wrecks as fresh. They are now marked stale, and removed once older than the proxy's own seven-day limit. --- src/layers/shipwrecks/index.js | 14 +++++++- src/layers/shipwrecks/index.test.mjs | 49 +++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/layers/shipwrecks/index.js b/src/layers/shipwrecks/index.js index f2451d7bd..c4599303c 100644 --- a/src/layers/shipwrecks/index.js +++ b/src/layers/shipwrecks/index.js @@ -29,6 +29,8 @@ const CARD_HOST_OPTIONS = Object.freeze({ moving: false, }); const MAX_CARD_LINES = 14; +/** Wrecks not refreshed for this long leave the map (the proxy's own limit). */ +const MAX_SHOWN_AGE_MS = 7 * 24 * 60 * 60 * 1000; /** * Marker size in pixels: grows with the death toll, from 16 px (no deaths known) to 36 px (about a thousand or more). */ @@ -67,6 +69,7 @@ export function createShipwrecksLayer({ openExternal = null, // One collection for every wreck marker (thousands of them): far cheaper to // draw and rebuild than an entity each. Tests pass a fake. + now = () => Date.now(), createMarkers = (scene) => new Cesium.BillboardCollection({ scene, @@ -378,7 +381,7 @@ export function createShipwrecksLayer({ // The page checks every 6 h; the proxy refreshes once a day. Only a // new snapshot rebuilds the markers. const unchanged = - snapshot.version !== null && + typeof snapshot.version === 'string' && snapshot.version === _snapshot?.version && _pins.length > 0; _snapshot = snapshot; @@ -396,6 +399,15 @@ export function createShipwrecksLayer({ return false; console.warn('[Data:Shipwrecks] Fetch error:', error); _lastError = error?.message || msg('Shipwrecks unavailable'); + // What is on the map was not refreshed: say so, and drop it once it + // is older than the proxy itself would serve. + if (_snapshot) { + const age = now() - (_snapshot.fetchedAt ?? now()); + if (age > MAX_SHOWN_AGE_MS) { + _snapshot = null; + rebuild([]); + } else _snapshot = { ..._snapshot, stale: true }; + } notifyRow(); return false; } finally { diff --git a/src/layers/shipwrecks/index.test.mjs b/src/layers/shipwrecks/index.test.mjs index d42aedaf1..92cb05733 100644 --- a/src/layers/shipwrecks/index.test.mjs +++ b/src/layers/shipwrecks/index.test.mjs @@ -51,7 +51,7 @@ function fakeMarkers() { return collection; } -function harness({ snapshots } = {}) { +function harness({ snapshots, now = () => 1 } = {}) { const markers = fakeMarkers(); const primitives = []; const cards = []; @@ -81,6 +81,7 @@ function harness({ snapshots } = {}) { getSnapshot({ signal }) { requests.push(signal); const next = queue.shift(); + if (next instanceof Error) return Promise.reject(next); return next instanceof Promise ? next : Promise.resolve(next); }, }, @@ -105,6 +106,7 @@ function harness({ snapshots } = {}) { picking, pointer: { isPointerFree: () => true }, openExternal: (url) => opened.push(url), + now: () => now(), }); layer.init(viewer); return { @@ -244,3 +246,48 @@ test('the year pickers list only years with a wreck, and each era is counted', a [0, 2, 0, 0], ); }); + +test('a source without a version rebuilds on every new snapshot', async () => { + const bare = (records) => { + const value = snapshot(records); + delete value.version; + return value; + }; + const h = harness({ + snapshots: [ + bare([wreck('a')]), + bare([wreck('a'), wreck('b')]), + { ...snapshot([wreck('a')]), version: null }, + { ...snapshot([wreck('a'), wreck('b'), wreck('c')]), version: null }, + ], + }); + h.layer.enable(); + for (const expected of [1, 2, 1, 3]) { + await h.layer.update(); + assert.equal(h.markers.billboards.length, expected); + } +}); + +test('a failed refresh marks the shown wrecks stale, and clears them after seven days', async () => { + const DAY = 24 * 60 * 60 * 1000; + let clock = 1; + const h = harness({ + now: () => clock, + snapshots: [ + snapshot([wreck('a')]), + new Error('Shipwrecks unavailable (HTTP 502)'), + new Error('Shipwrecks unavailable (HTTP 502)'), + ], + }); + h.layer.enable(); + await h.layer.update(); + assert.equal(h.layer.getStats().stale, false); + clock = 1 + 2 * DAY; + assert.equal(await h.layer.update(), false); + assert.equal(h.layer.getStats().stale, true); + assert.equal(h.layer.getStats().count, 1, 'still shown, as stale'); + clock = 1 + 8 * DAY; + await h.layer.update(); + assert.equal(h.layer.getStats().count, 0, 'expired wrecks leave the map'); + assert.equal(h.markers.billboards.length, 0); +});