fix(data): load bundled region packs in the browser; bound region lookups (#743)

The Natural Earth and neighborhood packs were loaded with a JSON-attributed
import(). The Vite dev server answers that request with a JavaScript module,
which the browser rejects, so every analyst region scope failed and fell
through to a geocode lookup that could take 12-28 s.

- src/data/bundledJson.js fetches the pack from a new URL(..., import.meta.url)
  asset in the browser; under Node the caller's attributed import runs.
- resolveRegionRingForQuery caps the geocode + admin-boundary rung at 3 s and
  returns { error: 'region-timeout' }; the lookup keeps running and fills the
  cache. The analyst query reports it as code 'region-timeout'. The Nepal
  locator opts out of the budget.
- Unit tests for the loader, the timeout and the analyst error; the behavior
  harness checks the Gulf of Mexico scope and retries Texas on region-timeout.

Co-authored-by: Bilawal Sidhu <[email protected]>
This commit is contained in:
Bilawal SidhuandBilawal Sidhu authored and GitHub committed 2026-09-24 01:52:41 -05:00
1 parent 513dc87a0e
commit ce671ce500
15 files changed
+263 -28

No files matched your search

+9 -1
View File
@@ -18,7 +18,7 @@
*
* Providers (injected — keeps the engine pure and node-testable):
* getRecords(layerKey) → Array<record> (layer accessor snapshot)
* resolveRegionRing(name) → Promise<{ring, name}|null> (NE pack / admin boundary)
* resolveRegionRing(name) → Promise<{ring, name}|{error:'region-timeout'}|null>
* getViewContext() → {lat, lon, viewRadiusKm, bounds?} (camera-derived)
*
* @module data/analystEngine
@@ -234,6 +234,14 @@ export function createAnalystEngine(providers) {
const scope = spec.scope || { kind: 'view' };
if (scope.kind === 'region' && scope.name) {
const region = await providers.resolveRegionRing(scope.name);
if (region?.error === 'region-timeout') {
return {
ok: false,
code: 'region-timeout',
error: `Looking up the boundary for "${scope.name}" is taking too long — ask again in a moment.`,
coverage: { layersQueried, scope: `region:${scope.name}:timeout` },
};
}
if (!region?.ring) {
return {
ok: false,
+13
View File
@@ -187,6 +187,19 @@ test('analyst: unresolved region is an honest failure, not empty success', async
assert.match(r.error, /Atlantis/);
});
test('analyst: a region lookup timeout is reported as region-timeout', async () => {
const eng = createAnalystEngine({
getRecords: () => FLIGHTS,
resolveRegionRing: async (name) => ({ name, ring: null, error: 'region-timeout' }),
getViewContext: () => ({ lat: 30.27, lon: -97.74, viewRadiusKm: 150 }),
});
const r = await eng.query({ layers: ['flights'], scope: { kind: 'region', name: 'Texas' } });
assert.equal(r.ok, false);
assert.equal(r.code, 'region-timeout');
assert.match(r.error, /Texas/);
assert.equal(r.coverage.scope, 'region:Texas:timeout');
});
test('analyst: route fields queryable from cached enrichment only', async () => {
const r = await makeEngine().query({
layers: ['flights'], scope: { kind: 'anywhere' },
+29
View File
@@ -0,0 +1,29 @@
/**
* @file Load a JSON file shipped beside the source modules.
*
* The browser fetches the file from `url` — a `new URL('./….json',
* import.meta.url)` written in the calling module, which Vite serves as-is in
* dev and emits as an asset in the production build. Under Node (node:test)
* that URL is a `file:` URL, which `fetch` cannot read, so the caller's
* JSON-attributed import runs instead.
*
* The attributed import cannot serve the browser: the Vite dev server answers
* it with a JavaScript module, which the browser rejects for a JSON import.
*
* @module data/bundledJson
*/
/**
* @param {URL} url - Resolved against the calling module's `import.meta.url`.
* @param {() => Promise<{default: any}>} importJson - The caller's literal
* `import('./….json', { with: { type: 'json' } })`, used only under Node.
* @returns {Promise<any>} The parsed JSON.
*/
export async function loadBundledJson(url, importJson) {
if (url.protocol === 'file:') return (await importJson()).default;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status} for ${url.pathname}`);
}
return response.json();
}
+36
View File
@@ -0,0 +1,36 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { loadBundledJson } from './bundledJson.js';
const MARINE = new URL('./local_data/natural_earth/marine.json', import.meta.url);
const importMarine = () =>
import('./local_data/natural_earth/marine.json', { with: { type: 'json' } });
test('bundled JSON: a file URL uses the JSON import under Node', async (t) => {
const fetchMock = t.mock.method(globalThis, 'fetch');
const pack = await loadBundledJson(MARINE, importMarine);
assert.ok(pack.features.some((ft) => ft.name === 'Gulf of Mexico'));
assert.equal(fetchMock.mock.callCount(), 0);
});
test('bundled JSON: a served URL is fetched as plain JSON', async (t) => {
const requests = [];
t.mock.method(globalThis, 'fetch', async (url) => {
requests.push(String(url));
return new Response('{"features":[]}', {
headers: { 'content-type': 'application/json' },
});
});
const url = new URL('http://localhost/src/data/local_data/natural_earth/marine.json');
const importJson = () => assert.fail('the browser path must not import JSON');
assert.deepEqual(await loadBundledJson(url, importJson), { features: [] });
assert.deepEqual(requests, [url.href]);
});
test('bundled JSON: an HTTP error rejects so the retryable loader can retry', async (t) => {
t.mock.method(globalThis, 'fetch', async () => new Response('', { status: 404 }));
await assert.rejects(
loadBundledJson(new URL('http://localhost/missing.json'), importMarine),
/HTTP 404/,
);
});
+22 -19
View File
@@ -11,11 +11,12 @@
*
* PURE data module — no Cesium imports, node-testable. The packs are lazy-
* loaded on first lookup and cached in module scope (bbox/area computed once
* at load). In the browser Vite bundles the JSON via dynamic import; under
* node the same files are read from disk. A failed load is retried on the
* next lookup rather than cached (see `createRetryableLoader`).
* at load) through `loadBundledJson`, which works in the browser and under
* node:test. A failed load is retried on the next lookup rather than cached
* (see `createRetryableLoader`).
*/
import { loadBundledJson } from './bundledJson.js';
import { createRetryableLoader } from './retryableLoad.js';
const EARTH_RADIUS_KM = 6371;
@@ -120,20 +121,22 @@ function suffixVariants(norm) {
/** @type {Array|null} flat entry list for listRegions() */
let _entries = null;
async function loadPackFile(base) {
// Vite bundles these JSON files as modules; the import attribute is what Node
// needs to load the same files under node:test (same pattern as
// neighborhoodPolygons.js). One path, so no node: import reaches the browser.
const mod =
base === 'regions'
? await import('./local_data/natural_earth/regions.json', {
with: { type: 'json' },
})
: await import('./local_data/natural_earth/marine.json', {
with: { type: 'json' },
});
return mod.default || mod;
}
const PACKS = {
regions: {
url: new URL('./local_data/natural_earth/regions.json', import.meta.url),
importJson: () =>
import('./local_data/natural_earth/regions.json', {
with: { type: 'json' },
}),
},
marine: {
url: new URL('./local_data/natural_earth/marine.json', import.meta.url),
importJson: () =>
import('./local_data/natural_earth/marine.json', {
with: { type: 'json' },
}),
},
};
function buildEntries(pack, kind) {
const out = [];
@@ -175,8 +178,8 @@ function buildEntries(pack, kind) {
*/
const loadIndex = createRetryableLoader(async () => {
const [regions, marine] = await Promise.all([
loadPackFile('regions'),
loadPackFile('marine'),
loadBundledJson(PACKS.regions.url, PACKS.regions.importJson),
loadBundledJson(PACKS.marine.url, PACKS.marine.importJson),
]);
_entries = [
...buildEntries(regions, 'natural'),
+7 -5
View File
@@ -10,6 +10,7 @@
* touching this module — see `local_data/neighborhoods/SOURCE.md`.
*/
import { loadBundledJson } from './bundledJson.js';
import { createRetryableLoader } from './retryableLoad.js';
// bbox = [west, south, east, north]; only load a city file when the point falls in its box.
@@ -17,7 +18,11 @@ const CITY_FILES = [
{
id: 'san-francisco',
bbox: [-122.55, 37.7, -122.35, 37.84],
loader: () =>
url: new URL(
'./local_data/neighborhoods/san-francisco.json',
import.meta.url,
),
importJson: () =>
import('./local_data/neighborhoods/san-francisco.json', {
with: { type: 'json' },
}),
@@ -107,10 +112,7 @@ function cityLoader(city) {
let loader = _cityLoaders.get(city.id);
if (!loader) {
loader = createRetryableLoader(async () => {
// One path for both runtimes: Vite bundles the JSON as a module, and the
// import attribute is what Node needs to load the same file under node:test.
const mod = await city.loader();
const fc = mod.default || mod;
const fc = await loadBundledJson(city.url, city.importJson);
return Array.isArray(fc.features) ? fc.features : [];
});
_cityLoaders.set(city.id, loader);