Preserve source identity and history references through layer state

This commit is contained in:
Sameh Khamis committed 2026-09-13 09:06:13 -07:00
1 parent 80c5a09f15
commit 73135a7ae4
8 files changed
+92 -37

No files matched your search

+2 -1
View File
@@ -10,7 +10,8 @@ retain separate heading/course and sea-surface datum. History is a best-effort
addition to the locally accumulated trail, never a promise of complete coverage.
Snapshot coverage, completeness and freshness are separate fields. A partially
admitted aircraft snapshot cannot evict absent contacts. An invalid nonempty
admitted aircraft snapshot retains absent contacts for up to five minutes before
the usual missed-poll eviction. Unknown snapshot times remain unknown in stats. An invalid nonempty
snapshot retains the previous display. Empty vessel refreshes retain the existing
first-connect grace and warm-data behavior. Known source failures have bounded
messages; arbitrary HTTP response bodies are not surfaced as diagnostics.
+4 -2
View File
@@ -1,6 +1,5 @@
import { createAisStreamSource } from '../sources/live/standalone.js';
import * as Cesium from 'cesium';
let _source = createAisStreamSource({ apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live' });
import {
registerEntityContext,
selectEntityContext,
@@ -48,6 +47,8 @@ import {
import { requestWorldFocus } from '../worldFocus.js';
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
let _source = createAisStreamSource({ apiUrl: import.meta.env?.VITE_AIS_LIVE_API_URL || '/api/ais-live' });
const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
/** Camera pose signature at the last vessel rotation pass. */
@@ -349,6 +350,7 @@ const aisLiveVesselsLayer = {
if (state.viewer) throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
_source = source;
this.source = source.label || this.source;
},
init(viewer) {
@@ -1639,7 +1641,7 @@ function startSelectedVesselTrail(record) {
state.trail = createTrail(state.viewer, { color: TRAIL_COLOR, width: 2.5 });
}
if (state.trail) state.trail.setPositions(state.trailPositions);
backfillVesselTrail(record.mmsi, state.trailBackfillToken);
backfillVesselTrail(record.mmsi, state.trailBackfillToken, record.reference);
}
/**
+25
View File
@@ -1641,3 +1641,28 @@ test('a vessel analyst record carries the MMSI the tracker keys on', () => {
assert.equal(nameless.id, '366999124');
assert.equal(nameless.mmsi, '366999124');
});
test('vessel selection passes the opaque source reference to optional history', async () => {
const { createAisStreamSource } = await import('../sources/live/standalone.js');
_setVesselStateForTest({ enabled: false });
const requests = [];
aisLiveVesselsLayer.setSource({
label: 'Test vessel source',
getSnapshot: async () => ({ records: [] }),
getTrack: async (reference, options) => { requests.push({ reference, options }); return { records: [] }; },
});
const harness = installWireHarness(undefined, { selectedRecord: null, trailMmsi: null });
try {
harness.record.reference = 'opaque:test-reference';
assert.equal(aisLiveVesselsLayer.selectById(harness.record.mmsi), true);
await new Promise(resolve => setImmediate(resolve));
assert.equal(requests.length, 1);
assert.equal(requests[0].reference, 'opaque:test-reference');
assert.ok(requests[0].options.signal instanceof AbortSignal);
assert.equal(aisLiveVesselsLayer.source, 'Test vessel source');
} finally {
harness.cleanup();
_setVesselStateForTest({ enabled: false });
aisLiveVesselsLayer.setSource(createAisStreamSource());
}
});
+24 -23
View File
@@ -21,7 +21,6 @@ import { createOpenSkySource } from '../sources/live/standalone.js';
* looking at (owner decision 2026-07-02).
*/
import * as Cesium from 'cesium';
let _source = createOpenSkySource();
import { aircraftIncludedInNearby } from './aircraftNearbyPolicy.js';
import { registerPickOwner, unregisterPickOwner, isOwnedByOtherLayer, resolvePickId } from './pickRegistry.js';
import {
@@ -97,6 +96,8 @@ import {
import { CONTACT_MATCH_TIER, contactMatchWins, rankContactMatch } from './contactMatch.js';
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
let _source = createOpenSkySource();
const FOCUS_EVIDENCE_DEV = import.meta.env?.DEV === true;
/** Amber tint for known-military aircraft rendered by this layer (matches the military layer's icon color). */
@@ -340,7 +341,7 @@ let _lastTrackingRefreshOutcome = {
epoch: 0,
status: 'unavailable',
ids: new Set(),
source: 'OpenSky Network',
source: _lastSource,
coverage: null,
};
/** @type {Cesium.Entity|null} Entity used for camera tracking */
@@ -412,7 +413,7 @@ function _contextSubjectMetadata(icao24) {
id: icao24,
layerId: 'flights',
layerName: 'Live Flights',
source: 'OpenSky Network',
source: _lastSource,
label: _contactLabel(icao24, _flightData.get(icao24)),
latitude: described.latitude,
longitude: described.longitude,
@@ -996,13 +997,6 @@ let _drReconcileValid = false;
/** @type {string|null} icao the reconciliation state currently belongs to */
let _drReconcileIcao = null;
/**
* Normalize a value to a trimmed lowercase string.
* @param {*} value - Any value (typically a header string or null).
* @returns {string} Lowercase trimmed string, or '' if falsy.
*/
/**
* Normalize a value to a trimmed string. A whitespace-only field (" ") is
* truthy, so every label chain must trim FIRST and then fall through.
@@ -2991,7 +2985,7 @@ function _startTrail(icao24) {
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
let path = null;
try {
const track = await _source.getTrack?.(icao24, { signal: AbortSignal.timeout(8000) });
const track = await _source.getTrack?.(_flightData.get(icao24)?.sourceReference ?? icao24, { signal: AbortSignal.timeout(8000) });
path = track?.records ?? null;
} catch {
return; // silent fallback to the accumulated trail
@@ -3822,26 +3816,28 @@ const flightsLayer = {
id: 'flights',
name: 'Live Flights',
icon: '✈️',
source: 'OpenSky Network',
source: _lastSource,
// Browser-harness seam: isolates synthetic display-floor scenarios without
// changing any production lifecycle or cache policy.
_clearDisplayFloorStateForTest,
/** @type {number} Polling interval (ms) between update() calls */
updateInterval: 30000,
/** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) {
if (_viewer) throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
_source = source;
_lastSource = source.label || _lastSource;
this.source = _lastSource;
},
/**
* Initialize the flights layer.
* Creates the BillboardCollection, resets all state, and installs the
* click-to-track handler on the scene canvas.
* @param {Cesium.Viewer} viewer - The CesiumJS viewer instance.
*/
/** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) {
if (_viewer) throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
_source = source;
},
init(viewer) {
clearFocusTarget('flights');
_focusEvidenceIds.clear();
@@ -3875,7 +3871,7 @@ const flightsLayer = {
_retryAt = 0;
_lastError = null;
_lastStatus = null;
_lastSource = 'OpenSky Network';
_lastSource = _source.label || 'OpenSky Network';
_lastCoverage = 'worldwide upstream snapshot';
_trackedIcao = null;
_resetTrackedSelectionState();
@@ -4029,7 +4025,7 @@ const flightsLayer = {
const usableStates = snapshot.records;
const sourceEpochMs = snapshot.observedAtMs;
const sourceAgeMs = snapshot.ageMs;
const sourceStale = snapshot.stale;
const sourceStale = snapshot.stale || snapshot.freshness === 'unknown';
_backoff = sourceStale;
_retryAt = 0;
_lastError = sourceStale
@@ -4219,6 +4215,8 @@ const flightsLayer = {
// Store flight metadata for click-to-track labels
const cat = stickyNumber(category, prevMeta?.category, null);
const meta = {
sourceReference: observation.reference,
observedReceiptMs: Date.now(),
callsign: stickyText(callsign, prevMeta?.callsign),
altitude: alt,
// geoAltitudeM/renderAltitudeM are ADDITIVE fields alongside the
@@ -4398,7 +4396,9 @@ const flightsLayer = {
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
// feed gap, and the full grace left phantom planes parked at airports.
for (const [icao24, bb] of _billboards) {
if (currentIcaos.has(icao24) || !snapshot.complete) continue;
if (currentIcaos.has(icao24)) continue;
// Partial admissions do not prove absence, but stale retention is bounded.
if (!snapshot.complete && Date.now() - (_flightData.get(icao24)?.observedReceiptMs ?? 0) < 300000) continue;
const misses = (_missingPolls.get(icao24) || 0) + 1;
const limit = _likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
if (misses < limit) {
@@ -4477,7 +4477,7 @@ const flightsLayer = {
_count = _billboards.size;
// Freshness belongs to the source snapshot, not the moment this browser
// received a cached 200 response.
_lastUpdate = sourceEpochMs ?? Date.now();
_lastUpdate = sourceEpochMs;
_lastTrackingRefreshOutcome = {
epoch: trackingRefreshEpoch,
status: 'accepted',
@@ -4496,6 +4496,7 @@ const flightsLayer = {
_backoff = true;
_retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
_lastStatus = e?.status ?? null;
if (e?.source) { _lastSource = e.source; this.source = _lastSource; }
_lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
} finally {
_activeUpdateControllers.delete(resourceController);
+17 -9
View File
@@ -1,6 +1,5 @@
import { createAdsbLolSource } from '../sources/live/standalone.js';
import * as Cesium from 'cesium';
let _source = createAdsbLolSource();
import { aircraftIncludedInNearby } from './aircraftNearbyPolicy.js';
import { registerPickOwner, unregisterPickOwner, isOwnedByOtherLayer, resolvePickId } from './pickRegistry.js';
import { registerSpriteCollection, restoreSpriteOrder } from './spriteOrder.js';
@@ -64,6 +63,8 @@ import {
import { CONTACT_MATCH_TIER, contactMatchWins, rankContactMatch } from './contactMatch.js';
import { holdContinuousRender, releaseContinuousRender } from '../renderGovernor.js';
let _source = createAdsbLolSource();
/**
* @module militaryFlights
* @description Real-time military flight tracking layer powered by the adsb.lol API.
@@ -2195,7 +2196,7 @@ function _startTrail(icao24) {
async function _backfillTrail(icao24, token, oldestFixEpochSec) {
let trace = null;
try {
const track = await _source.getTrack?.(icao24, { signal: AbortSignal.timeout(8000) });
const track = await _source.getTrack?.(_flightData.get(icao24)?.sourceReference ?? icao24, { signal: AbortSignal.timeout(8000) });
trace = track?.records ?? null;
} catch {
return; // silent fallback to the accumulated trail
@@ -2591,18 +2592,20 @@ const militaryFlightsLayer = {
/** @type {number} Polling interval in ms between API fetches */
updateInterval: 15000,
/**
* Initialize the layer: create the billboard collection, reset all state,
* and install the click-to-track handler.
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
*/
/** Configure the source before initialization; an active layer keeps its owner. */
setSource(source) {
if (_viewer) throw new Error('Configure the source before layer initialization');
if (typeof source?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
_source = source;
_lastSource = source.label || _lastSource;
this.source = _lastSource;
},
/**
* Initialize the layer: create the billboard collection, reset all state,
* and install the click-to-track handler.
* @param {Cesium.Viewer} viewer - The Cesium viewer instance
*/
init(viewer) {
clearFocusTarget('militaryFlights');
_viewer = viewer;
@@ -2782,7 +2785,7 @@ const militaryFlightsLayer = {
_lastSource = snapshot.source;
militaryFlightsLayer.source = _lastSource;
const usableAircraft = snapshot.records;
_backoff = snapshot.stale;
_backoff = snapshot.stale || snapshot.freshness === 'unknown';
_retryAt = 0;
_lastError = snapshot.freshness === 'unknown' ? 'Source snapshot time unavailable' : null;
const currentIcaos = new Set();
@@ -2900,6 +2903,8 @@ const militaryFlightsLayer = {
// last-known-good (bounded by the layer's eviction, which deletes the entry).
const stickyType = stickyText(type, prevMeta?.type);
const meta = {
sourceReference: aircraft.reference,
observedReceiptMs: Date.now(),
callsign: stickyText(callsign, prevMeta?.callsign),
type: stickyType,
// Type outranks category automatically inside classifyAircraft.
@@ -3094,7 +3099,9 @@ const militaryFlightsLayer = {
// LANDED_MISSING_POLL_LIMIT — their disappearance means "landed", not a
// feed gap, and the full grace left phantom planes parked at airports.
for (const [icao24, bb] of _billboards) {
if (currentIcaos.has(icao24) || !snapshot.complete) continue;
if (currentIcaos.has(icao24)) continue;
// Partial admissions do not prove absence, but stale retention is bounded.
if (!snapshot.complete && Date.now() - (_flightData.get(icao24)?.observedReceiptMs ?? 0) < 300000) continue;
const misses = (_missingPolls.get(icao24) || 0) + 1;
const limit = _likelyLanded(icao24) ? LANDED_MISSING_POLL_LIMIT : MISSING_POLL_LIMIT;
if (misses < limit) {
@@ -3155,6 +3162,7 @@ const militaryFlightsLayer = {
_backoff = true;
_retryAt = Date.now() + (e?.retryAfterMs ?? ERROR_BACKOFF_INTERVAL);
_lastStatus = e?.status ?? null;
if (e?.source) { _lastSource = e.source; this.source = _lastSource; }
_lastError = e?.name === 'LiveSourceError' ? e.message : 'Live data unavailable';
} finally {
_activeUpdateControllers.delete(resourceController);
+14
View File
@@ -247,3 +247,17 @@ test('AIS reports limited received coverage and keeps connection state separate
now,
);
});
test('a malformed vessel row cannot prevent admission of valid positions', async () => {
const source = createAisStreamSource({
fetchImpl: async () =>
response({
status: 'live',
rows: [null, { mmsi: '123456789', lat: 30, lon: -97 }],
}),
});
const snapshot = await source.getSnapshot();
assert.equal(snapshot.records.length, 1);
assert.equal(snapshot.rejectedCount, 1);
assert.equal(snapshot.complete, false);
});
+3
View File
@@ -54,6 +54,7 @@ export function createOpenSkySource({
now = () => Date.now(),
} = {}) {
return {
label: 'OpenSky Network',
async getSnapshot(query = {}, { signal } = {}) {
const params = new URLSearchParams();
if (Number.isFinite(query.latitude) && Number.isFinite(query.longitude)) {
@@ -111,6 +112,7 @@ export function createAdsbLolSource({
now = () => Date.now(),
} = {}) {
return {
label: 'adsb.lol',
async getSnapshot(_query = {}, { signal } = {}) {
const { response, payload } = await readResponse(
fetchImpl,
@@ -158,6 +160,7 @@ export function createAisStreamSource({
origin = () => globalThis.location?.origin || 'http://localhost',
} = {}) {
return {
label: 'AISStream',
async getSnapshot({ maxRows = 12000 } = {}, { signal } = {}) {
const url = new URL(apiUrl, origin());
url.searchParams.set('maxRows', String(maxRows));
+3 -2
View File
@@ -40,7 +40,8 @@ export function vesselSnapshot(
const records = [],
ids = new Set();
for (const row of rows) {
const record = normalizeVesselObservation(row, referenceFor(row));
const record = normalizeVesselObservation(row);
if (record) record.reference = referenceFor(row);
if (record && !ids.has(record.id)) {
records.push(record);
ids.add(record.id);
@@ -57,7 +58,7 @@ export function vesselSnapshot(
records,
source,
coverage,
complete: false,
complete: records.length === rows.length && !payload?.refreshing,
rejectedCount: rows.length - records.length,
observedAtMs,
freshness: payload?.refreshing