mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
216 lines
8.2 KiB
JavaScript
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';
|