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); +});