Show no notice for a lost context on a page that must not reload, and log each render error once

An inline panel that lost its WebGL context showed the panel runtime's own
notice and, over it, an alert dialog with no buttons that it could not act
on. With allowReload false a lost context now clears the notice and shows
none, as the docs already said. keepPanelRendering no longer logs render
errors itself: the render failure notice logs each one. Name the Cesium
private fields finishRaise and reportThrown rely on, and the version they
were checked against.
This commit is contained in:
Sameh Khamis committed 2026-11-13 18:16:07 -08:00
1 parent 73bd952736
commit 9d01f8df77
5 files changed
+40 -58

No files matched your search

+6 -4
View File
@@ -2482,10 +2482,12 @@ Pitfalls:
its buttons are 44 px tall on touch screens.
An inline conversation panel never reloads or navigates its host's page: it
offers Retry after a render error only, and nothing after a lost context, where
the panel runtime's own notice applies. Its timer (`keepPanelRendering()`)
draws only while the render loop is on. An error a timer frame throws is caught
there: it has already been raised as a render error.
offers Retry after a render error only, and no notice after a lost context,
where the panel runtime's own notice applies (`src/app/globePanelRuntime.js`).
Its timer (`keepPanelRendering()`) draws only while the render loop is on and
logs nothing itself, so each render error is logged once, by the notice. An
error a timer frame throws is caught there: it has already been raised as a
render error.
## Map Source control ownership
+2 -21
View File
@@ -94,17 +94,6 @@ export function panelAwareVisibility(doc = globalThis.document) {
const PANEL_FRAME_MS = 33;
const MISSED_FRAMES_MS = 250;
/** A render error's details, including the plain objects workers report. */
function describeError(error) {
if (error instanceof Error)
return `${error.name}: ${error.message}\n${error.stack}`;
try {
return JSON.stringify(error, Object.getOwnPropertyNames(error ?? {}));
} catch {
return String(error);
}
}
/**
* Keep the globe drawing in an inline panel. Some hosts report a panel on
* screen as hidden, which stops the browser's animation frames and with them
@@ -123,15 +112,8 @@ export function keepPanelRendering(
};
frameRequest = windowRef.requestAnimationFrame(onFrame);
// Cesium stops its render loop after a render error, and the timer stops
// with it; Retry restarts both. Report the error in full: workers report
// plain objects, which Cesium prints as [object Object].
const removeErrorListener = viewer.scene?.renderError?.addEventListener(
(_scene, error) =>
console.error(
"[God's Eye View panel] render error:",
describeError(error),
),
);
// with it; Retry restarts both. The render failure notice logs the error
// (src/ui/renderFailure.js).
const timer = windowRef.setInterval(() => {
if (
viewer.useDefaultRenderLoop === false ||
@@ -150,7 +132,6 @@ export function keepPanelRendering(
return () => {
windowRef.clearInterval(timer);
windowRef.cancelAnimationFrame(frameRequest);
removeErrorListener?.();
};
}
+14 -29
View File
@@ -457,8 +457,7 @@ test('a panel keeps drawing from a timer while animation frames stop', () => {
assert.equal(frame, null);
});
test('a panel reports a render error in full and stops drawing', () => {
let listener = null;
test('a panel stops drawing while its render loop is stopped', () => {
let tick = null;
const windowRef = {
requestAnimationFrame: () => 1,
@@ -467,33 +466,19 @@ test('a panel reports a render error in full and stops drawing', () => {
clearInterval() {},
};
const drawn = [];
const viewer = {
resize() {},
render: () => drawn.push('render'),
scene: {
renderError: {
addEventListener: (fn) => ((listener = fn), () => (listener = null)),
},
},
};
const logged = [];
const original = console.error;
console.error = (...args) => logged.push(args.join(' '));
try {
let clock = 0;
const stop = keepPanelRendering(viewer, { windowRef, now: () => clock });
// Cesium stops its render loop after a render error.
viewer.useDefaultRenderLoop = false;
listener(viewer.scene, { name: 'RuntimeError', message: 'worker failed' });
assert.match(logged[0], /render error: .*"message":"worker failed"/);
clock = 1000;
tick();
assert.deepEqual(drawn, []);
stop();
assert.equal(listener, null);
} finally {
console.error = original;
}
const viewer = { resize() {}, render: () => drawn.push('render') };
let clock = 0;
const stop = keepPanelRendering(viewer, { windowRef, now: () => clock });
// Cesium stops its render loop after a render error.
viewer.useDefaultRenderLoop = false;
clock = 1000;
tick();
assert.deepEqual(drawn, []);
// Retry restarts it, and the timer draws again.
viewer.useDefaultRenderLoop = true;
tick();
assert.deepEqual(drawn, ['render']);
stop();
});
test('a followed aircraft owns the camera, and cockpit view is entered after it', async () => {
+13 -2
View File
@@ -7,7 +7,8 @@ import { appRootUrl } from '../share/shareLinkPath.js';
* Show a recoverable notice when the globe stops drawing, instead of a black
* globe behind working controls. Retry restarts the render loop, or reloads
* after a lost WebGL context; Reset opens the app's root address. With
* `allowReload: false`, Reset is dropped, and so is Retry after a context loss.
* `allowReload: false`, Reset is dropped and a lost context shows no notice:
* only a reload draws again, and the page says so itself.
* Returns a function that removes the notice.
*/
export function installRenderFailureNotice(
@@ -78,7 +79,7 @@ export function installRenderFailureNotice(
notice = {
reason,
detail,
retry: contextLost && !allowReload ? null : retry,
retry,
reset: allowReload ? reset : null,
};
store.publishRenderFailure(notice);
@@ -97,6 +98,12 @@ export function installRenderFailureNotice(
contextLost = true;
// No frame can clear this one; only a reload draws again.
stopWatchingFrames();
// A page that must not reload has nothing to offer: an inline panel
// shows its own lost-context notice (src/app/globePanelRuntime.js).
if (!allowReload) {
clear();
return;
}
present('');
};
@@ -155,6 +162,10 @@ function frameEvents(viewer) {
/**
* Finish a raise a throwing listener cut short. Until a raise ends, Cesium's
* `Event` only queues added and removed listeners; apply that queue.
* These are Cesium's private fields (`_invokingListeners`, `_toAdd`,
* `_toRemove`, `_listeners`, and the widget's `_renderLoopRunning`), checked
* against cesium 1.138 / @cesium/engine 22.3.0; the render loop fixture tests
* drive the real classes and fail when an upgrade renames them.
*/
function finishRaise(event) {
if (!event?._invokingListeners) return;
+5 -2
View File
@@ -156,9 +156,12 @@ test('a page that must not reload offers neither reload nor reset', (t) => {
f.frame(new Error('boom'));
assert.equal(typeof f.store.renderFailure.retry, 'function');
assert.equal(f.store.renderFailure.reset, null);
// Only a reload draws again, which this page must not offer: the notice
// goes, and the page's own lost-context notice speaks instead.
f.loseContext();
assert.equal(f.store.renderFailure.retry, null);
assert.equal(f.store.renderFailure.reset, null);
assert.equal(f.store.renderFailure, null);
f.frame(new Error('Expected width to be greater than 0'));
assert.equal(f.store.renderFailure, null);
});
test('uninstall removes the listeners and the notice', (t) => {