fix(street-level): release superseded and unmounted Google viewer work (#1117)

A superseded open wait cleared the shared cancelShown slot that belonged to the newer wait, leaving its auth listener and timer alive after close; open() now settles the previous wait first. A lookup whose viewer unmounted while the Street View service loaded still sent a billed getPanorama; describe() now rechecks the lookup after acquisition.
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-10-16 15:34:26 -07:00
1 parent 3c3ea269a9
commit fb10889841
4 files changed
+76 -1

No files matched your search

@@ -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).
+4 -1
View File
@@ -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
@@ -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;
@@ -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();
});