Skip to content
6 changes: 4 additions & 2 deletions scripts/qcm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,9 +294,11 @@ switch-off, an older decode landing after a newer ask, and an uninterrupted ask

`quiet-corners-maps/request.js` is the one way the map reads from the network. `fetchWithDeadline(url, { fetchImpl, signal, timeoutMs, init, read, anyStatus })` (`read` swaps the body reader, e.g. `r => r.blob()`; `anyStatus` also reads the body of a non-2xx for services whose error answer carries the reason) answers `{ ok, status: 'ok' | 'timeout' | 'aborted' | 'error' }` and never throws. Its timer is a plain `AbortController`, so it works without `AbortSignal.any`, it ends a fetch that ignores its signal, and it clears the timer and the listener on the caller's signal when the call settles. A caller's cancel is `aborted`, never `timeout`. `orThrow(result, message)` is for the few adapters that throw on failure: the body of an ok result, else an Error carrying the result's `status` and `httpStatus` (a caller's cancel keeps the name `AbortError`). `createLatest()` is latest-wins: a new call aborts the previous one and the older answer comes back `stale`. `source-terrain-names.js` re-exports `fetchJson` and `createSearchGuard` from it for the callers that already import them.

The deadline is per source: `SOURCES.<id>.timeoutMs` in `source-registry.js`, `DEFAULT_SOURCE_TIMEOUT_MS` (10 s) unless the source cites a slower measurement in `timeoutMeasured`. GNIS place names keep 20 s because their latency is measured up to 31 s, and the wetlands and government-units layers keep the 20 s page their measurements were taken against. Every module in that directory except `main.js` and the seven listed next reads through the helper at its own source's value (the paged boundary and site callers pass it to `pageThrough`; a module with no registry entry, such as the ecoregions export, uses the default). `main.js` still has raw reads until PR-B2. The seven that reached master after this work was written (`address-geocode.js`, `source-avalanche.js`, `source-nrhp.js`, `source-ntad-rail-airports.js`, `source-nws-observations.js`, `source-snotel.js`, `source-stream-gauges.js`) still make their own raw calls, with no deadline from the helper, and are on the `ALLOW` list until a follow-up moves them. `REQUEST_POLICY.timeoutMs` is no longer a request deadline. The quad index pages at most `MAX_PAGES` (20) times and stops on a page that adds no new cell, ending `coverage: 'partial'`; its `timeoutMs` bounds each attempt, including `searchByName`, and a timeout is not retried.
The deadline is per source: `SOURCES.<id>.timeoutMs` in `source-registry.js`, `DEFAULT_SOURCE_TIMEOUT_MS` (10 s) unless the source cites a slower measurement in `timeoutMeasured`. GNIS place names keep 20 s because their latency is measured up to 31 s, and the wetlands and government-units layers keep the 20 s page their measurements were taken against. Every module in that directory except the seven listed next reads through the helper at its own source's value (the paged boundary and site callers pass it to `pageThrough`; a module with no registry entry, such as the ecoregions export, uses the default). The seven that reached master after this work was written (`address-geocode.js`, `source-avalanche.js`, `source-nrhp.js`, `source-ntad-rail-airports.js`, `source-nws-observations.js`, `source-snotel.js`, `source-stream-gauges.js`) still make their own raw calls, with no deadline from the helper, and are on the `ALLOW` list until a follow-up moves them. `REQUEST_POLICY.timeoutMs` is no longer a request deadline. The quad index pages at most `MAX_PAGES` (20) times and stops on a page that adds no new cell, ending `coverage: 'partial'`; its `timeoutMs` bounds each attempt, including `searchByName`, and a timeout is not retried.

