Accept authoritative empty vessel coverage without an outage

This commit is contained in:
Sameh Khamis committed 2026-10-09 20:50:19 -07:00
1 parent 89e566aa9b
commit 27a986f40f
5 files changed
+67 -4

No files matched your search

+3
View File
@@ -1,5 +1,8 @@
# Changelog
- Treat explicitly current, complete empty vessel coverage as a successful update,
clearing obsolete contacts while retaining stale or incomplete snapshots.
- Custom Street Level provider switches survive share links and stored state
using URL-safe stable provider IDs and a shared boolean-switch codec; existing
Mapillary links keep their meaning. Explicit provider toggles reach durable
+6
View File
@@ -1,5 +1,11 @@
# God's Eye View Current State
Vessel sources can report healthy empty coverage using a current, complete
snapshot with zero raw rows, a healthy transport and a positive last-message
time. This clears obsolete contacts and settles startup without manufacturing
position timestamps. Stale, incomplete and malformed empty results retain the
existing recovery behavior.
Catalog source contracts and their consuming layers are declared in
`src/app/sourceComposition.js`. Omitted or null acquisition sources preserve layer
registration and serialization, report unavailable, and are refused by the existing
+1 -1
View File
@@ -109,7 +109,7 @@ export function createIngestion({
feed.acceptedRowCount = snapshot.acceptedRowCount;
feed.partial = payload?.complete === false;
if (snapshot.acceptedRowCount === 0) {
if (snapshot.acceptedRowCount === 0 && !snapshot.emptyCoverage) {
feed.count = readCount();
feed.stale = feed.count > 0 || Boolean(payload?.refreshing);
if (isDefinitiveTransportFailure(snapshot.transportStatus)) {
+44
View File
@@ -361,3 +361,47 @@ test('partial vessel snapshots preserve freshness and recover after a complete u
layer.destroy();
assert.equal(layer.getStats().partial, false);
});
test('authoritative empty coverage clears old contacts without declaring an outage', async () => {
let current = snapshot([observation('111')]);
const { layer } = setup({
async getSnapshot() {
return current;
},
});
await layer.update();
current = snapshot([], { observedAtMs: null, lastMessageAt: 500 });
await layer.update();
assert.ok(!layer.hasContact('111'));
assert.equal(layer.getStats().count, 0);
assert.equal(layer.getStats().error, null);
assert.equal(layer.getStats().stale, false);
assert.equal(layer.getStats().lastUpdate, null);
});
test('empty coverage cannot hide stale, incomplete or rejected source data', async () => {
for (const patch of [
{ freshness: 'unknown' },
{ freshness: 'stale' },
{ complete: false },
{ rawRowCount: 1 },
{ transportStatus: 'stale' },
{ lastMessageAt: null },
]) {
let current = snapshot([observation('111')]);
const { layer } = setup({
async getSnapshot() {
return current;
},
});
await layer.update();
current = snapshot([], {
observedAtMs: null,
lastMessageAt: 500,
...patch,
});
await layer.update();
assert.equal(layer.hasContact('111'), true, JSON.stringify(patch));
assert.ok(layer.getStats().error, JSON.stringify(patch));
}
});
+13 -3
View File
@@ -119,7 +119,16 @@ export function createQueries({
typeof payload?.status === 'string' ? payload.status : null;
const lastMessageAt = payload?.lastMessageAt ?? null;
const acceptedRowCount = acceptedRows.length;
const emptyCoverage =
rawRows.length === 0 &&
payload?.rawRowCount === 0 &&
payload?.complete === true &&
payload?.freshness === 'current' &&
AIS_HEALTHY_STATUSES.has(transportStatus) &&
Number.isFinite(Number(lastMessageAt)) &&
Number(lastMessageAt) > 0;
return {
emptyCoverage,
transportStatus,
lastMessageAt,
rawRows,
@@ -130,9 +139,10 @@ export function createQueries({
? payload.rawRowCount
: rawRows.length,
acceptedRowCount,
error:
deriveAisFeedError(payload, acceptedRowCount) ||
(acceptedRowCount === 0 ? 'awaiting usable AIS positions…' : null),
error: emptyCoverage
? null
: deriveAisFeedError(payload, acceptedRowCount) ||
(acceptedRowCount === 0 ? 'awaiting usable AIS positions…' : null),
};
}