Merge branch 'main' into feat/voice-geometry

Picks up the terrain partial-heights work (#1456). No conflicts; it changes
no voice prompt or tool description.
This commit is contained in:
Manjunath committed 2026-11-03 17:58:29 +05:30
commit 0b6cdc8e1f
8 files changed
+152 -14

No files matched your search

+19 -6
View File
@@ -151,12 +151,25 @@ export function createApplicationRequestServices({
const query = points
.map(({ lat, lon }) => `${lon.toFixed(5)},${lat.toFixed(5)}`)
.join(';');
return requireOk(
await request(`${urls.terrain}?points=${encodeURIComponent(query)}`, {
signal,
}),
'Terrain heights',
)?.results;
const response = await request(
`${urls.terrain}?points=${encodeURIComponent(query)}`,
{ signal },
);
// A failed request still carries the real heights the server had, so
// the caller can keep them; null marks a point without one.
const partial = response.data?.partial;
if (
!response.ok &&
Array.isArray(partial) &&
partial.length === points.length
) {
const error = new Error(
`Terrain heights unavailable (${response.status})`,
);
error.partial = partial;
throw error;
}
return requireOk(response, 'Terrain heights')?.results;
},
},
regional: {
+17
View File
@@ -110,6 +110,23 @@ test('a failed probe is retried after its backoff, not on every query', async ()
]);
});
test('a failed terrain request carries the real heights the server had', async () => {
const partial = [{ ellipsoid: 412.5 }, null];
const services = createApplicationRequestServices({
fetchImpl: async () => Response.json({ error: 'x', partial }, { status: 502 }),
});
const points = [{ lat: 30, lon: -97 }, { lat: 31, lon: -97 }];
await assert.rejects(services.terrain.getHeights(points), (error) => {
assert.match(error.message, /Terrain heights unavailable \(502\)/);
assert.deepEqual(error.partial, partial);
return true;
});
await assert.rejects(services.terrain.getHeights(points.slice(0, 1)), (error) => {
assert.equal(error.partial, undefined, 'a list of the wrong length is not attached');
return true;
});
});
test('the summary capability probe is asked once and unknown when unanswered', async () => {
for (const [answer, expected] of [
[() => Response.json({ configured: false }), false],
+26 -6
View File
@@ -226,7 +226,8 @@ export function createTerrainHeights({
* - Remaining (deduplicated) coordinates are sent to `/api/terrain/heights`
* in sequential chunks of <=64 points.
* - If a chunk request fails (network error, non-ok HTTP, malformed body),
* every point in THAT chunk falls back to geoid math
* every point in THAT chunk without a real height in the failure's
* `partial` falls back to geoid math
* (`sourceOrthometricM + geoidHeight` or `geoidHeight` alone) rather than
* failing the whole batch — a transient proxy outage in the middle of a
* large batch shouldn't blank out entities whose chunk happened to land
@@ -314,15 +315,34 @@ export function createTerrainHeights({
cacheStore(item.key, entry);
}
}
} catch {
} catch (error) {
requestSignal?.throwIfAborted();
// Proxy down (or cold cache had nothing to serve-stale) — fall back to
// geoid math for every point in this chunk. `ensureGeoidReady()` is
// awaited lazily, only on the fallback path, so the common (proxy
// healthy) case never pays for the geoid grid's dynamic import.
// geoid math for the points in this chunk. A failure that still
// carries real heights (the proxy's `partial`, e.g. cached points
// during an upstream 429 pause) keeps them; only the rest use the
// geoid. `ensureGeoidReady()` is awaited lazily, only on the fallback
// path, so the common (proxy healthy) case never pays for the geoid
// grid's dynamic import.
const partial =
Array.isArray(error?.partial) && error.partial.length === chunk.length
? error.partial
: [];
const fallback = [];
chunk.forEach((item, index) => {
const ellipsoid = partial[index]?.ellipsoid;
if (!Number.isFinite(ellipsoid)) {
fallback.push(item);
return;
}
const entry = { ellipsoid, source: 'reearth' };
resolvedThisCall.set(item.key, entry);
cacheStore(item.key, entry);
});
if (fallback.length === 0) continue;
await ensureGeoidReady();
requestSignal?.throwIfAborted();
for (const item of chunk) {
for (const item of fallback) {
const ellipsoid = geoidFallback(
item.lat,
item.lon,