Extract military-flight components and shared aircraft mechanics

This commit is contained in:
Sameh Khamis committed 2026-09-13 10:23:13 -07:00
1 parent b5e546daf1
commit cb790ddea1
25 files changed
+4849 -3930

No files matched your search

+143
View File
@@ -0,0 +1,143 @@
/** Instance-owned military identity and active-layer classification.
* Both aircraft layers share the same application-supplied registry. */
export function createMilitaryRegistry({ source, now = Date.now } = {}) {
const MIL_POLL_INTERVAL_MS = 60000;
/** @type {Set<string>} Lowercase ICAO24 hexes known to be military. */
const _milIcaos = new Set();
/** @type {boolean} True while the dedicated military layer is enabled. */
let _militaryLayerActive = false;
/** @type {Set<(active: boolean) => void>} Fired on active-state TRANSITIONS. */
const _activeChangeListeners = new Set();
/** @type {number} Epoch ms of the last registry refresh (any source). */
let _lastRefreshMs = 0;
/** @type {boolean} A self-poll fetch is in flight. */
let _polling = false;
let currentSource = null;
let lifetime = null;
let releaseSource = null;
/** Replace the classification source and cancel its outstanding work. */
function configureSource(nextSource, { signal } = {}) {
if (typeof nextSource?.getSnapshot !== 'function') throw new TypeError('A snapshot source is required');
releaseSource?.();
const controller = new AbortController();
lifetime = controller;
currentSource = nextSource;
_milIcaos.clear();
_lastRefreshMs = 0;
_polling = false;
const release = () => {
controller.abort();
signal?.removeEventListener('abort', release);
if (lifetime !== controller) return;
currentSource = null;
lifetime = null;
releaseSource = null;
_milIcaos.clear();
_lastRefreshMs = 0;
_polling = false;
};
releaseSource = release;
if (signal?.aborted) release();
else signal?.addEventListener('abort', release, { once: true });
return release;
}
/**
* True when the dedicated military layer currently renders these aircraft
* (the flights layer should suppress duplicates rather than restyle them).
* @returns {boolean}
*/
function isMilitaryLayerActive() {
return _militaryLayerActive;
}
/**
* Marks the dedicated military layer enabled/disabled. On a TRANSITION
* (value actually changed) the registered change listeners fire so the
* flights layer can reconcile its duplicates immediately instead of waiting
* out its 30 s poll (pre-ship audit M2).
* @param {boolean} active - Whether the military layer renders.
* @returns {void}
*/
function setMilitaryLayerActive(active) {
const next = !!active;
if (next === _militaryLayerActive) return;
_militaryLayerActive = next;
for (const listener of _activeChangeListeners) {
try {
listener(next);
} catch {
// a broken listener must never break the layer toggle
}
}
}
/**
* Subscribes to military-layer active-state transitions (fired only when the
* value changes, AFTER the new state is committed).
* @param {(active: boolean) => void} listener - Change callback.
* @returns {() => void} Unsubscribe function.
*/
function onMilitaryLayerActiveChange(listener) {
if (typeof listener !== 'function') return () => {};
_activeChangeListeners.add(listener);
return () => _activeChangeListeners.delete(listener);
}
/**
* Replaces/extends the known-military set from a fresh poll.
* Adds only — transient dropouts from one poll must not declassify an
* aircraft mid-session (the set stays small: a few hundred hexes).
* @param {Iterable<string>} icaos - ICAO24 hexes from a /v2/mil response.
* @returns {void}
*/
function registerMilitaryIcaos(icaos) {
for (const icao of icaos || []) {
const hex = String(icao || '').trim().toLowerCase();
if (hex) _milIcaos.add(hex);
}
_lastRefreshMs = now();
}
/**
* Whether an aircraft is known military.
* @param {string} icao24 - ICAO24 hex (any case).
* @returns {boolean}
*/
function isMilitaryIcao(icao24) {
return _milIcaos.has(String(icao24 || '').toLowerCase());
}
/** Refresh known identities while the dedicated layer is disabled.
* A failed or cancelled request retains the current set and never swaps sources. */
async function refreshMilitaryRegistryIfStale() {
if (!currentSource || !lifetime || lifetime.signal.aborted || _militaryLayerActive) return;
if (_polling || now() - _lastRefreshMs < MIL_POLL_INTERVAL_MS) return;
const owner = lifetime;
const requestSource = currentSource;
const signal = AbortSignal.any([owner.signal, AbortSignal.timeout(10000)]);
_polling = true;
try {
const snapshot = await requestSource.getSnapshot({}, { signal });
signal.throwIfAborted();
if (lifetime !== owner) return;
registerMilitaryIcaos(snapshot.records.map((record) => record.id));
} catch {
// Keep the existing classification through source outages.
} finally {
if (lifetime === owner) _polling = false;
}
}
function dispose() {
releaseSource?.();
_milIcaos.clear();
_lastRefreshMs = 0;
_activeChangeListeners.clear();
_militaryLayerActive = false;
}
if (source) configureSource(source);
return { configureSource, dispose, isMilitaryLayerActive, setMilitaryLayerActive,
onMilitaryLayerActiveChange, registerMilitaryIcaos, isMilitaryIcao, refreshMilitaryRegistryIfStale };
}
@@ -0,0 +1,93 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createMilitaryRegistry } from './classification.js';
test('classification instances have independent identities, listeners and active states', () => {
let requests = 0;
const source = { getSnapshot() { requests++; } };
const first = createMilitaryRegistry({ source });
const second = createMilitaryRegistry({ source });
let firstEvents = 0;
let secondEvents = 0;
first.onMilitaryLayerActiveChange(() => firstEvents++);
second.onMilitaryLayerActiveChange(() => secondEvents++);
first.registerMilitaryIcaos([' ABC123 ']);
first.setMilitaryLayerActive(true);
assert.equal(first.isMilitaryIcao('abc123'), true);
assert.equal(second.isMilitaryIcao('abc123'), false);
assert.equal(firstEvents, 1);
assert.equal(secondEvents, 0);
assert.equal(second.isMilitaryLayerActive(), false);
assert.equal(requests, 0);
first.dispose();
second.dispose();
});
test('classification uses normalized identities and respects its polling interval and active layer', async () => {
let time = 120000;
let calls = 0;
const registry = createMilitaryRegistry({ now: () => time, source: {
async getSnapshot() { calls++; return { records: [{ id: 'abc123' }] }; },
} });
await registry.refreshMilitaryRegistryIfStale();
await registry.refreshMilitaryRegistryIfStale();
assert.equal(calls, 1);
assert.equal(registry.isMilitaryIcao('abc123'), true);
time += 60000;
registry.setMilitaryLayerActive(true);
await registry.refreshMilitaryRegistryIfStale();
assert.equal(calls, 1);
registry.setMilitaryLayerActive(false);
await registry.refreshMilitaryRegistryIfStale();
assert.equal(calls, 2);
registry.dispose();
});
test('source replacement rejects an old response and disposal cancels the current request', async () => {
let resolveOld;
let resolveNew;
let oldSignal;
let newSignal;
const registry = createMilitaryRegistry({ source: {
getSnapshot(query, { signal }) { oldSignal = signal; return new Promise((resolve) => { resolveOld = resolve; }); },
} });
const old = registry.refreshMilitaryRegistryIfStale();
const application = new AbortController();
registry.configureSource({ getSnapshot(query, { signal }) {
newSignal = signal;
return new Promise((resolve) => { resolveNew = resolve; });
} }, { signal: application.signal });
const current = registry.refreshMilitaryRegistryIfStale();
resolveOld({ records: [{ id: 'abc123' }] });
await old;
application.abort();
resolveNew({ records: [{ id: 'def456' }] });
await current;
assert.equal(oldSignal.aborted, true);
assert.equal(newSignal.aborted, true);
assert.equal(registry.isMilitaryIcao('abc123'), false);
assert.equal(registry.isMilitaryIcao('def456'), false);
registry.dispose();
});
test('a failed classification source preserves known identities and makes no fallback request', async () => {
let time = 120000;
let calls = 0;
const registry = createMilitaryRegistry({ now: () => time, source: {
async getSnapshot() { calls++; throw new Error('fixture source unavailable'); },
} });
registry.registerMilitaryIcaos(['abc123']);
time += 60000;
await assert.doesNotReject(registry.refreshMilitaryRegistryIfStale());
assert.equal(calls, 1);
assert.equal(registry.isMilitaryIcao('abc123'), true);
registry.dispose();
assert.equal(registry.isMilitaryIcao('abc123'), false);
});
test('disposing a registry fed directly by its layer also clears classification', () => {
const registry = createMilitaryRegistry();
registry.registerMilitaryIcaos(['abc123']);
registry.dispose();
assert.equal(registry.isMilitaryIcao('abc123'), false);
});
+17
View File
@@ -0,0 +1,17 @@
/** Common aircraft calculations and explicitly constructed classification. */
export { createMilitaryRegistry } from './classification.js';
export * from '../../data/aircraftClass.js';
export * from '../../data/aircraftIcons.js';
export * from '../../data/aircraftMeta.js';
export * from '../../data/aircraftNearbyPolicy.js';
export * from '../../data/cockpitAirLod.js';
export * from '../../data/cockpitContactDot.js';
export * from '../../data/contactMatch.js';
export * from '../../data/iconOrientation.js';
export * from '../../data/layerState.js';
export * from '../../data/modelVisualAnchor.js';
export * from '../../data/motionModel.js';
export * from '../../data/renderAltitude.js';
export * from '../../data/routePlausible.js';
export * from '../../data/trackedModelRegime.js';
export * from '../../data/trackingClickGesture.js';