fix(flights): don't treat OpenSky's position-less rows as a partial snapshot

OpenSky's state-vector spec allows null longitude/latitude, and every live
/states/all response carries such rows (about 50 of 7,800). The normaliser
rejects them, so admitRecords reported complete: false on effectively every
poll. While a snapshot is incomplete, FlightRecords.absence() retains any
aircraft received in the last five minutes, which meant the stale fade,
MISSING_POLL_LIMIT and the landed fast-cull never ran: vanished and landed
aircraft stayed on the globe at full opacity for up to five minutes.

admitRecords now takes an optional unpositionedId(row). A rejected row it
names is a contact without a position: its id is returned in retainIds and
the row does not count against completeness. OpenSky opts in for rows with an
ICAO24 address and strictly null longitude and latitude; the id comes from
the same helper the normaliser uses. Rows with no usable id, out-of-range or
half-null coordinates, truncated rows and positioned duplicates still make
the snapshot incomplete, and a nonempty feed with no positioned rows is
still malformed.

absence() retains exactly the retainIds aircraft under the existing
five-minute bound, except likely-landed ones, which keep the landed limit.
Everything else ages out normally. Military readsb admission does not opt
in and is unchanged.
This commit is contained in:
Aditya committed 2026-10-13 17:18:28 +05:30
1 parent 8b8ccc9a81
commit b90bcf1829
8 files changed
+118 -9

No files matched your search

+17 -3
View File
@@ -53,17 +53,30 @@ export function coordinates(latitude, longitude) {
);
}
/** Admit a snapshot atomically; an all-invalid nonempty feed is unavailable. */
export function admitRecords(rows, normalize, label) {
/**
* Admit a snapshot atomically; an all-invalid nonempty feed is unavailable.
* `unpositionedId(row)` names a rejected row that is a valid contact without
* a position: it is returned in `retainIds` and does not make the snapshot
* incomplete.
*/
export function admitRecords(rows, normalize, label, unpositionedId) {
if (!Array.isArray(rows))
throw new LiveSourceError('malformed', `Malformed ${label} response`);
const records = [];
const ids = new Set();
const retainIds = new Set();
let unpositioned = 0;
for (const row of rows) {
const record = normalize(row);
if (record && !ids.has(record.id)) {
records.push(record);
ids.add(record.id);
} else if (!record) {
const id = unpositionedId?.(row);
if (id) {
retainIds.add(id);
unpositioned += 1;
}
}
}
if (rows.length && !records.length) {
@@ -71,7 +84,8 @@ export function admitRecords(rows, normalize, label) {
}
return {
records,
complete: records.length === rows.length,
retainIds,
complete: records.length + unpositioned === rows.length,
rejectedCount: rows.length - records.length,
};
}