`request.test.mjs` covers the helper (timeout, abort, cleanup, no `AbortSignal.any`, `anyStatus`, `orThrow`, latest-wins); `no-raw-fetch.test.mjs` fails on any raw `fetch(` or `fetchImpl(` call in that directory outside `request.js` and its `ALLOW` list (`main.js`, removed in PR-B2, and the seven named above; it also fails if an entry stops holding a raw call, so the list cannot rot); `adapter-deadlines.test.mjs` hands every adapter that takes `fetchImpl` a fetch that never answers and asserts it settles at its deadline, and that a caller's abort ends it as a cancel, not a timeout (a new `source-*.js` module that takes `fetchImpl` must be added to its table); `build.test.mjs` holds that the build's cancel reaches the aerial, relief and elevation requests; `map-index.test.mjs`, `arcgis-pager.test.mjs` and `registry.test.mjs` hold the quad paging cap, the pager deadline and the per-source `timeoutMs` rule.
`request.test.mjs` covers the helper (timeout, abort, cleanup, no `AbortSignal.any`, `anyStatus`, `orThrow`, latest-wins); `no-raw-fetch.test.mjs` fails on any raw `fetch(` or `fetchImpl(` call in that directory outside `request.js` and its `ALLOW` list (the seven modules named above; an entry fails once it stops holding a raw call, so the list cannot rot); `adapter-deadlines.test.mjs` hands every adapter that takes `fetchImpl` a fetch that never answers and asserts it settles at its deadline, and that a caller's abort ends it as a cancel, not a timeout (a new `source-*.js` module that takes `fetchImpl` must be added to its table); `build.test.mjs` holds that the build's cancel reaches the aerial, relief and elevation requests; `map-index.test.mjs`, `arcgis-pager.test.mjs` and `registry.test.mjs` hold the quad paging cap, the pager deadline and the per-source `timeoutMs` rule.

`main.js` makes no raw `fetch` call: the camera index and files, the camera still and the base-map tile probe go through `fetchWithDeadline`, and the PDF build is handed `globalThis.fetch` bound. A source-check pass owns one `AbortController` (`state.passAbort`) that `setArea` and `clearArea` abort, and every adapter call in the pass gets its signal, so replacing the area cancels the pass's requests; a pass that was cancelled returns before its catch blocks write source state. The quad-name search is latest-wins (`createLatest`) and says "Searching…" the moment it starts. The aurora and radar answers are checked against their switch after the await, and two quick toggles of the ecoregions layer share one request. `request-races-browser.test.mjs` proves the three that need a real network: it holds requests with `Fetch.requestPaused` and checks (a) two quad searches answered in reverse order show the second, (b) aurora switched on and off before its answer is never drawn (with an on-the-whole-way control), (c) clearing the area mid-pass ends the pass's request as `Network.loadingFailed` with `canceled: true`. Every wait is a condition, never a fixed sleep. It needs the built site (`bash scripts/build-site.sh --configDir site-config --minify`) and skips without Chromium.

## Accessibility checks

Expand Down
15 changes: 6 additions & 9 deletions scripts/qcm/no-raw-fetch.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,13 @@ import { join } from 'node:path';

const DIR = new URL('../../themes/headlessmode/static/js/quiet-corners-maps/', import.meta.url);
const HELPER = 'request.js';
// PR-B2 moves main.js onto request.js and removes this entry (and the list). Until then the
// page's own call sites (camera snapshot, the tile probe, the print adapters) are the only raw calls.
//
// The other seven entries are modules that reached master after QQA-13 was written (the E-P1..E-P6
// field-map layers and the finder's street-address lookup). Each still owns a raw call and its own
// signal, so none has a deadline from request.js yet. Moving them onto fetchWithDeadline is a
// follow-up; until then they are listed so this test guards every other module, and the stale-entry
// test below drops an entry the day its raw call goes.
// main.js was the last module of the original set with raw calls and now goes through request.js
// too. The seven entries below reached master after QQA-13 was written (the E-P1..E-P6 field-map
// layers and the finder's street-address lookup). Each still owns a raw call and its own signal,
// so none has a deadline from request.js yet. Moving them onto fetchWithDeadline is a follow-up;
// until then they are listed so this test guards every other module. An entry is a debt with a
// reason beside it, and the stale-entry test below fails once it is paid.
const ALLOW = [
'main.js',
'address-geocode.js',
'source-avalanche.js', 'source-nrhp.js', 'source-ntad-rail-airports.js',
'source-nws-observations.js', 'source-snotel.js', 'source-stream-gauges.js',
Expand Down
Loading
Loading