mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
- src/geo drops the ported debug argument checks and the tests that bitExact already covers; package boundaries accept a directory entry, so src/geo/ is listed once per group. - Cesium no longer carries movers and lines nobody draws with: they are optional engine capabilities. The fake engine and the Cesium camera use the shared cameraMath; unused contract tables, clock.ticked, the 'finest' surface detail and Navara click statistics are gone. - ALPR keeps one overlay core and one presentation model for both engines, bikeshare one station bookkeeping, the Navara ground effects one definition helper; image atlases, view centres, mover banks and colours are shared. - The legacy switch route, the clock offset and rate controls and other unused exports are removed. - The acceptance runs the Navara draw, geo import and scope checks; scenarios use the harness switch and page helpers; the main chunk check keeps the size budget only and runs once in CI.
120 lines
3.8 KiB
JavaScript
120 lines
3.8 KiB
JavaScript
// Map tiles for headless QA: the disk cache entry of a tile request and one
|
|
// fetch per missing tile, with backoff on HTTP 429, for the free tile hosts
|
|
// the QA pages load (OpenStreetMap imagery, the keyless re:earth terrain).
|
|
// Back-to-back QA runs otherwise reload the same tiles until those hosts
|
|
// rate-limit them. The strict harness (scripts/qa-strict-harness.mjs) pauses
|
|
// only the requests to these hosts (CDP Fetch with URL patterns) and answers
|
|
// them from the cache in the system temp directory.
|
|
|
|
import { createHash } from 'node:crypto';
|
|
import path from 'node:path';
|
|
|
|
export const CACHED_TILE_HOSTS = Object.freeze([
|
|
'tile.openstreetmap.org',
|
|
'terrain.reearth.land',
|
|
]);
|
|
const RETRIES = 4;
|
|
/** Who is asking, with a contact URL (the tile hosts' usage policies). */
|
|
export const QA_USER_AGENT =
|
|
'gods-eye-view-qa/1 (headless tests with a disk cache; https://github.com/bilawalsidhu/gods-eye-view)';
|
|
/** Fetches to the tile hosts in flight at once. */
|
|
export const MAX_MISSES_IN_FLIGHT = 2;
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
/**
|
|
* The cache entry for a GET of `url` with `accept` (terrain answers by its
|
|
* Accept header), or null for a host that is not cached.
|
|
*/
|
|
export function tileCacheFile(url, accept, dir, hosts = CACHED_TILE_HOSTS) {
|
|
let host;
|
|
try {
|
|
const parsed = new URL(url);
|
|
if (parsed.protocol !== 'https:') return null;
|
|
host = parsed.host;
|
|
} catch {
|
|
return null;
|
|
}
|
|
if (!hosts.includes(host)) return null;
|
|
const key = createHash('sha256')
|
|
.update(`${url}\n${accept ?? ''}`)
|
|
.digest('hex')
|
|
.slice(0, 32);
|
|
return path.join(dir, `${host}-${key}`);
|
|
}
|
|
|
|
/**
|
|
* Waits before retry `attempt` (0-based) after a 429: the server's
|
|
* Retry-After in seconds when it sends one (at most 30 s), else 1, 2, 4, 8 s.
|
|
*/
|
|
export function backoffMs(attempt, retryAfter) {
|
|
const seconds =
|
|
retryAfter == null || retryAfter === '' ? Number.NaN : Number(retryAfter);
|
|
if (Number.isFinite(seconds) && seconds >= 0)
|
|
return Math.min(seconds, 30) * 1000;
|
|
return 1000 * 2 ** attempt;
|
|
}
|
|
|
|
/** GETs a tile, retrying on 429; resolves to { status, contentType, body }. */
|
|
export async function fetchTile(
|
|
url,
|
|
{ accept, fetchImpl = fetch, wait = sleep } = {},
|
|
) {
|
|
for (let attempt = 0; ; attempt++) {
|
|
const response = await fetchImpl(url, {
|
|
headers: {
|
|
'User-Agent': QA_USER_AGENT,
|
|
...(accept ? { Accept: accept } : {}),
|
|
},
|
|
});
|
|
if (response.status !== 429 || attempt >= RETRIES)
|
|
return {
|
|
status: response.status,
|
|
contentType: response.headers.get('content-type'),
|
|
body: Buffer.from(await response.arrayBuffer()),
|
|
};
|
|
await wait(backoffMs(attempt, response.headers.get('retry-after')));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads missing tiles politely: at most MAX_MISSES_IN_FLIGHT fetches at
|
|
* once (the rest queue), and one fetch for identical misses in flight (same
|
|
* URL and Accept). `load(url, accept)` resolves to fetchTile's result.
|
|
*/
|
|
export function createMissLoader({
|
|
fetchImpl = fetch,
|
|
wait = sleep,
|
|
maxInFlight = MAX_MISSES_IN_FLIGHT,
|
|
} = {}) {
|
|
const pending = new Map();
|
|
const queue = [];
|
|
let running = 0;
|
|
const next = () => {
|
|
while (running < maxInFlight && queue.length) {
|
|
const job = queue.shift();
|
|
running++;
|
|
fetchTile(job.url, { accept: job.accept, fetchImpl, wait })
|
|
.then(job.resolve, job.reject)
|
|
.finally(() => {
|
|
running--;
|
|
pending.delete(job.key);
|
|
next();
|
|
});
|
|
}
|
|
};
|
|
return {
|
|
load(url, accept) {
|
|
const key = `${url}\n${accept ?? ''}`;
|
|
let promise = pending.get(key);
|
|
if (!promise) {
|
|
promise = new Promise((resolve, reject) =>
|
|
queue.push({ key, url, accept, resolve, reject }),
|
|
);
|
|
pending.set(key, promise);
|
|
next();
|
|
}
|
|
return promise;
|
|
},
|
|
};
|
|
}
|