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.
This commit is contained in:
Sameh Khamis committed 2026-11-10 13:36:18 -08:00
1 parent 66fba3da47
commit c885825f2b
2 files changed
+61 -2

No files matched your search

+13 -1
View File
@@ -29,6 +29,8 @@ const CARD_HOST_OPTIONS = Object.freeze({
moving: false, moving: false,
}); });
const MAX_CARD_LINES = 14; 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). * 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, openExternal = null,
// One collection for every wreck marker (thousands of them): far cheaper to // One collection for every wreck marker (thousands of them): far cheaper to
// draw and rebuild than an entity each. Tests pass a fake. // draw and rebuild than an entity each. Tests pass a fake.
now = () => Date.now(),
createMarkers = (scene) => createMarkers = (scene) =>
new Cesium.BillboardCollection({ new Cesium.BillboardCollection({
scene, scene,
@@ -378,7 +381,7 @@ export function createShipwrecksLayer({
// The page checks every 6 h; the proxy refreshes once a day. Only a // The page checks every 6 h; the proxy refreshes once a day. Only a
// new snapshot rebuilds the markers. // new snapshot rebuilds the markers.
const unchanged = const unchanged =
snapshot.version !== null && typeof snapshot.version === 'string' &&
snapshot.version === _snapshot?.version && snapshot.version === _snapshot?.version &&
_pins.length > 0; _pins.length > 0;
_snapshot = snapshot; _snapshot = snapshot;
@@ -396,6 +399,15 @@ export function createShipwrecksLayer({
return false; return false;
console.warn('[Data:Shipwrecks] Fetch error:', error); console.warn('[Data:Shipwrecks] Fetch error:', error);
_lastError = error?.message || msg('Shipwrecks unavailable'); _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(); notifyRow();
return false; return false;
} finally { } finally {
+48 -1
View File
@@ -51,7 +51,7 @@ function fakeMarkers() {
return collection; return collection;
} }
function harness({ snapshots } = {}) { function harness({ snapshots, now = () => 1 } = {}) {
const markers = fakeMarkers(); const markers = fakeMarkers();
const primitives = []; const primitives = [];
const cards = []; const cards = [];
@@ -81,6 +81,7 @@ function harness({ snapshots } = {}) {
getSnapshot({ signal }) { getSnapshot({ signal }) {
requests.push(signal); requests.push(signal);
const next = queue.shift(); const next = queue.shift();
if (next instanceof Error) return Promise.reject(next);
return next instanceof Promise ? next : Promise.resolve(next); return next instanceof Promise ? next : Promise.resolve(next);
}, },
}, },
@@ -105,6 +106,7 @@ function harness({ snapshots } = {}) {
picking, picking,
pointer: { isPointerFree: () => true }, pointer: { isPointerFree: () => true },
openExternal: (url) => opened.push(url), openExternal: (url) => opened.push(url),
now: () => now(),
}); });
layer.init(viewer); layer.init(viewer);
return { 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], [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);
});