fix: release weather camera listeners while disabled

This commit is contained in:
Said committed 2026-10-20 14:01:22 +01:00
1 parent 2f23bbe87f
commit d6f89e3cfd
5 files changed
+49 -8

No files matched your search

+1
View File
@@ -1 +1,2 @@
- Preserve enable during first-load replacement (asturksever, #1275). - Preserve enable during first-load replacement (asturksever, #1275).
- Release disabled weather camera listeners and make teardown resource leaks fail the browser probe (asturksever, #1275).
+1
View File
@@ -253,6 +253,7 @@ catalog construction owns each instance.
Wind animates one forecast without advancing forecast time. Separate Weather Wind animates one forecast without advancing forecast time. Separate Weather
observation layers provide radar and satellite history; none claims measured cloud volume. observation layers provide radar and satellite history; none claims measured cloud volume.
Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work.
Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each
observed product is instead a raised, translucent shell: one rectangle over the observed product is instead a raised, translucent shell: one rectangle over the
product bounds at a fixed height (global infrared 5.5 km, regional infrared product bounds at a fixed height (global infrared 5.5 km, regional infrared
+24 -4
View File
@@ -141,7 +141,7 @@ const snapshot = (page) =>
), ),
}, },
weather, weather,
windCells: wind.gpu?.pathCount ?? wind.paths ?? null, windCells: wind.gpu?.pathCount ?? wind.paths ?? 0,
enabled: [...dm.layers].filter(([, e]) => e.enabled).map(([id]) => id), enabled: [...dm.layers].filter(([, e]) => e.enabled).map(([id]) => id),
}; };
}); });
@@ -220,7 +220,7 @@ try {
const baseline = await snapshot(a.page); const baseline = await snapshot(a.page);
console.log('baseline', JSON.stringify(baseline)); console.log('baseline', JSON.stringify(baseline));
for (const id of WEATHER) await setLayer(a.page, id, true); for (const id of WEATHER) await setLayer(a.page, id, true);
await a.page const weatherReady = await a.page
.waitForFunction( .waitForFunction(
() => () =>
['weather-radar', 'weather-satellite', 'weather-lightning'].every( ['weather-radar', 'weather-satellite', 'weather-lightning'].every(
@@ -233,7 +233,13 @@ try {
), ),
{ timeout: 120_000 }, { timeout: 120_000 },
) )
.catch(() => console.log(' !! weather not ready')); .then(
() => true,
() => false,
);
results.weatherReady = weatherReady;
if (!weatherReady)
console.log(' !! weather not ready — feature coverage incomplete');
await sleep(2000); await sleep(2000);
// exercise: history playback, wind reading, storm selection, product switch // exercise: history playback, wind reading, storm selection, product switch
await a.page.evaluate(() => { await a.page.evaluate(() => {
@@ -331,11 +337,25 @@ try {
'errors (control run):', 'errors (control run):',
results.errorsB.length ? results.errorsB : 'none', results.errorsB.length ? results.errorsB : 'none',
); );
// Resource ownership is an acceptance check; provider availability remains
// separately reported diagnostic evidence in both runs.
if (
!weatherReady ||
Object.keys(leak).length ||
[...a.errors, ...b.errors].some((e) => e.startsWith('pageerror:'))
) {
process.exitCode = 1;
}
} catch (error) {
results.failure = { name: error.name, message: error.message };
throw error;
} finally {
// Preserve completed resource evidence even if a later provider or browser
// phase aborts. A partial run still exits nonzero.
writeFileSync( writeFileSync(
path.join(outDir, 'results.json'), path.join(outDir, 'results.json'),
JSON.stringify(results, null, 2), JSON.stringify(results, null, 2),
); );
console.log(`wrote ${path.join(outDir, 'results.json')}`); console.log(`wrote ${path.join(outDir, 'results.json')}`);
} finally {
await browser.close(); await browser.close();
} }
+8 -4
View File
@@ -296,10 +296,6 @@ export function createWeatherLayer({
motion?.addEventListener?.('change', onVisibility); motion?.addEventListener?.('change', onVisibility);
documentRef?.addEventListener?.('visibilitychange', onVisibility); documentRef?.addEventListener?.('visibilitychange', onVisibility);
registerClock(); registerClock();
removeCamera = viewer.camera.moveEnd?.addEventListener(() => {
checkHost();
if (enabled) notify();
});
}, },
attachShellServices(services) { attachShellServices(services) {
imageryHost = imageryHost =
@@ -315,10 +311,18 @@ export function createWeatherLayer({
}, },
enable() { enable() {
enabled = true; enabled = true;
if (!removeCamera) {
removeCamera = viewer.camera.moveEnd?.addEventListener(() => {
checkHost();
notify();
});
}
registerClock(); registerClock();
}, },
disable() { disable() {
enabled = false; enabled = false;
removeCamera?.();
removeCamera = null;
unregisterClock?.(); unregisterClock?.();
unregisterClock = null; unregisterClock = null;
++generation; ++generation;
+15
View File
@@ -2341,3 +2341,18 @@ test('a product replacement accepts supersession but never paints the obsolete f
assert.equal(h.layer.getParams().product, 'clouds'); assert.equal(h.layer.getParams().product, 'clouds');
h.layer.destroy(); h.layer.destroy();
}); });
test('disabled weather releases its camera listener and re-enabling binds only once', () => {
const h = layerHarness();
h.layer.disable();
assert.equal(h.moveEnd.size, 0, 'disabled layers own no active camera work');
h.layer.enable();
h.layer.enable();
assert.equal(h.moveEnd.size, 1);
h.layer.disable();
assert.equal(h.moveEnd.size, 0);
h.layer.enable();
assert.equal(h.moveEnd.size, 1);
h.layer.destroy();
assert.equal(h.moveEnd.size, 0);
});