Preserve positionless aircraft identities for classification

This commit is contained in:
Sameh Khamis committed 2026-09-13 10:28:10 -07:00
1 parent 1edebcb6f8
commit 22138212b9
8 files changed
+99 -4

No files matched your search

+9 -2
View File
@@ -129,10 +129,17 @@ export function createMilitaryRegistry({ source, now = Date.now } = {}) {
const signal = AbortSignal.any([owner.signal, AbortSignal.timeout(10000)]);
_polling = true;
try {
const snapshot = await requestSource.getSnapshot({}, { signal });
const ids =
typeof requestSource.getIdentities === 'function'
? await requestSource.getIdentities({}, { signal })
: (await requestSource.getSnapshot({}, { signal })).records.map(
(record) => record.id,
);
signal.throwIfAborted();
if (lifetime !== owner) return;
registerMilitaryIcaos(snapshot.records.map((record) => record.id));
if (!Array.isArray(ids))
throw new TypeError('Expected aircraft identities');
registerMilitaryIcaos(ids);
} catch {
// Keep the existing classification through source outages.
} finally {
@@ -121,3 +121,21 @@ test('disposing a registry fed directly by its layer also clears classification'
registry.dispose();
assert.equal(registry.isMilitaryIcao('abc123'), false);
});
test('classification prefers an explicit identity capability over positioned observations', async () => {
const registry = createMilitaryRegistry({
source: {
getSnapshot() {
assert.fail(
'position snapshots must not replace an available identity capability',
);
},
async getIdentities() {
return ['abc123'];
},
},
});
await registry.refreshMilitaryRegistryIfStale();
assert.equal(registry.isMilitaryIcao('abc123'), true);
registry.dispose();
});
+13
View File
@@ -165,3 +165,16 @@ export function normalizeAircraftTrack(
];
});
}
/** Known source identities are useful for classification even without positions. */
export function readsbIdentities(payload) {
const admitted = admitRecords(
payload?.ac,
(row) => {
const id = cleanText(row?.hex).toLowerCase();
return id ? { id } : null;
},
'aircraft identities',
);
return admitted.records.map((record) => record.id);
}
+42
View File
@@ -271,3 +271,45 @@ test('out-of-range source epochs do not reach Date or globe time constructors',
assert.equal(snapshot.records[0].positionTimeMs, null);
assert.equal(snapshot.records[0].contactTimeMs, null);
});
test('classification identities include positionless aircraft without admitting them to rendering', async () => {
const source = createAdsbLolSource({
fetchImpl: async () =>
response({
ac: [
{ hex: ' ABC123 ' },
{ hex: 'abc123' },
{ hex: 'def456', lat: 30, lon: -97 },
],
}),
});
assert.deepEqual(await source.getIdentities(), ['abc123', 'def456']);
const snapshot = await source.getSnapshot();
assert.deepEqual(
snapshot.records.map((record) => record.id),
['def456'],
);
assert.equal(snapshot.complete, false);
});
test('identity lookup validates its response and honors body-parse cancellation', async () => {
const malformed = createAdsbLolSource({
fetchImpl: async () => response({ ac: [{}] }),
});
await assert.rejects(malformed.getIdentities(), /Malformed/);
const abort = new AbortController();
const source = createAdsbLolSource({
fetchImpl: async () => ({
ok: true,
status: 200,
headers: new Headers(),
async json() {
abort.abort();
return { ac: [{ hex: 'abc123' }] };
},
}),
});
await assert.rejects(source.getIdentities({}, { signal: abort.signal }), {
name: 'AbortError',
});
});
+1
View File
@@ -5,6 +5,7 @@ export {
normalizeAircraftTrack,
openSkySnapshot,
readsbSnapshot,
readsbIdentities,
} from './aircraft.js';
export {
normalizeVesselObservation,
+11
View File
@@ -9,6 +9,7 @@ import {
normalizeAircraftTrack,
openSkySnapshot,
readsbSnapshot,
readsbIdentities,
} from './aircraft.js';
import { normalizeVesselTrack, vesselSnapshot } from './vessels.js';
@@ -113,6 +114,16 @@ export function createAdsbLolSource({
} = {}) {
return {
label: 'adsb.lol',
async getIdentities(_query = {}, { signal } = {}) {
const { response, payload } = await readResponse(
fetchImpl,
'/api/adsblol/mil',
{ signal },
'adsb.lol',
);
if (!response.ok) throw httpError(response, 'adsb.lol');
return readsbIdentities(payload);
},
async getSnapshot(_query = {}, { signal } = {}) {
const { response, payload } = await readResponse(
fetchImpl,