diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index a2fa3502d..07df2ea24 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -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 diff --git a/src/app/embed.js b/src/app/embed.js index fb608d365..32541f85a 100644 --- a/src/app/embed.js +++ b/src/app/embed.js @@ -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?.(); }; } diff --git a/src/app/embed.test.mjs b/src/app/embed.test.mjs index cee064278..e4e8a1d22 100644 --- a/src/app/embed.test.mjs +++ b/src/app/embed.test.mjs @@ -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 () => { diff --git a/src/ui/renderFailure.js b/src/ui/renderFailure.js index 7600a8dc7..b593f57f4 100644 --- a/src/ui/renderFailure.js +++ b/src/ui/renderFailure.js @@ -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; diff --git a/src/ui/renderFailure.test.mjs b/src/ui/renderFailure.test.mjs index 43f83dff2..913348740 100644 --- a/src/ui/renderFailure.test.mjs +++ b/src/ui/renderFailure.test.mjs @@ -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) => {