refactor(app): share application components and request services

This commit is contained in:
Sameh Khamis committed 2026-09-13 22:41:11 -07:00
1 parent a0a0128a59
commit a67bc60a29
84 files changed
+2798 -1731

No files matched your search

+14 -6
View File
@@ -246,7 +246,7 @@ export function createAnnotationEngine({
// allSettled gives per-item error isolation (one failed item never aborts the batch); the
// mutation pass below then runs in ORDER, so de-dup, the synchronous live-cap check, and output
// order are all preserved exactly as the old serial loop had them.
const settled = await Promise.allSettled(list.map((spec) => resolveSpec(spec, controller.signal)));
const settled = await Promise.allSettled(list.map((spec) => resolveSpec(spec, controller.signal, opts.flyTo === true)));
try {
for (let i = 0; i < list.length; i += 1) {
@@ -385,7 +385,7 @@ export function createAnnotationEngine({
};
}
async function resolveSpec(spec, signal) {
async function resolveSpec(spec, signal, allowDistant = false) {
const type = normalizeType(spec?.type);
if (type === 'route') {
const points = Array.isArray(spec.points) ? spec.points : [];
@@ -395,13 +395,15 @@ export function createAnnotationEngine({
for (const pt of points) {
const name = pt.target ?? pt.name ?? null;
const r = await resolveTarget({
placeSearch, viewer,
placeSearch,
viewer,
target: name,
latitude: pt.latitude,
longitude: pt.longitude,
screenX: pt.screenX,
screenY: pt.screenY,
footprint: false,
allowDistant,
signal,
});
if (r) resolvedPts.push(r);
@@ -438,23 +440,27 @@ export function createAnnotationEngine({
}
if (type === 'arrow') {
const from = await resolveTarget({
placeSearch, viewer,
placeSearch,
viewer,
target: spec.target,
latitude: spec.latitude,
longitude: spec.longitude,
screenX: spec.screenX,
screenY: spec.screenY,
footprint: false,
allowDistant,
signal,
});
const to = await resolveTarget({
placeSearch, viewer,
placeSearch,
viewer,
target: spec.toTarget,
latitude: spec.toLatitude,
longitude: spec.toLongitude,
screenX: spec.toScreenX,
screenY: spec.toScreenY,
footprint: false,
allowDistant,
signal,
});
if (!from || !to) {
@@ -473,7 +479,8 @@ export function createAnnotationEngine({
}
const wantFootprint = type === 'area' ? spec.footprint !== false : Boolean(spec.footprint);
return resolveTarget({
placeSearch, viewer,
placeSearch,
viewer,
target: spec.target,
latitude: spec.latitude,
longitude: spec.longitude,
@@ -490,6 +497,7 @@ export function createAnnotationEngine({
// timeout — field test 7 logs); deferring them lets the mark appear and the tool
// result return while the outline resolves, then upgrades the mark in place.
deferFootprint: wantFootprint,
allowDistant,
signal,
});
}
+16
View File
@@ -710,3 +710,19 @@ test('a late annotation resolver cannot redraw after destruction', async (t) =>
assert.equal(calls.add, 0);
assert.equal(engine.count(), 0);
});
test('only an explicit navigation request permits resolving distant annotation targets', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer } = throwingRendererHarness();
renderer.destroy = () => {};
const received = [];
const engine = createAnnotationEngine({ viewer: {}, renderer,
resolveTarget: async (options) => { received.push(options); return null; },
});
await engine.annotate([{ type: 'point', target: 'Remote landmark' }]);
await engine.annotate([{ type: 'point', target: 'Remote landmark' }], { flyTo: true });
assert.deepEqual(received.map(options => options.allowDistant), [false, true]);
engine.destroy();
});
+5 -34
View File
@@ -1,3 +1,4 @@
import { applicationServices } from '../services/application.js';
import { defaultGeospatial } from '../search/defaults.js';
import * as Cesium from 'cesium';
import { lookupNeighborhoodRing } from '../data/neighborhoodPolygons.js';
@@ -93,7 +94,7 @@ function linkAbort(controller, externalSignal) {
export async function resolveAnnotationTarget({
placeSearch = unavailablePlaceSearch,
viewer, target, latitude, longitude, footprint = false, intent = 'the_thing',
entityKind = null, labelHint = null, deferFootprint = false, screenX, screenY, signal,
entityKind = null, labelHint = null, deferFootprint = false, allowDistant = false, screenX, screenY, signal,
}) {
let lon = Number(longitude);
let lat = Number(latitude);
@@ -158,7 +159,7 @@ export async function resolveAnnotationTarget({
// LOOKING AT. If the geocode missed or landed far from the view centre, try a view-biased
// Places Text Search; a hit within the trust bound overrides + skips the gate. Local geocodes
// (neighborhoods, nearby buildings) are NOT far, so they keep the geocode + scope/polygon path.
if (center && trace.places === 'skipped' && !bypassNearViewGuards) {
if (center && trace.places === 'skipped' && !bypassNearViewGuards && !allowDistant) {
let geocodeFar = source !== 'geocode';
if (source === 'geocode' && approximateDistanceM(center.lat, center.lon, lat, lon) / 1000 > MIN_DRIFT_FLOOR_KM) {
geocodeFar = true;
@@ -243,7 +244,7 @@ export async function resolveAnnotationTarget({
const limitKm = Math.max(VIEWPORT_DRIFT_FACTOR * vpGate.radiusKm, MIN_DRIFT_FLOOR_KM);
return driftKm > limitKm ? { driftKm, limitKm } : null;
};
if (fromGeocode && !bypassNearViewGuards) {
if (fromGeocode && !bypassNearViewGuards && !allowDistant) {
const drift = gateDrift(lat, lon);
if (drift) {
if (trace.query) {
@@ -739,48 +740,18 @@ export function refineScope(scope, entityKind) {
return scope;
}
/** True when an HTTP-200 Overpass body actually signals a runtime FAILURE (server-side
* timeout / out-of-memory) via its `remark` — a transient error, not an authoritative
* empty result, so callers must not cache it as a definitive not-found. */
function overpassHasError(data) {
const remark = String(data?.remark || '').toLowerCase();
return remark.includes('runtime error') || remark.includes('timed out') || remark.includes('out of memory');
}
/** A distinct Overpass throttle result that must not enter the ordinary transient ladder. */
export function isRateLimitedOutcome(value) {
return value?.rateLimited === true;
}
/** Parse Retry-After seconds or an HTTP date into a non-negative millisecond delay. */
function parseRetryAfterMs(value) {
if (value == null || String(value).trim() === '') return null;
const seconds = Number(value);
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1000);
const at = Date.parse(String(value));
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : null;
}
/** POST an Overpass QL query and return elements, a transient null, or a throttle object. */
async function overpassJson(query, timeoutMs = 14000, signal) {
const controller = new AbortController();
const detach = linkAbort(controller, signal);
const timer = window.setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch('/api/overpass', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `data=${encodeURIComponent(query)}`,
signal: controller.signal,
});
const retryAfter = res.headers?.get?.('Retry-After');
if (res.status === 429 || (res.status === 503 && retryAfter != null)) {
return { rateLimited: true, retryAfterMs: parseRetryAfterMs(retryAfter) };
}
if (!res.ok) return null;
const data = await res.json();
if (overpassHasError(data)) return null; // 200 with a body-level timeout/error → transient
return Array.isArray(data?.elements) ? data.elements : null;
return await applicationServices.boundaries.query(query, { signal: controller.signal });
} catch {
return null;
} finally {