Files
gods-eye-view/server/providers/military-installations.js

216 lines
8.2 KiB
JavaScript

import { makeRateLimiter, clientKey } from './common/rate-limit.js';
import {
MILITARY_INSTALLATION_ELEMENT_CAP,
MILITARY_INSTALLATION_MAX_RESPONSE_BYTES,
MILITARY_INSTALLATION_DISK_TTL_MS,
MILITARY_INSTALLATION_STALE_MS,
} from './military-installations/constants.js';
import { fetchOverpassPayload } from './overpass/transport.js';
import {
_militaryInstallationCache,
trimMilitaryInstallationCache,
writeMilitaryInstallationDisk,
resolveMilitaryInstallationTier,
readMilitaryInstallationDisk,
} from './military-installations/cache.js';
import {
validMilitaryInstallationBox,
quantizeMilitaryInstallationBox,
militaryInstallationCacheKey,
militaryInstallationFailureReason,
} from './military-installations/query.js';
import { coalesceProxyRequest } from './common/http.js';
const _militaryInstallationsRateLimiter = makeRateLimiter({
windowMs: 60_000,
max: 90,
globalMax: 300,
});
const _militaryInstallationInFlight = new Map();
function militaryInstallationsProxy() {
async function refresh(box, key) {
const bbox = `${box.south},${box.west},${box.north},${box.east}`;
const ql = `[out:json][timeout:20];(nwr["military"~"^(airfield|naval_base|range|barracks|base)$"](${bbox});nwr["landuse"="military"](${bbox}););out center tags geom ${MILITARY_INSTALLATION_ELEMENT_CAP};`;
const upstream = await fetchOverpassPayload(
`data=${encodeURIComponent(ql)}`,
MILITARY_INSTALLATION_MAX_RESPONSE_BYTES,
);
if (
upstream.status >= 400 ||
upstream.rateLimited ||
upstream.runtimeError
) {
throw Object.assign(
new Error('Mapped installation upstream unavailable'),
{
installationReason: upstream.rateLimited
? 'rate_limited'
: upstream.status === 504
? 'timeout'
: upstream.runtimeError
? 'query_failed'
: 'unavailable',
},
);
}
const parsed = JSON.parse(upstream.body);
const elements = Array.isArray(parsed?.elements)
? parsed.elements.slice(0, MILITARY_INSTALLATION_ELEMENT_CAP)
: [];
const payload = {
elements,
// Honest truncation flag — the client re-asks for its exact viewport so
// off-view features can never starve in-view ones. The cap travels with
// the payload so the client never has to hard-code it.
saturated: elements.length >= MILITARY_INSTALLATION_ELEMENT_CAP,
elementCap: MILITARY_INSTALLATION_ELEMENT_CAP,
retrievedAt: new Date().toISOString(),
status: 'ready',
};
const entry = { payload, cachedAt: Date.now() };
_militaryInstallationCache.set(key, entry);
trimMilitaryInstallationCache();
writeMilitaryInstallationDisk(key, entry);
return payload;
}
function install(middlewares) {
middlewares.use('/api/military-installations', async (req, res) => {
if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method Not Allowed' }));
return;
}
if (!_militaryInstallationsRateLimiter(clientKey(req))) {
res.writeHead(429, {
'Content-Type': 'application/json',
'Retry-After': '5',
});
res.end(JSON.stringify({ error: 'Rate limit exceeded' }));
return;
}
const url = new URL(req.url, 'http://localhost');
const requested = validMilitaryInstallationBox(url.searchParams);
if (!requested) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(
JSON.stringify({
error: 'A non-dateline bbox no larger than 10 degrees is required',
}),
);
return;
}
// Query the SNAPPED box, not the raw viewport: neighbouring views then
// share one cache entry, and an outward snap always covers what was asked.
// `exact=1` opts out — the client sends it after a SATURATED snapped
// response, so a truncated tile can never starve the actual viewport. It
// is keyed separately so exact and snapped answers never collide.
const exact = url.searchParams.get('exact') === '1';
const box = exact
? requested
: quantizeMilitaryInstallationBox(requested);
// Key at the precision the query actually uses (see militaryInstallationCacheKey).
const key = exact
? `exact:${militaryInstallationCacheKey(box, 5)}`
: militaryInstallationCacheKey(box);
const now = Date.now();
const cached = _militaryInstallationCache.get(key);
const preflight = await resolveMilitaryInstallationTier({
cacheKey: key,
memoryCache: _militaryInstallationCache,
inFlight: _militaryInstallationInFlight,
readDisk: () =>
readMilitaryInstallationDisk(key, MILITARY_INSTALLATION_DISK_TTL_MS),
now,
});
if (preflight.source !== 'UPSTREAM') {
if (preflight.source === 'DISK') {
_militaryInstallationCache.set(key, preflight.entry);
trimMilitaryInstallationCache();
}
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=60',
'X-Military-Installations': preflight.source,
});
res.end(
JSON.stringify({ ...preflight.entry.payload, status: 'cached' }),
);
return;
}
const request = coalesceProxyRequest(
_militaryInstallationInFlight,
key,
() => refresh(box, key),
);
try {
const payload = await request.promise;
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=60',
'X-Military-Installations': request.shared ? 'INFLIGHT' : 'MISS',
});
res.end(JSON.stringify(payload));
} catch (error) {
if (cached && now - cached.cachedAt <= MILITARY_INSTALLATION_STALE_MS) {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'X-Military-Installations': 'STALE',
});
res.end(JSON.stringify({ ...cached.payload, status: 'stale' }));
return;
}
// Overpass is down: last-good mapped context at ANY age beats an empty
// layer (the same serve-stale rule the Overpass proxy applies).
const stale = await readMilitaryInstallationDisk(key, Infinity);
if (stale) {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
'X-Military-Installations': 'STALE-DISK',
});
res.end(JSON.stringify({ ...stale.payload, status: 'stale' }));
return;
}
res.writeHead(503, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({
error: 'Mapped installation context is temporarily unavailable',
reason: militaryInstallationFailureReason(error),
}),
);
}
});
}
return {
name: 'military-installations-proxy',
configureServer(server) {
install(server.middlewares);
},
configurePreviewServer(server) {
install(server.middlewares);
},
};
}
export { militaryInstallationsProxy };
export { MILITARY_INSTALLATION_ELEMENT_CAP } from './military-installations/constants.js';
export { quantizeMilitaryInstallationBox } from './military-installations/query.js';
export { militaryInstallationCacheKey } from './military-installations/query.js';
export { resolveMilitaryInstallationTier } from './military-installations/cache.js';
export { migrateMilitaryInstallationEntry } from './military-installations/cache.js';
export { militaryInstallationDiskFresh } from './military-installations/cache.js';
export { militaryInstallationDiskPath } from './military-installations/cache.js';
export { readMilitaryInstallationDisk } from './military-installations/cache.js';
export { writeMilitaryInstallationDisk } from './military-installations/cache.js';
export { validMilitaryInstallationBox } from './military-installations/query.js';
export { militaryInstallationFailureReason } from './military-installations/query.js';