style: format terrain traffic fire and bike-share modules

This commit is contained in:
Sameh Khamis committed 2026-09-11 21:57:51 -07:00
1 parent f4054469d5
commit a2d6461b52
13 files changed
+638 -213

No files matched your search

+13 -1
View File
@@ -57,5 +57,17 @@
"server/providers/space/celestrak.js", "server/providers/space/celestrak.js",
"server/providers/space/launch-library.js", "server/providers/space/launch-library.js",
"src/data/spaceProviderRequests.js", "src/data/spaceProviderRequests.js",
"src/tooling/spaceProviders.test.mjs" "src/tooling/spaceProviders.test.mjs",
"server/providers/terrain.js",
"server/providers/traffic.js",
"server/providers/firms.js",
"server/providers/gbfs.js",
"src/data/gbfsSource.js",
"src/data/terrainHeightsProxy.js",
"src/data/terrainHeightsProxy.test.mjs",
"src/data/tomtomTiles.js",
"src/data/tomtomTiles.test.mjs",
"src/data/firmsCsv.js",
"src/data/firmsCsv.test.mjs",
"src/tooling/environmentProviders.test.mjs"
] ]
+43 -11
View File
@@ -49,10 +49,16 @@ export function firmsProxy() {
diskChecked = true; diskChecked = true;
try { try {
const parsed = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8')); const parsed = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8'));
if (Number.isFinite(parsed?.at) && Array.isArray(parsed?.sources) && Array.isArray(parsed?.fires)) { if (
Number.isFinite(parsed?.at) &&
Array.isArray(parsed?.sources) &&
Array.isArray(parsed?.fires)
) {
mem = parsed; mem = parsed;
} }
} catch { /* no disk cache yet */ } } catch {
/* no disk cache yet */
}
} }
async function writeDisk(entry) { async function writeDisk(entry) {
@@ -97,7 +103,10 @@ export function firmsProxy() {
for (const record of records) fires.push(record); for (const record of records) fires.push(record);
sources.push({ source, count: records.length, ok: true }); sources.push({ source, count: records.length, ok: true });
} catch (err) { } catch (err) {
console.warn(`[firms-proxy] ${source} fetch failed:`, err?.message || err); console.warn(
`[firms-proxy] ${source} fetch failed:`,
err?.message || err,
);
sources.push({ source, count: 0, ok: false }); sources.push({ source, count: 0, ok: false });
} }
} }
@@ -136,9 +145,14 @@ export function firmsProxy() {
const body = await res.json(); const body = await res.json();
const used = Number(body?.current_transactions); const used = Number(body?.current_transactions);
const limit = Number(body?.transaction_limit); const limit = Number(body?.transaction_limit);
return Number.isFinite(used) && Number.isFinite(limit) ? { used, limit } : null; return Number.isFinite(used) && Number.isFinite(limit)
? { used, limit }
: null;
} catch (err) { } catch (err) {
console.warn('[firms-proxy] mapkey status failed:', err?.message || err); console.warn(
'[firms-proxy] mapkey status failed:',
err?.message || err,
);
return null; return null;
} }
})() })()
@@ -146,7 +160,9 @@ export function firmsProxy() {
statusCache = { at: Date.now(), transactions }; statusCache = { at: Date.now(), transactions };
return transactions; return transactions;
}) })
.finally(() => { statusInflight = null; }); .finally(() => {
statusInflight = null;
});
} }
return statusInflight; return statusInflight;
} }
@@ -157,7 +173,10 @@ export function firmsProxy() {
server.middlewares.use('/api/firms', async (req, res) => { server.middlewares.use('/api/firms', async (req, res) => {
const sendJson = (status, obj) => { const sendJson = (status, obj) => {
if (res.headersSent) return; if (res.headersSent) return;
res.writeHead(status, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(status, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify(obj)); res.end(JSON.stringify(obj));
}; };
try { try {
@@ -167,7 +186,14 @@ export function firmsProxy() {
if (subPath === '/status') { if (subPath === '/status') {
if (!key) { if (!key) {
sendJson(200, { hasKey: false, lastFetch: null, count: null, stale: false, ttlMs: TTL_MS, transactions: null }); sendJson(200, {
hasKey: false,
lastFetch: null,
count: null,
stale: false,
ttlMs: TTL_MS,
transactions: null,
});
return; return;
} }
const transactions = await getTransactions(key); const transactions = await getTransactions(key);
@@ -203,10 +229,14 @@ export function firmsProxy() {
return fresh; return fresh;
}) })
.catch((err) => { .catch((err) => {
console.warn(`[firms-proxy] refresh failed (${err?.message || err}) — serving cache if any`); console.warn(
`[firms-proxy] refresh failed (${err?.message || err}) — serving cache if any`,
);
return null; return null;
}) })
.finally(() => { inflight = null; }); .finally(() => {
inflight = null;
});
} }
const pending = inflight; const pending = inflight;
const fresh = await pending; const fresh = await pending;
@@ -215,7 +245,9 @@ export function firmsProxy() {
} else if (entry) { } else if (entry) {
sendJson(200, buildPayload(entry, true)); // upstream down — stale beats empty sendJson(200, buildPayload(entry, true)); // upstream down — stale beats empty
} else { } else {
sendJson(502, { error: 'firms fetch failed and no cache available' }); sendJson(502, {
error: 'firms fetch failed and no cache available',
});
} }
} catch (err) { } catch (err) {
console.warn('[firms-proxy] error:', err?.message || err); console.warn('[firms-proxy] error:', err?.message || err);
+74 -19
View File
@@ -1,4 +1,8 @@
import { isAllowedGbfsHost, isAllowedGbfsPath, gbfsCacheControl } from '../../src/data/gbfsSource.js'; import {
isAllowedGbfsHost,
isAllowedGbfsPath,
gbfsCacheControl,
} from '../../src/data/gbfsSource.js';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// GBFS (General Bikeshare Feed Specification) proxy constants // GBFS (General Bikeshare Feed Specification) proxy constants
@@ -23,7 +27,10 @@ export function gbfsProxy() {
server.middlewares.use('/api/gbfs', async (req, res) => { server.middlewares.use('/api/gbfs', async (req, res) => {
try { try {
if (req.method !== 'GET') { if (req.method !== 'GET') {
res.writeHead(405, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(405, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify({ error: 'Method Not Allowed' })); res.end(JSON.stringify({ error: 'Method Not Allowed' }));
return; return;
} }
@@ -31,7 +38,10 @@ export function gbfsProxy() {
const url = new URL(req.url || '/', 'http://localhost'); const url = new URL(req.url || '/', 'http://localhost');
const encodedTarget = url.pathname.replace(/^\/+/, ''); const encodedTarget = url.pathname.replace(/^\/+/, '');
if (!encodedTarget) { if (!encodedTarget) {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(400, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify({ error: 'Missing GBFS upstream target' })); res.end(JSON.stringify({ error: 'Missing GBFS upstream target' }));
return; return;
} }
@@ -40,7 +50,10 @@ export function gbfsProxy() {
try { try {
decodedTarget = decodeURIComponent(encodedTarget); decodedTarget = decodeURIComponent(encodedTarget);
} catch { } catch {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(400, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify({ error: 'Invalid GBFS target encoding' })); res.end(JSON.stringify({ error: 'Invalid GBFS target encoding' }));
return; return;
} }
@@ -49,31 +62,53 @@ export function gbfsProxy() {
try { try {
upstreamUrl = new URL(decodedTarget); upstreamUrl = new URL(decodedTarget);
} catch { } catch {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(400, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify({ error: 'Invalid GBFS upstream URL' })); res.end(JSON.stringify({ error: 'Invalid GBFS upstream URL' }));
return; return;
} }
if (upstreamUrl.protocol !== 'https:') { if (upstreamUrl.protocol !== 'https:') {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(400, {
res.end(JSON.stringify({ error: 'Only https GBFS targets are allowed' })); 'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({ error: 'Only https GBFS targets are allowed' }),
);
return; return;
} }
if (!isAllowedGbfsHost(upstreamUrl.hostname)) { if (!isAllowedGbfsHost(upstreamUrl.hostname)) {
res.writeHead(403, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(403, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify({ error: 'GBFS host not allowed' })); res.end(JSON.stringify({ error: 'GBFS host not allowed' }));
return; return;
} }
if (!isAllowedGbfsPath(upstreamUrl.pathname)) { if (!isAllowedGbfsPath(upstreamUrl.pathname)) {
res.writeHead(400, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(400, {
res.end(JSON.stringify({ error: 'Only station_information/station_status endpoints are allowed' })); 'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({
error:
'Only station_information/station_status endpoints are allowed',
}),
);
return; return;
} }
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), GBFS_PROXY_TIMEOUT_MS); const timeoutId = setTimeout(
() => controller.abort(),
GBFS_PROXY_TIMEOUT_MS,
);
let upstream; let upstream;
try { try {
upstream = await fetch(upstreamUrl.toString(), { upstream = await fetch(upstreamUrl.toString(), {
@@ -91,18 +126,32 @@ export function gbfsProxy() {
// Limit response size to prevent memory exhaustion from malicious upstream // Limit response size to prevent memory exhaustion from malicious upstream
const GBFS_MAX_BODY_BYTES = 5 * 1024 * 1024; // 5 MB const GBFS_MAX_BODY_BYTES = 5 * 1024 * 1024; // 5 MB
const contentLength = Number(upstream.headers.get('content-length')); const contentLength = Number(upstream.headers.get('content-length'));
if (Number.isFinite(contentLength) && contentLength > GBFS_MAX_BODY_BYTES) { if (
res.writeHead(502, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); Number.isFinite(contentLength) &&
res.end(JSON.stringify({ error: 'GBFS upstream response too large' })); contentLength > GBFS_MAX_BODY_BYTES
) {
res.writeHead(502, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({ error: 'GBFS upstream response too large' }),
);
return; return;
} }
const body = await upstream.text(); const body = await upstream.text();
if (Buffer.byteLength(body, 'utf8') > GBFS_MAX_BODY_BYTES) { if (Buffer.byteLength(body, 'utf8') > GBFS_MAX_BODY_BYTES) {
res.writeHead(502, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(502, {
res.end(JSON.stringify({ error: 'GBFS upstream response too large' })); 'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(
JSON.stringify({ error: 'GBFS upstream response too large' }),
);
return; return;
} }
const contentType = upstream.headers.get('content-type') || 'application/json'; const contentType =
upstream.headers.get('content-type') || 'application/json';
res.writeHead(upstream.status, { res.writeHead(upstream.status, {
'Content-Type': contentType, 'Content-Type': contentType,
'Cache-Control': gbfsCacheControl(upstreamUrl.pathname), 'Cache-Control': gbfsCacheControl(upstreamUrl.pathname),
@@ -112,12 +161,18 @@ export function gbfsProxy() {
res.end(body); res.end(body);
} catch (error) { } catch (error) {
if (error?.name === 'AbortError') { if (error?.name === 'AbortError') {
res.writeHead(504, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(504, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify({ error: 'GBFS upstream timeout' })); res.end(JSON.stringify({ error: 'GBFS upstream timeout' }));
return; return;
} }
console.error('[GBFS Proxy]', error?.message || String(error)); console.error('[GBFS Proxy]', error?.message || String(error));
res.writeHead(502, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' }); res.writeHead(502, {
'Content-Type': 'application/json',
'Cache-Control': 'no-store',
});
res.end(JSON.stringify({ error: 'GBFS proxy error' })); res.end(JSON.stringify({ error: 'GBFS proxy error' }));
} }
}); });
+37 -16
View File
@@ -39,12 +39,19 @@ export function terrainHeightsProxy() {
diskLoaded = true; diskLoaded = true;
try { try {
const parsed = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8')); const parsed = JSON.parse(await fsp.readFile(CACHE_PATH, 'utf8'));
const pointEntries = parsed?.version === 2 && parsed.points && typeof parsed.points === 'object' const pointEntries =
? parsed.points parsed?.version === 2 &&
: null; parsed.points &&
typeof parsed.points === 'object'
? parsed.points
: null;
if (pointEntries) { if (pointEntries) {
for (const [key, entry] of Object.entries(pointEntries)) { for (const [key, entry] of Object.entries(pointEntries)) {
if (entry && Number.isFinite(entry.at) && validTerrainResult(entry.result)) { if (
entry &&
Number.isFinite(entry.at) &&
validTerrainResult(entry.result)
) {
mem.set(key, entry); mem.set(key, entry);
} }
} }
@@ -53,18 +60,27 @@ export function terrainHeightsProxy() {
// positionally present results; an absent value never becomes height 0. // positionally present results; an absent value never becomes height 0.
for (const [rawPoints, entry] of Object.entries(parsed)) { for (const [rawPoints, entry] of Object.entries(parsed)) {
const points = parseTerrainPoints(rawPoints); const points = parseTerrainPoints(rawPoints);
if (!points || !entry || !Number.isFinite(entry.at) || !Array.isArray(entry.results)) continue; if (
!points ||
!entry ||
!Number.isFinite(entry.at) ||
!Array.isArray(entry.results)
)
continue;
for (let i = 0; i < points.length; i += 1) { for (let i = 0; i < points.length; i += 1) {
const result = entry.results[i]; const result = entry.results[i];
if (!validTerrainResult(result)) continue; if (!validTerrainResult(result)) continue;
const key = terrainPointKey(points[i]); const key = terrainPointKey(points[i]);
const existing = mem.get(key); const existing = mem.get(key);
if (!existing || entry.at > existing.at) mem.set(key, { at: entry.at, result }); if (!existing || entry.at > existing.at)
mem.set(key, { at: entry.at, result });
} }
} }
diskDirty = mem.size > 0; diskDirty = mem.size > 0;
} }
} catch { /* no disk cache yet */ } } catch {
/* no disk cache yet */
}
// Periodic flush, same shape as adsbdbProxy: coalesce writes instead of // Periodic flush, same shape as adsbdbProxy: coalesce writes instead of
// hitting disk on every request. // hitting disk on every request.
setInterval(async () => { setInterval(async () => {
@@ -97,7 +113,8 @@ export function terrainHeightsProxy() {
const chunkResults = await fetchTerrainChunkWithRetry(chunk); const chunkResults = await fetchTerrainChunkWithRetry(chunk);
// Keep later chunks aligned even if a malformed upstream response omits // Keep later chunks aligned even if a malformed upstream response omits
// trailing positions. The resolver will reject each null individually. // trailing positions. The resolver will reject each null individually.
for (let j = 0; j < chunk.length; j += 1) results.push(chunkResults[j] ?? null); for (let j = 0; j < chunk.length; j += 1)
results.push(chunkResults[j] ?? null);
} }
return results; return results;
} }
@@ -106,10 +123,9 @@ export function terrainHeightsProxy() {
function fetchMissingSingleFlight(points) { function fetchMissingSingleFlight(points) {
const key = points.map(terrainPointKey).join(';'); const key = points.map(terrainPointKey).join(';');
if (!inflight.has(key)) { if (!inflight.has(key)) {
const request = fetchUpstreamAll(points) const request = fetchUpstreamAll(points).finally(() => {
.finally(() => { if (inflight.get(key) === request) inflight.delete(key);
if (inflight.get(key) === request) inflight.delete(key); });
});
inflight.set(key, request); inflight.set(key, request);
} }
return inflight.get(key); return inflight.get(key);
@@ -130,11 +146,16 @@ export function terrainHeightsProxy() {
const rawPoints = parsedUrl.searchParams.get('points'); const rawPoints = parsedUrl.searchParams.get('points');
const points = parseTerrainPoints(rawPoints); const points = parseTerrainPoints(rawPoints);
if (!points) { if (!points) {
send(400, { error: 'invalid points parameter — expected "lon,lat;lon,lat;…" with finite numbers' }); send(400, {
error:
'invalid points parameter — expected "lon,lat;lon,lat;…" with finite numbers',
});
return; return;
} }
if (points.length > MAX_POINTS) { if (points.length > MAX_POINTS) {
send(500, { error: `too many points (${points.length}); max ${MAX_POINTS} per request` }); send(500, {
error: `too many points (${points.length}); max ${MAX_POINTS} per request`,
});
return; return;
} }
@@ -147,8 +168,8 @@ export function terrainHeightsProxy() {
if (outcome.cacheChanged) diskDirty = true; if (outcome.cacheChanged) diskDirty = true;
if (outcome.upstreamError) { if (outcome.upstreamError) {
console.warn( console.warn(
'[terrain-heights-proxy] refresh incomplete' '[terrain-heights-proxy] refresh incomplete' +
+ ' — serving stale points when available' ' — serving stale points when available',
); );
} }
send(outcome.status, outcome.body); send(outcome.status, outcome.body);
+46 -21
View File
@@ -60,10 +60,16 @@ export function tomtomProxy() {
budgetLoaded = true; budgetLoaded = true;
try { try {
const parsed = JSON.parse(await fsp.readFile(BUDGET_PATH, 'utf8')); const parsed = JSON.parse(await fsp.readFile(BUDGET_PATH, 'utf8'));
if (parsed && typeof parsed.date === 'string' && Number.isFinite(parsed.count)) { if (
parsed &&
typeof parsed.date === 'string' &&
Number.isFinite(parsed.count)
) {
budget = parsed; budget = parsed;
} }
} catch { /* no budget file yet */ } } catch {
/* no budget file yet */
}
} }
async function persistBudget() { async function persistBudget() {
@@ -87,7 +93,8 @@ export function tomtomProxy() {
void persistBudget(); void persistBudget();
} }
const tilePath = (key) => path.join(CACHE_DIR, `flow-${key.replaceAll('/', '-')}.pbf`); const tilePath = (key) =>
path.join(CACHE_DIR, `flow-${key.replaceAll('/', '-')}.pbf`);
/** Disk-cache read; tile age comes from the file's mtime. */ /** Disk-cache read; tile age comes from the file's mtime. */
async function readDiskTile(key) { async function readDiskTile(key) {
@@ -97,7 +104,9 @@ export function tomtomProxy() {
fsp.readFile(tilePath(key)), fsp.readFile(tilePath(key)),
]); ]);
return { at: stat.mtimeMs, buf }; return { at: stat.mtimeMs, buf };
} catch { return null; } } catch {
return null;
}
} }
async function writeDiskTile(key, buf) { async function writeDiskTile(key, buf) {
@@ -105,7 +114,10 @@ export function tomtomProxy() {
await fsp.mkdir(CACHE_DIR, { recursive: true }); await fsp.mkdir(CACHE_DIR, { recursive: true });
await fsp.writeFile(tilePath(key), buf); await fsp.writeFile(tilePath(key), buf);
} catch (err) { } catch (err) {
console.warn(`[tomtom-proxy] tile cache write failed for ${key}:`, err?.message || err); console.warn(
`[tomtom-proxy] tile cache write failed for ${key}:`,
err?.message || err,
);
} }
} }
@@ -119,10 +131,13 @@ export function tomtomProxy() {
} }
async function fetchUpstream(z, x, y) { async function fetchUpstream(z, x, y) {
const url = 'https://api.tomtom.com/traffic/map/4/tile/flow/relative/' const url =
+ `${z}/${x}/${y}.pbf?key=${encodeURIComponent(process.env.TOMTOM_API_KEY)}`; 'https://api.tomtom.com/traffic/map/4/tile/flow/relative/' +
`${z}/${x}/${y}.pbf?key=${encodeURIComponent(process.env.TOMTOM_API_KEY)}`;
recordUpstreamFetch(); // attempts count — upstream bills the request either way recordUpstreamFetch(); // attempts count — upstream bills the request either way
const res = await fetch(url, { signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS) }); const res = await fetch(url, {
signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const buf = Buffer.from(await res.arrayBuffer()); const buf = Buffer.from(await res.arrayBuffer());
if (buf.length === 0) throw new Error('empty tile body'); if (buf.length === 0) throw new Error('empty tile body');
@@ -161,7 +176,12 @@ export function tomtomProxy() {
if (urlPath === '/status') { if (urlPath === '/status') {
const hasKey = Boolean(process.env.TOMTOM_API_KEY); const hasKey = Boolean(process.env.TOMTOM_API_KEY);
const b = currentBudget(); const b = currentBudget();
sendJson(200, { hasKey, dailyCount: b.count, budget: dailyBudgetLimit(), date: b.date }); sendJson(200, {
hasKey,
dailyCount: b.count,
budget: dailyBudgetLimit(),
date: b.date,
});
return; return;
} }
@@ -208,18 +228,23 @@ export function tomtomProxy() {
// Stale or missing → refresh, single-flight per tile. // Stale or missing → refresh, single-flight per tile.
if (!inflight.has(key)) { if (!inflight.has(key)) {
inflight.set(key, fetchUpstream(z, x, y) inflight.set(
.then(async (buf) => { key,
const fresh = { at: Date.now(), buf }; fetchUpstream(z, x, y)
memSet(key, fresh); .then(async (buf) => {
await writeDiskTile(key, buf); const fresh = { at: Date.now(), buf };
return fresh; memSet(key, fresh);
}) await writeDiskTile(key, buf);
.catch((err) => { return fresh;
console.warn(`[tomtom-proxy] ${key} fetch failed (${err?.message || err}) — serving stale if any`); })
return null; .catch((err) => {
}) console.warn(
.finally(() => inflight.delete(key))); `[tomtom-proxy] ${key} fetch failed (${err?.message || err}) — serving stale if any`,
);
return null;
})
.finally(() => inflight.delete(key)),
);
} }
const fresh = await inflight.get(key); const fresh = await inflight.get(key);
if (fresh) { if (fresh) {
+30 -7
View File
@@ -17,7 +17,14 @@
*/ */
/** Header fields that must all be present for a payload to count as FIRMS CSV. */ /** Header fields that must all be present for a payload to count as FIRMS CSV. */
const REQUIRED_HEADER_FIELDS = ['latitude', 'longitude', 'acq_date', 'acq_time', 'confidence', 'frp']; const REQUIRED_HEADER_FIELDS = [
'latitude',
'longitude',
'acq_date',
'acq_time',
'confidence',
'frp',
];
const HOUR_MS = 3600_000; const HOUR_MS = 3600_000;
/** Trailing window size for {@link filterTrailing24h}. */ /** Trailing window size for {@link filterTrailing24h}. */
@@ -36,8 +43,10 @@ export function isLikelyCsv(text) {
if (typeof text !== 'string') return false; if (typeof text !== 'string') return false;
const trimmed = text.trimStart(); const trimmed = text.trimStart();
if (!trimmed || trimmed[0] === '<') return false; if (!trimmed || trimmed[0] === '<') return false;
const headerLine = trimmed.slice(0, trimmed.indexOf('\n') === -1 ? undefined : trimmed.indexOf('\n')) const headerLine = trimmed
.trim().toLowerCase(); .slice(0, trimmed.indexOf('\n') === -1 ? undefined : trimmed.indexOf('\n'))
.trim()
.toLowerCase();
const fields = headerLine.split(',').map((f) => f.trim()); const fields = headerLine.split(',').map((f) => f.trim());
return REQUIRED_HEADER_FIELDS.every((required) => fields.includes(required)); return REQUIRED_HEADER_FIELDS.every((required) => fields.includes(required));
} }
@@ -62,8 +71,13 @@ export function parseFirmsCsv(text) {
// Locate the header (first non-empty line) and build a column index so the // Locate the header (first non-empty line) and build a column index so the
// parser survives column reordering across FIRMS product versions. // parser survives column reordering across FIRMS product versions.
let headerIndex = 0; let headerIndex = 0;
while (headerIndex < lines.length && !lines[headerIndex].trim()) headerIndex += 1; while (headerIndex < lines.length && !lines[headerIndex].trim())
const header = lines[headerIndex].trim().toLowerCase().split(',').map((f) => f.trim()); headerIndex += 1;
const header = lines[headerIndex]
.trim()
.toLowerCase()
.split(',')
.map((f) => f.trim());
const col = new Map(header.map((name, i) => [name, i])); const col = new Map(header.map((name, i) => [name, i]));
const iLat = col.get('latitude'); const iLat = col.get('latitude');
const iLon = col.get('longitude'); const iLon = col.get('longitude');
@@ -114,7 +128,8 @@ export function parseFirmsCsv(text) {
* @returns {number} Epoch ms, or NaN when unparseable. * @returns {number} Epoch ms, or NaN when unparseable.
*/ */
export function acquisitionMsUtc(acqDate, acqTime) { export function acquisitionMsUtc(acqDate, acqTime) {
if (typeof acqDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(acqDate)) return NaN; if (typeof acqDate !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(acqDate))
return NaN;
const timeText = String(acqTime ?? '').trim(); const timeText = String(acqTime ?? '').trim();
if (!/^\d{1,4}$/.test(timeText)) return NaN; if (!/^\d{1,4}$/.test(timeText)) return NaN;
const hhmm = timeText.padStart(4, '0'); const hhmm = timeText.padStart(4, '0');
@@ -123,7 +138,15 @@ export function acquisitionMsUtc(acqDate, acqTime) {
const day = Number(acqDate.slice(8, 10)); const day = Number(acqDate.slice(8, 10));
const hours = Number(hhmm.slice(0, 2)); const hours = Number(hhmm.slice(0, 2));
const minutes = Number(hhmm.slice(2, 4)); const minutes = Number(hhmm.slice(2, 4));
if (month < 1 || month > 12 || day < 1 || day > 31 || hours > 23 || minutes > 59) return NaN; if (
month < 1 ||
month > 12 ||
day < 1 ||
day > 31 ||
hours > 23 ||
minutes > 59
)
return NaN;
return Date.UTC(year, month - 1, day, hours, minutes); return Date.UTC(year, month - 1, day, hours, minutes);
} }
+21 -6
View File
@@ -14,10 +14,11 @@ import {
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const FIXTURE = fs.readFileSync( const FIXTURE = fs.readFileSync(
path.join(__dirname, 'fixtures', 'firms-viirs-noaa20-sample.csv'), path.join(__dirname, 'fixtures', 'firms-viirs-noaa20-sample.csv'),
'utf8' 'utf8',
); );
const HEADER = 'latitude,longitude,bright_ti4,scan,track,acq_date,acq_time,satellite,instrument,confidence,version,bright_ti5,frp,daynight'; const HEADER =
'latitude,longitude,bright_ti4,scan,track,acq_date,acq_time,satellite,instrument,confidence,version,bright_ti5,frp,daynight';
test('fixture parses to 45 records with finite lat/lon/frp', () => { test('fixture parses to 45 records with finite lat/lon/frp', () => {
const records = parseFirmsCsv(FIXTURE); const records = parseFirmsCsv(FIXTURE);
@@ -48,15 +49,24 @@ test('fixture first row maps every field, categorical confidence preserved raw',
}); });
test('acquisitionMsUtc: "1006" = 10:06 UTC', () => { test('acquisitionMsUtc: "1006" = 10:06 UTC', () => {
assert.equal(acquisitionMsUtc('2026-07-16', '1006'), Date.UTC(2026, 6, 16, 10, 6)); assert.equal(
acquisitionMsUtc('2026-07-16', '1006'),
Date.UTC(2026, 6, 16, 10, 6),
);
}); });
test('acquisitionMsUtc: non-zero-padded "45" = 00:45 UTC', () => { test('acquisitionMsUtc: non-zero-padded "45" = 00:45 UTC', () => {
assert.equal(acquisitionMsUtc('2026-07-16', '45'), Date.UTC(2026, 6, 16, 0, 45)); assert.equal(
acquisitionMsUtc('2026-07-16', '45'),
Date.UTC(2026, 6, 16, 0, 45),
);
}); });
test('acquisitionMsUtc: "0" = midnight UTC; garbage → NaN', () => { test('acquisitionMsUtc: "0" = midnight UTC; garbage → NaN', () => {
assert.equal(acquisitionMsUtc('2026-07-16', '0'), Date.UTC(2026, 6, 16, 0, 0)); assert.equal(
acquisitionMsUtc('2026-07-16', '0'),
Date.UTC(2026, 6, 16, 0, 0),
);
assert.ok(Number.isNaN(acquisitionMsUtc('not-a-date', '1006'))); assert.ok(Number.isNaN(acquisitionMsUtc('not-a-date', '1006')));
assert.ok(Number.isNaN(acquisitionMsUtc('2026-07-16', 'xx'))); assert.ok(Number.isNaN(acquisitionMsUtc('2026-07-16', 'xx')));
}); });
@@ -118,7 +128,12 @@ test('filterTrailing24h: window is [now − 24 h, now + 2 h] inclusive', () => {
const kept = filterTrailing24h(records, now); const kept = filterTrailing24h(records, now);
assert.deepEqual( assert.deepEqual(
kept.map((r) => `${r.acqDate} ${r.acqTime}`), kept.map((r) => `${r.acqDate} ${r.acqTime}`),
['2026-07-15 1200', '2026-07-15 1201', '2026-07-16 1130', '2026-07-16 1400'] [
'2026-07-15 1200',
'2026-07-15 1201',
'2026-07-16 1130',
'2026-07-16 1400',
],
); );
}); });
+3 -1
View File
@@ -19,7 +19,9 @@ const GBFS_ALLOWED_HOSTS = new Set([
* @returns {boolean} * @returns {boolean}
*/ */
export function isAllowedGbfsHost(hostname) { export function isAllowedGbfsHost(hostname) {
const host = String(hostname || '').trim().toLowerCase(); const host = String(hostname || '')
.trim()
.toLowerCase();
if (!host) return false; if (!host) return false;
if (GBFS_ALLOWED_HOSTS.has(host)) return true; if (GBFS_ALLOWED_HOSTS.has(host)) return true;
return host.endsWith('.publicbikesystem.net'); return host.endsWith('.publicbikesystem.net');
+45 -19
View File
@@ -24,7 +24,10 @@ function defaultSleep(ms) {
export function parseTerrainPoints(raw) { export function parseTerrainPoints(raw) {
const text = String(raw || '').trim(); const text = String(raw || '').trim();
if (!text) return null; if (!text) return null;
const pairs = text.split(';').map((part) => part.trim()).filter(Boolean); const pairs = text
.split(';')
.map((part) => part.trim())
.filter(Boolean);
if (pairs.length === 0) return null; if (pairs.length === 0) return null;
const points = []; const points = [];
for (const pair of pairs) { for (const pair of pairs) {
@@ -83,16 +86,19 @@ export function terrainRetryAfterMs(value, nowMs = Date.now()) {
* @param {object} [options] * @param {object} [options]
* @returns {Promise<Array<object>>} * @returns {Promise<Array<object>>}
*/ */
export async function fetchTerrainChunkWithRetry(points, { export async function fetchTerrainChunkWithRetry(
fetchImpl = globalThis.fetch, points,
sleep = defaultSleep, {
random = Math.random, fetchImpl = globalThis.fetch,
now = Date.now, sleep = defaultSleep,
makeSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs), random = Math.random,
attemptTimeoutMs = 30_000, now = Date.now,
retryBudgetMs = TERRAIN_RETRY_BUDGET_MS, makeSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs),
maxAttempts = TERRAIN_MAX_ATTEMPTS, attemptTimeoutMs = 30_000,
} = {}) { retryBudgetMs = TERRAIN_RETRY_BUDGET_MS,
maxAttempts = TERRAIN_MAX_ATTEMPTS,
} = {},
) {
const pointsParam = points.map(terrainPointKey).join(';'); const pointsParam = points.map(terrainPointKey).join(';');
const url = `https://terrain.reearth.land/heights.json?points=${encodeURIComponent(pointsParam)}`; const url = `https://terrain.reearth.land/heights.json?points=${encodeURIComponent(pointsParam)}`;
let retryStartedAt = null; let retryStartedAt = null;
@@ -117,7 +123,9 @@ export async function fetchTerrainChunkWithRetry(points, {
} }
const json = await res.json(); const json = await res.json();
if (!Array.isArray(json?.results)) { if (!Array.isArray(json?.results)) {
const error = new Error('malformed upstream response (no results array)'); const error = new Error(
'malformed upstream response (no results array)',
);
error.retryable = false; error.retryable = false;
throw error; throw error;
} }
@@ -131,14 +139,18 @@ export async function fetchTerrainChunkWithRetry(points, {
if (remaining <= 0) break; if (remaining <= 0) break;
const retryAfterMs = terrainRetryAfterMs(error?.retryAfter, now()); const retryAfterMs = terrainRetryAfterMs(error?.retryAfter, now());
const backoffMs = Math.round(350 * (2 ** attempt) * (0.75 + random() * 0.5)); const backoffMs = Math.round(
350 * 2 ** attempt * (0.75 + random() * 0.5),
);
const requestedDelay = retryAfterMs ?? backoffMs; const requestedDelay = retryAfterMs ?? backoffMs;
const delayMs = Math.min(requestedDelay, remaining); const delayMs = Math.min(requestedDelay, remaining);
if (delayMs > 0) await sleep(delayMs); if (delayMs > 0) await sleep(delayMs);
} }
} }
throw lastError || new Error('terrain heights upstream retry budget exhausted'); throw (
lastError || new Error('terrain heights upstream retry budget exhausted')
);
} }
/** /**
@@ -171,7 +183,12 @@ export async function resolveTerrainHeightRequest({
const missing = []; const missing = [];
for (const [key, point] of unique) { for (const [key, point] of unique) {
const entry = cache.get(key); const entry = cache.get(key);
if (entry && validTerrainResult(entry.result) && requestStartedAt - entry.at < ttlMs) continue; if (
entry &&
validTerrainResult(entry.result) &&
requestStartedAt - entry.at < ttlMs
)
continue;
missing.push({ key, point }); missing.push({ key, point });
} }
@@ -180,7 +197,8 @@ export async function resolveTerrainHeightRequest({
if (missing.length > 0) { if (missing.length > 0) {
try { try {
const fetched = await fetchMissing(missing.map((item) => item.point)); const fetched = await fetchMissing(missing.map((item) => item.point));
if (!Array.isArray(fetched)) throw new Error('malformed upstream response (no results array)'); if (!Array.isArray(fetched))
throw new Error('malformed upstream response (no results array)');
const fetchedAt = now(); const fetchedAt = now();
for (let i = 0; i < missing.length; i += 1) { for (let i = 0; i < missing.length; i += 1) {
const result = fetched[i]; const result = fetched[i];
@@ -188,8 +206,13 @@ export async function resolveTerrainHeightRequest({
cache.set(missing[i].key, { at: fetchedAt, result }); cache.set(missing[i].key, { at: fetchedAt, result });
cacheChanged = true; cacheChanged = true;
} }
if (fetched.length !== missing.length || missing.some((_, i) => !validTerrainResult(fetched[i]))) { if (
upstreamError = new Error('upstream omitted one or more terrain heights'); fetched.length !== missing.length ||
missing.some((_, i) => !validTerrainResult(fetched[i]))
) {
upstreamError = new Error(
'upstream omitted one or more terrain heights',
);
} }
} catch (error) { } catch (error) {
upstreamError = error; upstreamError = error;
@@ -203,7 +226,10 @@ export async function resolveTerrainHeightRequest({
if (results.some((result) => result == null)) { if (results.some((result) => result == null)) {
return { return {
status: 502, status: 502,
body: { error: 'terrain heights fetch failed and no cache available for every point' }, body: {
error:
'terrain heights fetch failed and no cache available for every point',
},
cacheChanged, cacheChanged,
upstreamError, upstreamError,
}; };
+48 -18
View File
@@ -18,7 +18,13 @@ test('per-point cache reconstruction preserves exact request order with mixed hi
[terrainPointKey([10, 1]), { at: now, result: result('cached-a', 101) }], [terrainPointKey([10, 1]), { at: now, result: result('cached-a', 101) }],
[terrainPointKey([30, 3]), { at: now, result: result('cached-c', 303) }], [terrainPointKey([30, 3]), { at: now, result: result('cached-c', 303) }],
]); ]);
const points = [[30, 3], [20, 2], [10, 1], [40, 4], [20, 2]]; const points = [
[30, 3],
[20, 2],
[10, 1],
[40, 4],
[20, 2],
];
let fetchedPoints = null; let fetchedPoints = null;
const response = await resolveTerrainHeightRequest({ const response = await resolveTerrainHeightRequest({
@@ -33,11 +39,18 @@ test('per-point cache reconstruction preserves exact request order with mixed hi
}); });
assert.equal(response.status, 200); assert.equal(response.status, 200);
assert.deepEqual(fetchedPoints, [[20, 2], [40, 4]], 'only unique cache misses go upstream'); assert.deepEqual(
fetchedPoints,
[
[20, 2],
[40, 4],
],
'only unique cache misses go upstream',
);
assert.deepEqual( assert.deepEqual(
response.body.results.map((item) => item.id), response.body.results.map((item) => item.id),
['cached-c', 'fresh-b', 'cached-a', 'fresh-d', 'fresh-b'], ['cached-c', 'fresh-b', 'cached-a', 'fresh-d', 'fresh-b'],
'each output position must map back to its own input point' 'each output position must map back to its own input point',
); );
}); });
@@ -59,7 +72,9 @@ test('429 retry honors Retry-After before the next attempt', async () => {
return { return {
ok: false, ok: false,
status: 429, status: 429,
headers: { get: (name) => name.toLowerCase() === 'retry-after' ? '3' : null }, headers: {
get: (name) => (name.toLowerCase() === 'retry-after' ? '3' : null),
},
}; };
} }
return { return {
@@ -83,21 +98,28 @@ test('permanently failing upstream exhausts retries and returns 502 without fake
cache: new Map(), cache: new Map(),
ttlMs: 10_000, ttlMs: 10_000,
now: () => clock, now: () => clock,
fetchMissing: (missing) => fetchTerrainChunkWithRetry(missing, { fetchMissing: (missing) =>
now: () => clock, fetchTerrainChunkWithRetry(missing, {
random: () => 0.5, now: () => clock,
makeSignal: () => undefined, random: () => 0.5,
sleep: async (ms) => { clock += ms; }, makeSignal: () => undefined,
fetchImpl: async () => { sleep: async (ms) => {
attempts += 1; clock += ms;
return { ok: false, status: 503, headers: { get: () => null } }; },
}, fetchImpl: async () => {
}), attempts += 1;
return { ok: false, status: 503, headers: { get: () => null } };
},
}),
}); });
assert.equal(attempts, 4, 'one initial attempt plus three retries'); assert.equal(attempts, 4, 'one initial attempt plus three retries');
assert.equal(response.status, 502); assert.equal(response.status, 502);
assert.equal('results' in response.body, false, 'failure body must not fabricate positional heights'); assert.equal(
'results' in response.body,
false,
'failure body must not fabricate positional heights',
);
}); });
test('stale per-point entries serve through a failed refresh only when every point is real', async () => { test('stale per-point entries serve through a failed refresh only when every point is real', async () => {
@@ -106,12 +128,20 @@ test('stale per-point entries serve through a failed refresh only when every poi
[terrainPointKey([2, 2]), { at: 0, result: result('stale-b', 22) }], [terrainPointKey([2, 2]), { at: 0, result: result('stale-b', 22) }],
]); ]);
const response = await resolveTerrainHeightRequest({ const response = await resolveTerrainHeightRequest({
points: [[2, 2], [1, 1]], points: [
[2, 2],
[1, 1],
],
cache, cache,
ttlMs: 100, ttlMs: 100,
now: () => 1000, now: () => 1000,
fetchMissing: async () => { throw new Error('offline'); }, fetchMissing: async () => {
throw new Error('offline');
},
}); });
assert.equal(response.status, 200); assert.equal(response.status, 200);
assert.deepEqual(response.body.results.map((item) => item.id), ['stale-b', 'stale-a']); assert.deepEqual(
response.body.results.map((item) => item.id),
['stale-b', 'stale-a'],
);
}); });
+14 -8
View File
@@ -29,7 +29,8 @@ const MERCATOR_LAT_LIMIT = 85.05112878;
* @returns {boolean} True when the coordinate is a fetchable tile. * @returns {boolean} True when the coordinate is a fetchable tile.
*/ */
export function isValidTileCoord(z, x, y) { export function isValidTileCoord(z, x, y) {
if (!Number.isInteger(z) || !Number.isInteger(x) || !Number.isInteger(y)) return false; if (!Number.isInteger(z) || !Number.isInteger(x) || !Number.isInteger(y))
return false;
if (z < MIN_TILE_ZOOM || z > MAX_TILE_ZOOM) return false; if (z < MIN_TILE_ZOOM || z > MAX_TILE_ZOOM) return false;
const n = 2 ** z; const n = 2 ** z;
return x >= 0 && x < n && y >= 0 && y < n; return x >= 0 && x < n && y >= 0 && y < n;
@@ -47,11 +48,14 @@ export function isValidTileCoord(z, x, y) {
*/ */
export function lonLatToTile(lon, lat, z) { export function lonLatToTile(lon, lat, z) {
const n = 2 ** z; const n = 2 ** z;
const clampedLat = Math.max(-MERCATOR_LAT_LIMIT, Math.min(MERCATOR_LAT_LIMIT, lat)); const clampedLat = Math.max(
-MERCATOR_LAT_LIMIT,
Math.min(MERCATOR_LAT_LIMIT, lat),
);
const latRad = (clampedLat * Math.PI) / 180; const latRad = (clampedLat * Math.PI) / 180;
const x = Math.floor(((lon + 180) / 360) * n); const x = Math.floor(((lon + 180) / 360) * n);
const y = Math.floor( const y = Math.floor(
((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * n ((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2) * n,
); );
return { return {
x: Math.max(0, Math.min(n - 1, x)), x: Math.max(0, Math.min(n - 1, x)),
@@ -70,7 +74,8 @@ export function lonLatToTile(lon, lat, z) {
export function tileToBBox(z, x, y) { export function tileToBBox(z, x, y) {
const n = 2 ** z; const n = 2 ** z;
const lonAt = (col) => (col / n) * 360 - 180; const lonAt = (col) => (col / n) * 360 - 180;
const latAt = (row) => (Math.atan(Math.sinh(Math.PI * (1 - (2 * row) / n))) * 180) / Math.PI; const latAt = (row) =>
(Math.atan(Math.sinh(Math.PI * (1 - (2 * row) / n))) * 180) / Math.PI;
return { return {
west: lonAt(x), west: lonAt(x),
east: lonAt(x + 1), east: lonAt(x + 1),
@@ -132,10 +137,11 @@ export function utcDayKey(epochMs = Date.now()) {
* @returns {{date:string, count:number}} Valid state for `dayKey`. * @returns {{date:string, count:number}} Valid state for `dayKey`.
*/ */
export function normalizeBudget(state, dayKey) { export function normalizeBudget(state, dayKey) {
const valid = Boolean(state) const valid =
&& state.date === dayKey Boolean(state) &&
&& Number.isFinite(state.count) state.date === dayKey &&
&& state.count >= 0; Number.isFinite(state.count) &&
state.count >= 0;
return valid ? state : { date: dayKey, count: 0 }; return valid ? state : { date: dayKey, count: 0 };
} }
+57 -16
View File
@@ -53,22 +53,39 @@ test('lonLatToTile: downtown Austin @ z12 -> x935 y1686 (matches fixture)', () =
test('tileToBBox: fixture tile bbox contains downtown Austin', () => { test('tileToBBox: fixture tile bbox contains downtown Austin', () => {
const bbox = tileToBBox(12, 935, 1686); const bbox = tileToBBox(12, 935, 1686);
assert.ok(bbox.west <= AUSTIN.lon && AUSTIN.lon <= bbox.east, `lon outside [${bbox.west}, ${bbox.east}]`); assert.ok(
assert.ok(bbox.south <= AUSTIN.lat && AUSTIN.lat <= bbox.north, `lat outside [${bbox.south}, ${bbox.north}]`); bbox.west <= AUSTIN.lon && AUSTIN.lon <= bbox.east,
`lon outside [${bbox.west}, ${bbox.east}]`,
);
assert.ok(
bbox.south <= AUSTIN.lat && AUSTIN.lat <= bbox.north,
`lat outside [${bbox.south}, ${bbox.north}]`,
);
assert.ok(bbox.north > bbox.south && bbox.east > bbox.west); assert.ok(bbox.north > bbox.south && bbox.east > bbox.west);
}); });
test('tileToBBox <-> lonLatToTile roundtrip at the bbox center', () => { test('tileToBBox <-> lonLatToTile roundtrip at the bbox center', () => {
const bbox = tileToBBox(12, 935, 1686); const bbox = tileToBBox(12, 935, 1686);
const center = { lon: (bbox.west + bbox.east) / 2, lat: (bbox.south + bbox.north) / 2 }; const center = {
lon: (bbox.west + bbox.east) / 2,
lat: (bbox.south + bbox.north) / 2,
};
const t = lonLatToTile(center.lon, center.lat, 12); const t = lonLatToTile(center.lon, center.lat, 12);
assert.deepEqual({ x: t.x, y: t.y }, { x: 935, y: 1686 }); assert.deepEqual({ x: t.x, y: t.y }, { x: 935, y: 1686 });
}); });
test('lonLatToTile clamps poles and antimeridian into valid range', () => { test('lonLatToTile clamps poles and antimeridian into valid range', () => {
for (const [lon, lat] of [[-180, 89.9], [180, -89.9], [179.9999, 0]]) { for (const [lon, lat] of [
[-180, 89.9],
[180, -89.9],
[179.9999, 0],
]) {
const t = lonLatToTile(lon, lat, 12); const t = lonLatToTile(lon, lat, 12);
assert.equal(isValidTileCoord(12, t.x, t.y), true, `invalid tile for ${lon},${lat}: ${t.x},${t.y}`); assert.equal(
isValidTileCoord(12, t.x, t.y),
true,
`invalid tile for ${lon},${lat}: ${t.x},${t.y}`,
);
} }
}); });
@@ -76,14 +93,19 @@ test('lonLatToTile clamps poles and antimeridian into valid range', () => {
test('tilesForBounds: clamped Austin viewport covers the fixture tile', () => { test('tilesForBounds: clamped Austin viewport covers the fixture tile', () => {
const bounds = { const bounds = {
south: AUSTIN.lat - 0.025, north: AUSTIN.lat + 0.025, south: AUSTIN.lat - 0.025,
west: AUSTIN.lon - 0.025, east: AUSTIN.lon + 0.025, north: AUSTIN.lat + 0.025,
west: AUSTIN.lon - 0.025,
east: AUSTIN.lon + 0.025,
}; };
const tiles = tilesForBounds(bounds, 12); const tiles = tilesForBounds(bounds, 12);
assert.ok(tiles.length >= 1 && tiles.length <= 4, `tile count ${tiles.length}`); assert.ok(
tiles.length >= 1 && tiles.length <= 4,
`tile count ${tiles.length}`,
);
assert.ok( assert.ok(
tiles.some((t) => t.z === 12 && t.x === 935 && t.y === 1686), tiles.some((t) => t.z === 12 && t.x === 935 && t.y === 1686),
`fixture tile missing from ${JSON.stringify(tiles)}` `fixture tile missing from ${JSON.stringify(tiles)}`,
); );
}); });
@@ -102,7 +124,7 @@ test('tilesForBounds: bounds straddling a tile edge return both tiles', () => {
}); });
test('tilesForBounds: default zoom is 12 and every tile is valid', () => { test('tilesForBounds: default zoom is 12 and every tile is valid', () => {
const bounds = { south: 51.49, north: 51.52, west: -0.14, east: -0.10 }; // London const bounds = { south: 51.49, north: 51.52, west: -0.14, east: -0.1 }; // London
const tiles = tilesForBounds(bounds); const tiles = tilesForBounds(bounds);
assert.ok(tiles.length >= 1); assert.ok(tiles.length >= 1);
for (const t of tiles) { for (const t of tiles) {
@@ -112,7 +134,11 @@ test('tilesForBounds: default zoom is 12 and every tile is valid', () => {
}); });
test('tilesForBounds: runaway bounds are truncated by the safety cap', () => { test('tilesForBounds: runaway bounds are truncated by the safety cap', () => {
const tiles = tilesForBounds({ south: -60, north: 60, west: -170, east: 170 }, 12, { maxTiles: 16 }); const tiles = tilesForBounds(
{ south: -60, north: 60, west: -170, east: 170 },
12,
{ maxTiles: 16 },
);
assert.ok(tiles.length <= 16, `got ${tiles.length}`); assert.ok(tiles.length <= 16, `got ${tiles.length}`);
}); });
@@ -129,15 +155,30 @@ test('normalizeBudget: same-day state passes through untouched', () => {
}); });
test('normalizeBudget: day rollover resets the counter', () => { test('normalizeBudget: day rollover resets the counter', () => {
const rolled = normalizeBudget({ date: '2026-07-16', count: 39999 }, '2026-07-17'); const rolled = normalizeBudget(
{ date: '2026-07-16', count: 39999 },
'2026-07-17',
);
assert.deepEqual(rolled, { date: '2026-07-17', count: 0 }); assert.deepEqual(rolled, { date: '2026-07-17', count: 0 });
}); });
test('normalizeBudget: missing/corrupt state starts fresh', () => { test('normalizeBudget: missing/corrupt state starts fresh', () => {
assert.deepEqual(normalizeBudget(null, '2026-07-16'), { date: '2026-07-16', count: 0 }); assert.deepEqual(normalizeBudget(null, '2026-07-16'), {
assert.deepEqual(normalizeBudget({ date: '2026-07-16', count: NaN }, '2026-07-16'), { date: '2026-07-16', count: 0 }); date: '2026-07-16',
assert.deepEqual(normalizeBudget({ count: 5 }, '2026-07-16'), { date: '2026-07-16', count: 0 }); count: 0,
assert.deepEqual(normalizeBudget({ date: '2026-07-16', count: -3 }, '2026-07-16'), { date: '2026-07-16', count: 0 }); });
assert.deepEqual(
normalizeBudget({ date: '2026-07-16', count: NaN }, '2026-07-16'),
{ date: '2026-07-16', count: 0 },
);
assert.deepEqual(normalizeBudget({ count: 5 }, '2026-07-16'), {
date: '2026-07-16',
count: 0,
});
assert.deepEqual(
normalizeBudget({ date: '2026-07-16', count: -3 }, '2026-07-16'),
{ date: '2026-07-16', count: 0 },
);
}); });
test('isOverBudget: at or above the limit is over, below is not', () => { test('isOverBudget: at or above the limit is over, below is not', () => {
+207 -70
View File
@@ -9,111 +9,248 @@ import { localProviderPlugins } from '../../server/providers/local.js';
function install(plugin) { function install(plugin) {
const routes = new Map(); const routes = new Map();
plugin.configureServer({ middlewares: { use(route, handler) { routes.set(route, handler); } } }); plugin.configureServer({
middlewares: {
use(route, handler) {
routes.set(route, handler);
},
},
});
assert.equal(routes.size, 1); assert.equal(routes.size, 1);
return async (url = '/', method = 'GET') => { return async (url = '/', method = 'GET') => {
const res = { headersSent: false, writeHead(status, headers) { Object.assign(this, { status, headers, headersSent: true }); }, end(body) { this.body = body; } }; const res = {
headersSent: false,
writeHead(status, headers) {
Object.assign(this, { status, headers, headersSent: true });
},
end(body) {
this.body = body;
},
};
await [...routes.values()][0]({ url, method }, res); await [...routes.values()][0]({ url, method }, res);
return res; return res;
}; };
} }
function isolate(t, env = {}) { function isolate(t, env = {}) {
for(const [name,value] of Object.entries(env)) { for (const [name, value] of Object.entries(env)) {
const previous = process.env[name]; const previous = process.env[name];
t.after(() => { if(previous === undefined) delete process.env[name]; else process.env[name] = previous; }); t.after(() => {
if (previous === undefined) delete process.env[name];
else process.env[name] = previous;
});
process.env[name] = value; process.env[name] = value;
} }
t.mock.method(fsp, 'readFile', async () => { throw Error('no disk cache'); }); t.mock.method(fsp, 'readFile', async () => {
t.mock.method(fsp, 'stat', async () => { throw Error('no disk cache'); }); throw Error('no disk cache');
});
t.mock.method(fsp, 'stat', async () => {
throw Error('no disk cache');
});
t.mock.method(fsp, 'mkdir', async () => {}); t.mock.method(fsp, 'mkdir', async () => {});
t.mock.method(fsp, 'writeFile', async () => {}); t.mock.method(fsp, 'writeFile', async () => {});
t.mock.method(globalThis, 'setInterval', () => ({ unref() {} })); t.mock.method(globalThis, 'setInterval', () => ({ unref() {} }));
t.mock.method(console, 'warn', () => {}); t.mock.method(console, 'warn', () => {});
} }
const json = res => JSON.parse(res.body); const json = (res) => JSON.parse(res.body);
test('standalone composition mounts every extracted provider exactly once without acquisition', (t) => { test('standalone composition mounts every extracted provider exactly once without acquisition', (t) => {
t.mock.method(globalThis, 'fetch', () => { throw Error('construction must not fetch'); }); t.mock.method(globalThis, 'fetch', () => {
throw Error('construction must not fetch');
});
const plugins = localProviderPlugins(); const plugins = localProviderPlugins();
for(const factory of [terrainHeightsProxy,tomtomProxy,firmsProxy,gbfsProxy]) assert.equal(plugins.filter(p=>p.name===factory().name).length,1); for (const factory of [
terrainHeightsProxy,
tomtomProxy,
firmsProxy,
gbfsProxy,
])
assert.equal(plugins.filter((p) => p.name === factory().name).length, 1);
}); });
test('terrain middleware chunks missing points and reconstructs repeated/reordered requests from cache', async (t) => { test('terrain middleware chunks missing points and reconstructs repeated/reordered requests from cache', async (t) => {
isolate(t);let calls=0;const sizes=[]; isolate(t);
let calls = 0;
const sizes = [];
t.mock.method(globalThis, 'fetch', async (raw) => { t.mock.method(globalThis, 'fetch', async (raw) => {
calls++;const url=new URL(raw);assert.equal(url.origin,'https://terrain.reearth.land'); calls++;
const points=url.searchParams.get('points').split(';').map(p=>p.split(',').map(Number));sizes.push(points.length); const url = new URL(raw);
return Response.json({results:points.map(([lon])=>({ellipsoid:lon+100}))}); assert.equal(url.origin, 'https://terrain.reearth.land');
const points = url.searchParams
.get('points')
.split(';')
.map((p) => p.split(',').map(Number));
sizes.push(points.length);
return Response.json({
results: points.map(([lon]) => ({ ellipsoid: lon + 100 })),
});
}); });
const request=install(terrainHeightsProxy()); const request = install(terrainHeightsProxy());
const points=Array.from({length:257},(_,i)=>`${i/100},1`); const points = Array.from({ length: 257 }, (_, i) => `${i / 100},1`);
const res=await request('/?points='+points.join(';')); const res = await request('/?points=' + points.join(';'));
assert.equal(res.status,200);assert.deepEqual(sizes,[256,1]); assert.equal(res.status, 200);
const reordered=await request('/?points=2.56,1;0,1;2.56,1'); assert.deepEqual(sizes, [256, 1]);
assert.deepEqual(json(reordered),{results:[{ellipsoid:102.56},{ellipsoid:100},{ellipsoid:102.56}]});assert.equal(calls,2); const reordered = await request('/?points=2.56,1;0,1;2.56,1');
assert.equal((await request('/?points=invalid')).status,400); assert.deepEqual(json(reordered), {
assert.equal((await request('/?points='+Array(2001).fill('0,1').join(';'))).status,500); results: [{ ellipsoid: 102.56 }, { ellipsoid: 100 }, { ellipsoid: 102.56 }],
assert.equal(calls,2); });
assert.equal(calls, 2);
assert.equal((await request('/?points=invalid')).status, 400);
assert.equal(
(await request('/?points=' + Array(2001).fill('0,1').join(';'))).status,
500,
);
assert.equal(calls, 2);
}); });
test('terrain middleware migrates valid legacy disk points without fabricating omitted heights', async (t) => { test('terrain middleware migrates valid legacy disk points without fabricating omitted heights', async (t) => {
isolate(t); isolate(t);
t.mock.method(fsp,'readFile',async()=>JSON.stringify({'1,2;3,4':{at:Date.now(),results:[{ellipsoid:77}]}})); t.mock.method(fsp, 'readFile', async () =>
let calls=0; JSON.stringify({
t.mock.method(globalThis,'fetch',async(raw)=>{calls++;assert.equal(new URL(raw).searchParams.get('points'),'3.00000,4.00000');return Response.json({results:[{ellipsoid:88}]});}); '1,2;3,4': { at: Date.now(), results: [{ ellipsoid: 77 }] },
const res=await install(terrainHeightsProxy())('/?points=3,4;1,2'); }),
assert.deepEqual(json(res),{results:[{ellipsoid:88},{ellipsoid:77}]});assert.equal(calls,1); );
let calls = 0;
t.mock.method(globalThis, 'fetch', async (raw) => {
calls++;
assert.equal(new URL(raw).searchParams.get('points'), '3.00000,4.00000');
return Response.json({ results: [{ ellipsoid: 88 }] });
});
const res = await install(terrainHeightsProxy())('/?points=3,4;1,2');
assert.deepEqual(json(res), {
results: [{ ellipsoid: 88 }, { ellipsoid: 77 }],
});
assert.equal(calls, 1);
}); });
test('traffic middleware preserves keyless mode, caching, stale budget fallback and UTC rollover', async (t) => { test('traffic middleware preserves keyless mode, caching, stale budget fallback and UTC rollover', async (t) => {
isolate(t,{TOMTOM_API_KEY:'',TOMTOM_DAILY_TILE_BUDGET:'1'}); isolate(t, { TOMTOM_API_KEY: '', TOMTOM_DAILY_TILE_BUDGET: '1' });
let now=Date.UTC(2026,8,12,12);t.mock.method(Date,'now',()=>now); let now = Date.UTC(2026, 8, 12, 12);
let calls=0;t.mock.method(globalThis,'fetch',async(raw)=>{calls++;assert.equal(new URL(raw).searchParams.get('key'),'fixture-key');return new Response(new Uint8Array([1,2,3]));}); t.mock.method(Date, 'now', () => now);
const request=install(tomtomProxy()); let calls = 0;
assert.equal(json(await request('/status')).hasKey,false); t.mock.method(globalThis, 'fetch', async (raw) => {
assert.equal((await request('/flow/8/1/1.pbf')).status,503); calls++;
assert.equal((await request('/flow/7/1/1.pbf')).status,400);assert.equal(calls,0); assert.equal(new URL(raw).searchParams.get('key'), 'fixture-key');
process.env.TOMTOM_API_KEY='fixture-key'; return new Response(new Uint8Array([1, 2, 3]));
assert.equal((await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],'MISS'); });
assert.equal((await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],'HIT'); const request = install(tomtomProxy());
assert.equal(json(await request('/status')).dailyCount,1);assert.equal(calls,1); assert.equal(json(await request('/status')).hasKey, false);
now+=120001; assert.equal((await request('/flow/8/1/1.pbf')).status, 503);
assert.equal((await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],'STALE-BUDGET'); assert.equal((await request('/flow/7/1/1.pbf')).status, 400);
assert.equal((await request('/flow/8/2/1.pbf')).status,429);assert.equal(calls,1); assert.equal(calls, 0);
now+=86400000; process.env.TOMTOM_API_KEY = 'fixture-key';
assert.equal(json(await request('/status')).dailyCount,0); assert.equal(
assert.equal((await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],'MISS');assert.equal(calls,2); (await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],
'MISS',
);
assert.equal(
(await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],
'HIT',
);
assert.equal(json(await request('/status')).dailyCount, 1);
assert.equal(calls, 1);
now += 120001;
assert.equal(
(await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],
'STALE-BUDGET',
);
assert.equal((await request('/flow/8/2/1.pbf')).status, 429);
assert.equal(calls, 1);
now += 86400000;
assert.equal(json(await request('/status')).dailyCount, 0);
assert.equal(
(await request('/flow/8/1/1.pbf')).headers['x-tomtom-cache'],
'MISS',
);
assert.equal(calls, 2);
}); });
test('FIRMS retains a large successful source during partial failure and filters stale data at serve time', async (t) => { test('FIRMS retains a large successful source during partial failure and filters stale data at serve time', async (t) => {
isolate(t,{FIRMS_MAP_KEY:''});let now=Date.UTC(2026,8,12,12);t.mock.method(Date,'now',()=>now); isolate(t, { FIRMS_MAP_KEY: '' });
let calls=0; let now = Date.UTC(2026, 8, 12, 12);
const header='latitude,longitude,acq_date,acq_time,confidence,frp\n'; t.mock.method(Date, 'now', () => now);
const csv=header+'30,-97,2026-09-12,1200,h,10\n'.repeat(130001); let calls = 0;
t.mock.method(globalThis,'fetch',async(raw)=>{ const header = 'latitude,longitude,acq_date,acq_time,confidence,frp\n';
calls++;const url=new URL(raw);assert.equal(url.hostname,'firms.modaps.eosdis.nasa.gov'); const csv = header + '30,-97,2026-09-12,1200,h,10\n'.repeat(130001);
if(url.pathname.includes('mapkey_status')) return Response.json({current_transactions:3,transaction_limit:5000}); t.mock.method(globalThis, 'fetch', async (raw) => {
return url.pathname.includes('VIIRS_NOAA20')?new Response(csv):new Response('offline',{status:503}); calls++;
const url = new URL(raw);
assert.equal(url.hostname, 'firms.modaps.eosdis.nasa.gov');
if (url.pathname.includes('mapkey_status'))
return Response.json({
current_transactions: 3,
transaction_limit: 5000,
});
return url.pathname.includes('VIIRS_NOAA20')
? new Response(csv)
: new Response('offline', { status: 503 });
}); });
const request=install(firmsProxy()); const request = install(firmsProxy());
assert.equal((await request()).status,503);assert.equal(json(await request('/status')).hasKey,false);assert.equal(calls,0); assert.equal((await request()).status, 503);
process.env.FIRMS_MAP_KEY='fixture-key'; assert.equal(json(await request('/status')).hasKey, false);
const first=json(await request());assert.equal(first.count,130001);assert.equal(first.sources.filter(s=>s.ok).length,1);assert.equal(calls,3); assert.equal(calls, 0);
assert.equal(json(await request()).count,130001);assert.equal(calls,3); process.env.FIRMS_MAP_KEY = 'fixture-key';
assert.deepEqual(json(await request('/status')).transactions,{used:3,limit:5000}); const first = json(await request());
now+=25*3600000;t.mock.method(globalThis,'fetch',async()=>new Response('offline',{status:503})); assert.equal(first.count, 130001);
const stale=json(await request());assert.equal(stale.stale,true);assert.equal(stale.count,0); assert.equal(first.sources.filter((s) => s.ok).length, 1);
assert.equal(calls, 3);
assert.equal(json(await request()).count, 130001);
assert.equal(calls, 3);
assert.deepEqual(json(await request('/status')).transactions, {
used: 3,
limit: 5000,
});
now += 25 * 3600000;
t.mock.method(
globalThis,
'fetch',
async () => new Response('offline', { status: 503 }),
);
const stale = json(await request());
assert.equal(stale.stale, true);
assert.equal(stale.count, 0);
}); });
test('GBFS keeps host/path/method guards, response caps and distinct information/status cache headers', async (t) => { test('GBFS keeps host/path/method guards, response caps and distinct information/status cache headers', async (t) => {
let calls=0;t.mock.method(globalThis,'fetch',async(raw)=>{calls++;assert.equal(new URL(raw).hostname,'gbfs.lyft.com');return Response.json({data:{stations:[]}});}); let calls = 0;
const request=install(gbfsProxy());const target=p=>'/'+encodeURIComponent('https://gbfs.lyft.com/'+p); t.mock.method(globalThis, 'fetch', async (raw) => {
assert.equal((await request(target('station_status.json'),'POST')).status,405); calls++;
assert.equal((await request('/'+encodeURIComponent('https://example.com/station_status.json'))).status,403); assert.equal(new URL(raw).hostname, 'gbfs.lyft.com');
assert.equal((await request(target('gbfs.json'))).status,400); return Response.json({ data: { stations: [] } });
assert.equal((await request('/%zz')).status,400);assert.equal(calls,0); });
assert.equal((await request(target('station_information.json'))).headers['Cache-Control'],'public, max-age=300'); const request = install(gbfsProxy());
assert.equal((await request(target('station_status.json'))).headers['Cache-Control'],'no-store'); const target = (p) => '/' + encodeURIComponent('https://gbfs.lyft.com/' + p);
t.mock.method(globalThis,'fetch',async()=>new Response('x',{headers:{'content-length':String(6*1024*1024)}})); assert.equal(
assert.equal((await request(target('station_status.json'))).status,502); (await request(target('station_status.json'), 'POST')).status,
405,
);
assert.equal(
(
await request(
'/' + encodeURIComponent('https://example.com/station_status.json'),
)
).status,
403,
);
assert.equal((await request(target('gbfs.json'))).status, 400);
assert.equal((await request('/%zz')).status, 400);
assert.equal(calls, 0);
assert.equal(
(await request(target('station_information.json'))).headers[
'Cache-Control'
],
'public, max-age=300',
);
assert.equal(
(await request(target('station_status.json'))).headers['Cache-Control'],
'no-store',
);
t.mock.method(
globalThis,
'fetch',
async () =>
new Response('x', {
headers: { 'content-length': String(6 * 1024 * 1024) },
}),
);
assert.equal((await request(target('station_status.json'))).status, 502);
}); });