mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
Keep STALE on Contacts while a poll is in flight
A poll that returned the same stale snapshot dropped STALE from the Contacts cohort until the next answer, because the in-flight loading flag outranked stale and the source revision ignored the provider name. A source that has answered now reads its status without the loading flag; one that has never answered (AIS first connect) still reads as loading, so it shows no STALE before any snapshot exists.
This commit is contained in:
1 parent
490c8d1769
commit
99831ea19b
5 files changed
+167
-5
No files matched your search
@@ -91,6 +91,7 @@ export function createQueries({ state: layerState, services, parts, source }) {
|
|||||||
stats.count ?? null,
|
stats.count ?? null,
|
||||||
stats.status || null,
|
stats.status || null,
|
||||||
stats.error || null,
|
stats.error || null,
|
||||||
|
stats.source || null,
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
.map((parts) => parts.join(':'))
|
.map((parts) => parts.join(':'))
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { createQueries } from './queries.js';
|
||||||
|
import { createSubject } from './subject.js';
|
||||||
|
import { createModel } from './model.js';
|
||||||
|
import { summarizeAwarenessCohort } from '../../data/militaryAwarenessEngine.js';
|
||||||
|
import { layerFeedState } from '../../data/feedState.js';
|
||||||
|
import aisLiveVesselsLayer, {
|
||||||
|
_setVesselStateForTest,
|
||||||
|
_beginAisSessionForTest,
|
||||||
|
_applyAisFeedSnapshotForTest,
|
||||||
|
_setAisRuntimeForTest,
|
||||||
|
} from '../../data/aisLiveVessels.js';
|
||||||
|
|
||||||
|
test('a poll that returns the same stale AIS snapshot restores STALE on Contacts cohorts', () => {
|
||||||
|
globalThis.document ??= { body: null };
|
||||||
|
const services = {
|
||||||
|
installations: { getNearby: () => [], getStats: () => ({}) },
|
||||||
|
flights: { getNearby: () => [] },
|
||||||
|
military: { getNearby: () => [] },
|
||||||
|
vessels: { getNearby: () => [] },
|
||||||
|
};
|
||||||
|
const layerState = {
|
||||||
|
enabled: true,
|
||||||
|
passive: true,
|
||||||
|
sourceRevision: '',
|
||||||
|
cohortPages: new Map(),
|
||||||
|
subject: {
|
||||||
|
id: 'site-1',
|
||||||
|
layerId: 'military-installations',
|
||||||
|
label: 'Site',
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let states;
|
||||||
|
const parts = {
|
||||||
|
navigation: {
|
||||||
|
summarizeAwarenessCohortForNavigation: (items, source) =>
|
||||||
|
summarizeAwarenessCohort(items, source),
|
||||||
|
isFlightLayer: (id) => id === 'flights' || id === 'military',
|
||||||
|
},
|
||||||
|
rendering: { renderVisual() {}, scheduleDirectionOverlayUpdate() {} },
|
||||||
|
panel: { renderResults() {} },
|
||||||
|
};
|
||||||
|
const realQueries = createQueries({ state: layerState, services, parts });
|
||||||
|
parts.queries = { ...realQueries, collectSourceStates: () => states };
|
||||||
|
parts.model = createModel({ state: layerState, services, parts });
|
||||||
|
const subject = createSubject({ state: layerState, services, parts });
|
||||||
|
const flights = (lastUpdate) => ({
|
||||||
|
available: true, enabled: true, stale: false,
|
||||||
|
stats: { source: 'OpenSky Network', count: 4, lastUpdate },
|
||||||
|
});
|
||||||
|
const vessels = (loading) => ({
|
||||||
|
available: true, enabled: true, stale: true,
|
||||||
|
stats: { source: 'Datalastic', stale: true, count: 3, lastUpdate: 1000, loading },
|
||||||
|
});
|
||||||
|
const rest = {
|
||||||
|
military: { available: true, enabled: true, stale: false, stats: { source: 'adsb.lol', fallback: false, count: 1, lastUpdate: 1 } },
|
||||||
|
'military-installations': { available: true, enabled: true, stale: false, stats: { coverage: { kind: 'subject', radiusM: 100000 } } },
|
||||||
|
};
|
||||||
|
const ais = () => layerState.results.cohorts.find((c) => c.id === 'ais-live-vessels');
|
||||||
|
|
||||||
|
// 1. Stale AIS cache, nothing loading.
|
||||||
|
states = { flights: flights(1), 'ais-live-vessels': vessels(false), ...rest };
|
||||||
|
subject.refreshSelectedSubject(true);
|
||||||
|
assert.equal(ais().sourceStatus, 'stale');
|
||||||
|
|
||||||
|
// 2. AIS re-poll in flight while the flights poll lands (revision changes).
|
||||||
|
states = { flights: flights(2), 'ais-live-vessels': vessels(true), ...rest };
|
||||||
|
subject.refreshSelectedSubject();
|
||||||
|
assert.equal(ais().sourceStatus, 'stale', 'a poll in flight keeps the snapshot stale');
|
||||||
|
|
||||||
|
// 3. AIS poll returns the same stale snapshot; only loading flips back.
|
||||||
|
states = { flights: flights(2), 'ais-live-vessels': vessels(false), ...rest };
|
||||||
|
subject.refreshSelectedSubject();
|
||||||
|
subject.refreshSelectedSubject();
|
||||||
|
assert.equal(ais().sourceStatus, 'stale', 'Layers reads STALE again; Contacts/Cockpit must too');
|
||||||
|
});
|
||||||
|
|
||||||
|
function fakeRuntime() {
|
||||||
|
return {
|
||||||
|
now: () => 1000,
|
||||||
|
setTimeout: () => ({}),
|
||||||
|
clearTimeout() {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('first AIS connect (no snapshot yet) carries no STALE on the Contacts cohort', () => {
|
||||||
|
globalThis.document ??= { body: null };
|
||||||
|
_setAisRuntimeForTest(fakeRuntime());
|
||||||
|
_setVesselStateForTest({ viewer: {}, records: [] });
|
||||||
|
try {
|
||||||
|
_beginAisSessionForTest();
|
||||||
|
// What server/providers/vessels/ais-live.js answers while its socket connects.
|
||||||
|
_applyAisFeedSnapshotForTest({}, {
|
||||||
|
rows: [], source: 'AISStream', status: 'connecting', refreshing: true,
|
||||||
|
});
|
||||||
|
const vesselStats = aisLiveVesselsLayer.getStats();
|
||||||
|
assert.equal(vesselStats.loading, true);
|
||||||
|
assert.equal(vesselStats.stale, true);
|
||||||
|
assert.equal(vesselStats.lastUpdate, null);
|
||||||
|
assert.equal(layerFeedState(vesselStats), 'loading', 'Layers chip reads LOADING');
|
||||||
|
|
||||||
|
const flightsStats = { source: 'OpenSky Network', count: 2, lastUpdate: 5 };
|
||||||
|
const militaryStats = { source: 'adsb.lol', fallback: false, count: 1, lastUpdate: 5 };
|
||||||
|
const installStats = { coverage: { kind: 'subject', radiusM: 100000 } };
|
||||||
|
const statsById = {
|
||||||
|
flights: flightsStats,
|
||||||
|
military: militaryStats,
|
||||||
|
'ais-live-vessels': vesselStats,
|
||||||
|
'military-installations': installStats,
|
||||||
|
};
|
||||||
|
const services = {
|
||||||
|
installations: { getNearby: () => [], getStats: () => installStats },
|
||||||
|
flights: { getNearby: () => [] },
|
||||||
|
military: { getNearby: () => [] },
|
||||||
|
vessels: aisLiveVesselsLayer,
|
||||||
|
};
|
||||||
|
const layerState = {
|
||||||
|
enabled: true,
|
||||||
|
passive: true,
|
||||||
|
sourceRevision: '',
|
||||||
|
cohortPages: new Map(),
|
||||||
|
dataManager: {
|
||||||
|
isEnabled: () => true,
|
||||||
|
getLayerLifecycleState: () => ({ enabled: true, lifecycleState: 'enabled' }),
|
||||||
|
layers: new Map(Object.entries(statsById).map(([id, s]) => [id, { module: { getStats: () => s } }])),
|
||||||
|
},
|
||||||
|
subject: {
|
||||||
|
id: 'site-1',
|
||||||
|
layerId: 'military-installations',
|
||||||
|
label: 'Site',
|
||||||
|
position: Cesium.Cartesian3.fromDegrees(0, 0, 0),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const parts = {
|
||||||
|
navigation: {
|
||||||
|
summarizeAwarenessCohortForNavigation: (items, source) => summarizeAwarenessCohort(items, source),
|
||||||
|
isFlightLayer: (id) => id === 'flights' || id === 'military',
|
||||||
|
},
|
||||||
|
rendering: { renderVisual() {}, scheduleDirectionOverlayUpdate() {} },
|
||||||
|
panel: { renderResults() {} },
|
||||||
|
};
|
||||||
|
parts.queries = createQueries({ state: layerState, services, parts });
|
||||||
|
parts.model = createModel({ state: layerState, services, parts });
|
||||||
|
const subject = createSubject({ state: layerState, services, parts });
|
||||||
|
|
||||||
|
const vesselsSource = parts.queries.collectSourceStates()['ais-live-vessels'];
|
||||||
|
assert.equal(vesselsSource.available, false, 'never-answered source is unavailable');
|
||||||
|
|
||||||
|
subject.refreshSelectedSubject(true);
|
||||||
|
const ais = layerState.results.cohorts.find((c) => c.id === 'ais-live-vessels');
|
||||||
|
assert.equal(ais.sourceStatus, null, 'no snapshot yet: no STALE label, Layers says LOADING');
|
||||||
|
} finally {
|
||||||
|
_setVesselStateForTest({ enabled: false });
|
||||||
|
_setAisRuntimeForTest();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -54,8 +54,14 @@ export function createSubject({ state: layerState, services, parts, source }) {
|
|||||||
const installationsState = sourceStates['military-installations'];
|
const installationsState = sourceStates['military-installations'];
|
||||||
const evaluatedAt = Date.now();
|
const evaluatedAt = Date.now();
|
||||||
// Whether the provider answered as a fallback or from a stale snapshot.
|
// Whether the provider answered as a fallback or from a stale snapshot.
|
||||||
|
// A poll in flight does not change that, and the source revision ignores it.
|
||||||
|
// Before the first answer there is no snapshot, so loading stays.
|
||||||
const sourceStatus = (sourceState) => {
|
const sourceStatus = (sourceState) => {
|
||||||
const feedState = layerFeedState(sourceState.stats);
|
const feedState = layerFeedState(
|
||||||
|
sourceState.available
|
||||||
|
? { ...sourceState.stats, loading: false }
|
||||||
|
: sourceState.stats,
|
||||||
|
);
|
||||||
return feedState === 'fallback' || feedState === 'stale'
|
return feedState === 'fallback' || feedState === 'stale'
|
||||||
? feedState
|
? feedState
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -411,8 +411,6 @@ test('empty coverage cannot hide stale, incomplete or rejected source data', asy
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('vessel stats name the provider the last snapshot reported', async () => {
|
test('vessel stats name the provider the last snapshot reported', async () => {
|
||||||
// HAL-721: Contacts named a fixed AIS provider while this layer showed the
|
|
||||||
// one its server reported.
|
|
||||||
const { layer } = setup({
|
const { layer } = setup({
|
||||||
label: 'Vessels',
|
label: 'Vessels',
|
||||||
async getSnapshot() {
|
async getSnapshot() {
|
||||||
|
|||||||
@@ -167,8 +167,6 @@ test('a snapshot fills the card and keeps classes other code sets', async (t) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('each count names the provider its snapshot reports, with its feed state', async (t) => {
|
test('each count names the provider its snapshot reports, with its feed state', async (t) => {
|
||||||
// HAL-721: the counters named fixed providers while the layer and the
|
|
||||||
// Contacts list named the one that answered.
|
|
||||||
const reported = snapshot();
|
const reported = snapshot();
|
||||||
reported.cohorts[0].source = 'adsb.lol';
|
reported.cohorts[0].source = 'adsb.lol';
|
||||||
reported.cohorts[0].sourceStatus = 'fallback';
|
reported.cohorts[0].sourceStatus = 'fallback';
|
||||||
|
|||||||
Reference in new issue
Block a user