From 4682757e71d263c5988f00f7dd209fe680d0b431 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Thu, 8 Oct 2026 16:21:13 +0400 Subject: [PATCH] fix(street-level): a new sequence load drops the last one's error; framing survives a detached navigation Co-Authored-By: Claude Opus 5.5 --- src/layers/streetLevel/cameraFraming.js | 3 ++- src/layers/streetLevel/cameraFraming.test.mjs | 9 +++++++++ src/layers/streetLevel/providers/mapillary/sequences.js | 2 ++ .../streetLevel/providers/mapillary/sequences.test.mjs | 3 ++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/layers/streetLevel/cameraFraming.js b/src/layers/streetLevel/cameraFraming.js index 13246570e..432475411 100644 --- a/src/layers/streetLevel/cameraFraming.js +++ b/src/layers/streetLevel/cameraFraming.js @@ -44,7 +44,8 @@ export function createCameraFraming({ state }) { function frame(ticket) { const { position, bearing, altitude } = state.street; if (!ticket || !position || !state.viewer) return; - if (ticket.generation !== null && !navigation.reassert(ticket.generation)) + // Detached (shell teardown) since the ticket was issued: nothing to frame for. + if (ticket.generation !== null && !navigation?.reassert(ticket.generation)) return; const ground = groundHeightAt(position.lon, position.lat, altitude); // Owned before the call: starting it cancels the previous flight (ours diff --git a/src/layers/streetLevel/cameraFraming.test.mjs b/src/layers/streetLevel/cameraFraming.test.mjs index 22e752627..7adb194a3 100644 --- a/src/layers/streetLevel/cameraFraming.test.mjs +++ b/src/layers/streetLevel/cameraFraming.test.mjs @@ -177,3 +177,12 @@ test('a refused claim (cockpit) does not frame the photo', () => { framing.frame(framing.begin()); assert.equal(flights.length, 0); }); + +test('a photo that loads after the navigation was detached does not frame', () => { + const { framing, state, flights } = setup({ sampled: 160 }); + framing.attachNavigation(navigationFor(state)); + const ticket = framing.begin(); + framing.attachNavigation(null); + assert.doesNotThrow(() => framing.frame(ticket)); + assert.equal(flights.length, 0); +}); diff --git a/src/layers/streetLevel/providers/mapillary/sequences.js b/src/layers/streetLevel/providers/mapillary/sequences.js index 53cfe64e2..e86d5b6f9 100644 --- a/src/layers/streetLevel/providers/mapillary/sequences.js +++ b/src/layers/streetLevel/providers/mapillary/sequences.js @@ -131,6 +131,8 @@ export function createSequences({ state, source, parts }) { const controller = new AbortController(); state.sequence.abort = controller; state.sequence.loading = true; + // An earlier sequence's error does not belong to this one. + state.sequence.error = null; notify(); try { const records = await source.getSequenceImages(sequenceId, { diff --git a/src/layers/streetLevel/providers/mapillary/sequences.test.mjs b/src/layers/streetLevel/providers/mapillary/sequences.test.mjs index 72f25ee45..5e5a1886b 100644 --- a/src/layers/streetLevel/providers/mapillary/sequences.test.mjs +++ b/src/layers/streetLevel/providers/mapillary/sequences.test.mjs @@ -101,9 +101,10 @@ test('a failed sequence load leaves no stale cones and can be retried', async () assert.equal(highlights.at(-1), null); assert.equal(state.sequence.error, 'Sequence images unavailable'); - // Clicking B again asks again. + // Clicking B again asks again, without the old error. sequences.select('B'); assert.equal(source.calls.filter((c) => c.sequenceId === 'B').length, 2); + assert.equal(state.sequence.error, null, 'not shown while B loads again'); source.last('B').resolve(records('B')); await settle(); assert.deepEqual(drawn(), ['B-0', 'B-1']);