diff --git a/changelog.d/1117-google-viewer-lifecycle.md b/changelog.d/1117-google-viewer-lifecycle.md new file mode 100644 index 000000000..66d73d4a2 --- /dev/null +++ b/changelog.d/1117-google-viewer-lifecycle.md @@ -0,0 +1,3 @@ +- Release superseded Google Street View open waits and suppress metadata + requests whose viewer was unmounted while the service loaded (rudycelekli, + #1117). diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index f1c04711e..f425d6ebe 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3465,7 +3465,10 @@ controls (the address goes in the caption); a pose goes out at once and again when the panorama's month, photographer and address arrive (one successful lookup per panorama per session, seeded by the nearest lookup; a details lookup that fails or takes over ten seconds waits five seconds -before a view event can retry, and an unmount drops lookups in flight). A key +before a view event can retry, and an unmount drops lookups in flight). Opening +another panorama settles the previous open before registering its wait; closing +then releases the current wait immediately. A details lookup released while its +service loads makes no panorama request after that service becomes available. A key Google refuses (`gm_authFailure`: the API not enabled, a referrer it does not allow, billing off) reads KEY REJECTED and fails a pending open at once; a lookup or open Google never answers fails after 10 or 20 s. In CI diff --git a/src/layers/streetLevel/providers/google/viewer.js b/src/layers/streetLevel/providers/google/viewer.js index 422f52c0c..25e0b08fe 100644 --- a/src/layers/streetLevel/providers/google/viewer.js +++ b/src/layers/streetLevel/providers/google/viewer.js @@ -97,6 +97,8 @@ export function createGoogleViewer({ loader, getService, render } = {}) { service ||= getService ? await getService() : new (await ensureLibrary()).StreetViewService(); + if (!current()) + throw new Error('Street View details lookup was released'); return service.getPanorama({ pano: panoId }); })().then(resolve, reject); }); @@ -236,6 +238,7 @@ export function createGoogleViewer({ loader, getService, render } = {}) { const panoId = String(imageId); if (!host) throw new Error('Street View is not mounted'); if (loader.authFailed()) throw new Error(KEY_REJECTED_MESSAGE); + cancelShown?.(); pendingOpen = panoId; const instance = await ensurePanorama(host); if (pendingOpen !== panoId) return; diff --git a/src/layers/streetLevel/providers/google/viewer.test.mjs b/src/layers/streetLevel/providers/google/viewer.test.mjs index a3d746a34..e1d4a187c 100644 --- a/src/layers/streetLevel/providers/google/viewer.test.mjs +++ b/src/layers/streetLevel/providers/google/viewer.test.mjs @@ -403,3 +403,69 @@ test('a remount looks up again a panorama whose lookup hung', async () => { await settle(); assert.equal(poses.length, count); }); + +test('superseded panorama waits cannot steal the current close cleanup', async (t) => { + t.mock.timers.enable({ apis: ['setTimeout'] }); + const { OPEN_TIMEOUT_MS } = await import('./policy.js'); + const flush = async () => { + for (let i = 0; i < 10; i++) await Promise.resolve(); + }; + const { fake, loader, viewer, host } = setup(); + await viewer.mount(host); + const [panorama] = fake.built; + panorama.setPano = (id) => { + panorama.pano = id; + }; + const first = viewer.open('pano-a'); + await flush(); + const second = viewer.open('pano-b'); + await flush(); + panorama.fire('position_changed'); // new panorama has not reached OK + await flush(); + viewer.close(); + const remaining = loader.listenerCount(); + t.mock.timers.tick(OPEN_TIMEOUT_MS); + await Promise.allSettled([first, second]); + assert.equal( + remaining, + 0, + 'close disposes every pending open without waiting 20 seconds', + ); + assert.equal(panorama.visible, false); +}); + +test('unmount during details-service acquisition makes no later billed lookup', async () => { + const fake = fakeStreetViewLibrary({ panoramas: PANORAMAS }); + const loader = fakeMapsLoader(fake.library); + const { createGoogleProvider } = await import('./index.js'); + let release; + const acquiring = new Promise((resolve) => { + release = resolve; + }); + const importLibrary = loader.importLibrary.bind(loader); + let imports = 0; + loader.importLibrary = async (name) => { + if (++imports > 1) await acquiring; + return importLibrary(name); + }; + const provider = createGoogleProvider({ + getApiKey: () => 'fixture-key', + loader, + }); + const instance = provider.create({ + notify() {}, + actions: { reportError() {} }, + }); + const viewer = instance.viewer; + await viewer.mount(fakeHost()); + await viewer.open('pano-a'); + viewer.unmount(); + release(); + for (let i = 0; i < 10; i++) await Promise.resolve(); + assert.equal( + fake.lookups.length, + 0, + 'teardown invalidates the request before calling getPanorama', + ); + instance.destroy(); +});