Skip to content

Load published documents

windgram/transport fetches published documents correctly. Its problem is the torn read: a model’s manifest and its site profiles are separately cached static files, so around a publish, a pair fetched together can describe two different runs. loadProfile() performs the consistency check a consumer would otherwise have to hand-write.

Two cache entries, two runs, one page

Independently cached manifest and profile files can describe different runs; loadProfile detects the torn pair and retries it once.

A sequence diagram of the transport's reference-time skew dance. A publish refreshes the manifest cache entry before the profile cache entry, so a consumer fetching both receives a manifest from the 06Z run and a profile from the 00Z run. loadProfile compares the pair's reference times with runsConsistent, waits 1.5 seconds, refetches, and returns either a consistent pair, the freshest complete pair marked stale, or a discriminated DocumentMiss.

The reference-time skew dance1ONE PUBLISH, TWO CACHE ENTRIESstatic storage · independent expirydata/<model>/manifest.jsonreferenceTime 06Zcache entry expired first — the new run is visibledata/<model>/sites/<site>.jsonrun.referenceTime 00Zcache entry still valid — the old run is still served06Z ≠ 00Z — a torn pair. Rendering it as one forecast lies about both runs.2THE SKEW DANCEloadProfile — fetch, compare, retry onceFETCH THE PAIRmanifest + profile, in parallelrunsConsistent(m, p)?same model and referenceTimefalseWAIT ≈1.5 spublishes converge quicklyREFETCHthe pair, oncetrue → return the pair with stale: false; after the refetch, stale records whether the pair still disagrees3EVERY WAY IT RESOLVESdiscriminate a miss with "miss" in result{ manifest, profile, stale: false }a consistent pair — render it{ manifest, profile, stale: true }freshest complete pair, still torn —note it or fall back; never mix the two{ miss: "absent" | "invalid", url }absent: routine 404 · invalid: contract break —log it loudly; other HTTP errors throw
The retry delay defaults to 1500 ms and is injectable. The transport keeps no cache and writes no storage — the stale flag is a report, and the policy it triggers belongs to the caller.Units one publish cycle; no numeric scale
load-profile.ts
import { loadProfile, type LoadedProfile } from "windgram/transport";
export async function loadTestHill(): Promise<LoadedProfile | null> {
const result = await loadProfile({
fetch,
baseUrl: "https://data.meteo.azohra.com",
modelSlug: "hrrr-conus",
siteSlug: "test-hill",
});
if ("miss" in result) {
// "absent" is routine: the model or site is not published here.
if (result.miss === "invalid") console.error(`contract break at ${result.url}`);
return null;
}
return result; // result.stale reports a pair still torn after the retry
}

loadProfile fetches both documents, validates each with its contract guard, and compares them with runsConsistent. On disagreement it waits (1500 ms by default, configurable and injectable through retry) and refetches the pair once. It resolves to one of three shapes:

  • { manifest, profile, stale: false } — a consistent pair; render it.
  • { manifest, profile, stale: true } — the freshest complete pair seen, still torn after the retry. A publish is in flight; render with a “still syncing” note or fall back to a pair you cached earlier. Never mix the two documents as if they were one forecast.
  • a DocumentMiss — nothing to render, with the reason discriminated.

A DocumentMiss separates two situations that would otherwise present identically as “no chart”:

miss Meaning Treat it as
"absent" HTTP 404 — the model or site is not published at this root Routine; a site outside a model’s domain reads this way
"invalid" The document exists but failed its contract guard Never routine — a contract break or prototype data; log the url loudly

Discriminate with "miss" in result. Any non-404 HTTP failure throws TransportHttpError (carrying status and url) instead of masking itself as absence. When both documents miss, the manifest’s miss wins: a model not publishing at all is the root cause of its site documents missing too.

loadRuns({ fetch, baseUrl }) fetches runs.json from the data root, the cross-model run index — each published model’s current (referenceTime, generatedAt), keyed by slug. It is a single document, so there is no pair to tear; it exists so run discovery gets the same miss semantics as loadProfile. Compare its timestamps with the catalogue’s runIntervalHours under your own freshness policy.

The pure pair check is exported too: runsConsistent(manifest, profile) is true exactly when both documents name the same model and run — useful when documents arrive through your own storage rather than these loaders.

fetch is a parameter, not an import. Pass the runtime’s own WHATWG-shaped fetch — browser, Node, workers, undici, or a test stub — which keeps this module runtime-agnostic and the rest of the package I/O-free.

The transport performs no caching and no storage writes, because no storage API is portable across runtimes. Cache keys, quotas, invalidation, and stale-pair policy belong to the consumer: the transport reports stale; what happens next is yours. TransportResponse, TransportFetch, RetryOptions, LoadProfileOptions, LoadedProfile, and LoadRunsOptions type those seams.

Where these files come from and how they are deployed is the publisher’s side of the story: Publish static output and Downstream access.