mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Route the six proxied fetches that still buffered upstream bodies without a limit through the shared capped readers in src/sources/httpBody.js: FIRMS CSV, CelesTrak TLE groups, adsb.lol /v2/mil (both the OK and relayed-status paths), TomTom flow tiles, OpenSky states/all and adsbdb enrichment lookups. Each cap is a bound sized well above the measured payload, not a budget. readResponseBytesCapped gains the same optional abort signal as the text reader, with the same cancel-on-abort plumbing and reader release, so the TomTom tile path can share the route deadline instead of reading arrayBuffer() unbounded. Where a route already had a deadline, the same signal now covers the body read as well as the headers. Two polls had no deadline at all: the adsb.lol military poll gains a 12-second one and the OpenSky states/all exchange, including its Basic-auth retry, a 20-second one; both miss into their existing STALE paths. FIRMS map-key status, the OpenSky token exchange and CCTV media are left as they are. Co-Authored-By: Claude Opus 5 <[email protected]>
156 lines
5.4 KiB
JavaScript
156 lines
5.4 KiB
JavaScript
import path from 'node:path';
|
|
import { promises as fsp } from 'node:fs';
|
|
import { readResponseJsonCapped } from '../common/http.js';
|
|
|
|
/** Route and aircraft lookups are a few hundred bytes; 1 MiB is a hard bound. */
|
|
const ADSBDB_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
/**
|
|
* adsbdb.com enrichment proxy: callsign → route (airline + origin/destination
|
|
* airports) and hex → aircraft type/registration. Free community API — cached
|
|
* aggressively: ONE upstream request per new key ever (404s negative-cached),
|
|
* persisted to disk so restarts don't re-hammer it. Adapted from skylight
|
|
* (MIT) server/src/enrich/routes.ts.
|
|
*/
|
|
export function adsbdbProxy() {
|
|
const TTL_MS = 24 * 3600_000;
|
|
const CACHE_PATH = path.join(process.cwd(), '.gev-cache', 'adsbdb.json');
|
|
let cache = { routes: {}, aircraft: {} };
|
|
let dirty = false;
|
|
let loaded = false;
|
|
const inflight = new Map();
|
|
|
|
async function loadOnce() {
|
|
if (loaded) return;
|
|
loaded = true;
|
|
try {
|
|
const parsed = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8'));
|
|
cache = { routes: parsed.routes ?? {}, aircraft: parsed.aircraft ?? {} };
|
|
} catch {
|
|
/* first run */
|
|
}
|
|
setInterval(async () => {
|
|
if (!dirty) return;
|
|
dirty = false;
|
|
try {
|
|
await fsp.mkdir(path.dirname(CACHE_PATH), { recursive: true });
|
|
await fsp.writeFile(CACHE_PATH, JSON.stringify(cache), 'utf8');
|
|
} catch {
|
|
dirty = true;
|
|
} // retry next tick
|
|
}, 15_000).unref?.();
|
|
}
|
|
|
|
const fresh = (e) => e && Date.now() - e.at < TTL_MS;
|
|
|
|
function parseRoute(json) {
|
|
const fr = json?.response?.flightroute;
|
|
if (!fr?.origin || !fr?.destination) return null;
|
|
const airport = (a) => ({
|
|
code: a.iata_code || a.icao_code || '',
|
|
name: a.municipality || a.name || '',
|
|
lat: Number.isFinite(a.latitude) ? a.latitude : null,
|
|
lon: Number.isFinite(a.longitude) ? a.longitude : null,
|
|
});
|
|
return {
|
|
airline: fr.airline?.name || null,
|
|
origin: airport(fr.origin),
|
|
destination: airport(fr.destination),
|
|
};
|
|
}
|
|
|
|
function parseAircraft(json) {
|
|
const a = json?.response?.aircraft;
|
|
if (!a) return null;
|
|
return {
|
|
typeCode: a.icao_type || null, // ICAO designator, e.g. "B738" — feeds classifyAircraft
|
|
typeName:
|
|
a.manufacturer && a.type
|
|
? `${a.manufacturer} ${a.type}`
|
|
: a.type || null,
|
|
registration: a.registration || null,
|
|
};
|
|
}
|
|
|
|
function lookup(kind, key) {
|
|
const store = kind === 'route' ? cache.routes : cache.aircraft;
|
|
if (fresh(store[key])) return Promise.resolve(store[key].data);
|
|
const ik = `${kind}:${key}`;
|
|
if (!inflight.has(ik)) {
|
|
inflight.set(
|
|
ik,
|
|
(async () => {
|
|
try {
|
|
const url =
|
|
kind === 'route'
|
|
? `https://api.adsbdb.com/v0/callsign/${encodeURIComponent(key)}`
|
|
: `https://api.adsbdb.com/v0/aircraft/${encodeURIComponent(key)}`;
|
|
const signal = AbortSignal.timeout(8000);
|
|
const res = await fetch(url, { signal });
|
|
if (res.ok) {
|
|
const payload = await readResponseJsonCapped(
|
|
res,
|
|
ADSBDB_MAX_RESPONSE_BYTES,
|
|
signal,
|
|
);
|
|
const data =
|
|
kind === 'route' ? parseRoute(payload) : parseAircraft(payload);
|
|
store[key] = { at: Date.now(), data }; // data may be null — negative cache
|
|
dirty = true;
|
|
return data;
|
|
}
|
|
if (res.status === 404) {
|
|
store[key] = { at: Date.now(), data: null }; // known-missing — cache the miss
|
|
dirty = true;
|
|
}
|
|
// other statuses: leave uncached so we retry later
|
|
return fresh(store[key]) ? store[key].data : null;
|
|
} catch {
|
|
return fresh(store[key]) ? store[key].data : null; // network error → stale if any
|
|
} finally {
|
|
inflight.delete(ik);
|
|
}
|
|
})(),
|
|
);
|
|
}
|
|
return inflight.get(ik);
|
|
}
|
|
|
|
const installMiddleware = (server) => {
|
|
server.middlewares.use('/api/adsbdb', async (req, res) => {
|
|
await loadOnce();
|
|
const send = (status, obj) => {
|
|
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(obj));
|
|
};
|
|
try {
|
|
const [, kind, rawKey] = String(req.url || '')
|
|
.split('?')[0]
|
|
.split('/');
|
|
if (kind === 'route') {
|
|
const cs = String(rawKey || '').toUpperCase();
|
|
if (!/^[A-Z0-9]{2,8}$/.test(cs))
|
|
return send(400, { error: 'invalid callsign' });
|
|
const data = await lookup('route', cs);
|
|
return send(200, data ? { found: true, ...data } : { found: false });
|
|
}
|
|
if (kind === 'type') {
|
|
const hex = String(rawKey || '').toLowerCase();
|
|
if (!/^[0-9a-f]{6}$/.test(hex))
|
|
return send(400, { error: 'invalid hex' });
|
|
const data = await lookup('aircraft', hex);
|
|
return send(200, data ? { found: true, ...data } : { found: false });
|
|
}
|
|
return send(404, { error: 'unknown endpoint' });
|
|
} catch (err) {
|
|
console.error('[adsbdb-proxy] request failed');
|
|
return send(500, { error: 'adsbdb proxy error' });
|
|
}
|
|
});
|
|
};
|
|
return {
|
|
name: 'adsbdb-proxy',
|
|
configureServer: installMiddleware,
|
|
configurePreviewServer: installMiddleware,
|
|
};
|
|
}
|