fix(street-level): own the framing flight and the nearest-photo lookup

From the IC8 review of #768, each with tests that fail before:

- P1: closing a photo, switching the layer off or destroying it stops the
  1.6 s framing flight toward that photo. The flight is claimed with a
  token before flyToBoundingSphere and released by its complete/cancel
  callbacks, so cancelFraming() calls camera.cancelFlight() only while
  the flight is still Street Level's: a newer navigation flight (a search
  result, a scene) is never cancelled. Turning FOLLOW on also stops it, so
  the tween cannot overwrite the follow view.
- P2: an older nearest-photo lookup can no longer replace a newer choice.
  The core owns one AbortController per lookup, aborted by a newer lookup,
  a direct open or cone click, closing the viewer, disable and destroy;
  the signal reaches source.nearestImages through the provider, and an
  aborted lookup never surfaces as an error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Opus 5.5 committed 2026-10-06 15:16:57 +01:00
1 parent c2f1b54d00
commit 09fc10756c
8 files changed
+371 -27

No files matched your search

+29 -4
View File
@@ -239,7 +239,21 @@ export function createStreetLevelLayer({
parts.credits.hide(state.viewer, entry.def);
}
/**
* The nearest-image lookup in flight. Anything newer the user asks for (a
* later lookup, a picked image, closing, the layer going off) aborts it,
* so its late answer can never replace that choice.
* @type {AbortController|null}
*/
let nearestLookup = null;
function abortNearest() {
nearestLookup?.abort();
nearestLookup = null;
}
function openImage(providerId, imageId) {
abortNearest();
return parts.viewerHost.open(providerId, imageId);
}
@@ -358,6 +372,7 @@ export function createStreetLevelLayer({
disable() {
state.enabled = false;
abortNearest();
unwatchSurface();
parts.viewerHost.unmount();
for (const entry of state.providers.values()) entry.instance.deactivate();
@@ -461,6 +476,10 @@ export function createStreetLevelLayer({
const view = point || viewCentre(state.viewer);
if (!Number.isFinite(view?.lat) || !Number.isFinite(view?.lon))
return false;
abortNearest();
const lookup = new AbortController();
nearestLookup = lookup;
const { signal } = lookup;
state.street.loading = true;
state.street.error = null;
notify();
@@ -468,20 +487,25 @@ export function createStreetLevelLayer({
for (const entry of activeEntries()) {
let imageId = null;
try {
imageId = await entry.instance.nearestImage({
lat: view.lat,
lon: view.lon,
});
imageId = await entry.instance.nearestImage(
{ lat: view.lat, lon: view.lon },
{ signal },
);
} catch (error) {
// Overtaken: whatever replaced it owns the panel, errors included.
if (signal.aborted) return false;
// One provider failing (no key, offline) must not hide the others.
lastError = error;
continue;
}
if (signal.aborted) return false;
if (!state.enabled) break;
// This provider was switched off while it looked: try the next.
if (!imageId || !entry.on) continue;
nearestLookup = null; // answered: nothing left to abort
return openImage(entry.def.id, imageId);
}
if (nearestLookup === lookup) nearestLookup = null;
// Switched off while it looked: nothing to open, nothing to report.
if (state.enabled && activeEntries().length)
state.street.error =
@@ -493,6 +517,7 @@ export function createStreetLevelLayer({
},
/** Close the image and deselect it everywhere on the map. */
closeViewer() {
abortNearest();
parts.viewerHost.close();
parts.clearSequences();
},