Real weather, real aircraft, a heightfield off the main thread, and instruments
Three things that were built and never connected, connected.
**The weather was already there.** `observe()` has always taken a
`WeatherObservation` and `main.ts` has always passed null, so the cloud,
precipitation, visibility and marine-layer paths in atmosphere.ts had never run
outside a test. The server already shipped NWS, met.no and Open-Meteo, all
configured off. What was actually missing was that a single TERA_ORIGIN_LAT/LNG
served one metro and lied to the other — so weather and traffic are per-region
now, derived from the city's own bounds, and the Bay Area gets its fog while
Long Beach gets its own sky. The route takes ?city= or a validated ?lat=&lng=
and refuses to become an open geocoding proxy for the planet.
**The heightfield moved to a Worker.** 2.3 s of blocked main thread at boot, and
another ~950 ms of point-in-polygon on top of it: the park mask is filled in the
worker now, and block placement samples four corners and only runs the exact
test on a cell that straddles an edge — 8 buildings differ out of 185,036.
createScene is async and takes a Stage as a consequence, and there is a
main-thread fallback because "clone it and it works" has no exception clause.
**Spaces is a chunk you fetch when you reach for the door**, not one everybody
downloads. Same for the godmode tools. The entry chunk is 722 kB rather than
772; three.js is most of what is left and splitting it is a different job.
**Godmode is an instrument panel now** rather than one slider: the date and the
season, not just the hour, so the Meeus moon and the sun's seasonal arc become
visible instead of merely correct; a weather override that says on screen when
it is lying; a frame-time and draw-call readout; and a pose editor that emits a
paste-ready Chapter block, which is the thing that makes adding New York cheap.
Two blockers the review caught:
- Every city switch leaked 8 GPU textures — one of them a 2048x2048 shadow map
— and ~10.5 shader programs, and deleteTexture had never been called once in
the app's lifetime. The renderer was being built per scene; it belongs to the
canvas, for the life of the page.
- An upstream fetch that threw rather than returning null skipped the cache
stamp, so the TTL — the only rate limit on outbound calls — collapsed to one
upstream request per inbound request, and the caller got a 500.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+66
-3
@@ -9,28 +9,91 @@
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 6000;
|
||||
|
||||
/**
|
||||
* The largest upstream body this service will read.
|
||||
*
|
||||
* "Every outbound call is bounded" was true of the *time* and not of the size:
|
||||
* a bare `res.json()` reads whatever arrives, and what arrives is chosen by
|
||||
* somebody else's server. An ADS-B endpoint answering with 200,000 aircraft was
|
||||
* measured at 14.9 MB, parsed into an array this process then held and served
|
||||
* — cached, publicly — to every anonymous caller for the length of the TTL.
|
||||
*
|
||||
* Four megabytes is roughly two orders of magnitude above any honest answer
|
||||
* from the four upstreams here (a busy adsb.lol circle is tens of kilobytes, an
|
||||
* NWS observation is under ten) and well under anything that would trouble the
|
||||
* heap. It bounds the damage; `flights/adsb.ts` caps the row count, which
|
||||
* bounds what is kept.
|
||||
*/
|
||||
const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
||||
|
||||
export interface GetJsonOptions {
|
||||
headers?: Record<string, string>;
|
||||
timeoutMs?: number;
|
||||
/** Body-size ceiling in bytes. Defaults to `MAX_BODY_BYTES`. */
|
||||
maxBytes?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* `null` on any failure at all — transport, status, or unparseable body. The
|
||||
* caller decides what a missing answer means; nothing here does.
|
||||
* `null` on any failure at all — transport, status, an oversized body, or one
|
||||
* that will not parse. The caller decides what a missing answer means; nothing
|
||||
* here does.
|
||||
*/
|
||||
export async function getJson<T>(url: string, opts: GetJsonOptions = {}): Promise<T | null> {
|
||||
const maxBytes = opts.maxBytes ?? MAX_BODY_BYTES;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
headers: { accept: "application/json", ...opts.headers },
|
||||
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS),
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return (await res.json()) as T;
|
||||
|
||||
/**
|
||||
* The header first, because it is free and it is the one that stops the
|
||||
* transfer before it happens. It is only advisory — a chunked response
|
||||
* sends none — so the body is counted as it streams as well, and the
|
||||
* `cancel()` closes the socket on a server that lied or did not say.
|
||||
*/
|
||||
const declared = Number(res.headers.get("content-length"));
|
||||
if (Number.isFinite(declared) && declared > maxBytes) {
|
||||
await res.body?.cancel();
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = await readBounded(res, maxBytes);
|
||||
if (text === null) return null;
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The body as text, or `null` the moment it goes over `maxBytes`. */
|
||||
async function readBounded(res: Response, maxBytes: number): Promise<string | null> {
|
||||
const body = res.body;
|
||||
// Undici always gives a stream; a test double or a `fetch` polyfill may not,
|
||||
// and falling back to `res.text()` there is still bounded by the header check
|
||||
// above and by the timeout.
|
||||
if (!body) {
|
||||
const text = await res.text();
|
||||
return text.length > maxBytes ? null : text;
|
||||
}
|
||||
const reader = body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let size = 0;
|
||||
let out = "";
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
size += value.byteLength;
|
||||
if (size > maxBytes) {
|
||||
await reader.cancel();
|
||||
return null;
|
||||
}
|
||||
out += decoder.decode(value, { stream: true });
|
||||
}
|
||||
return out + decoder.decode();
|
||||
}
|
||||
|
||||
/**
|
||||
* A User-Agent that identifies this software and the operator running it.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user