Shorten the render failure notice's comments and docs

Comments keep only the why: the loop-running flag, the mid-raise events and
the one-time canvas focus. The CURRENT-STATE section lists behaviour and
pitfalls instead of retelling the code, and no longer says every tileset
event is raised outside Scene's error handling.
This commit is contained in:
German Khokhlov committed 2026-11-13 19:19:15 +04:00
1 parent d42b6fe764
commit df41c0affb
7 files changed
+74 -120

No files matched your search

+20 -35
View File
@@ -4,14 +4,11 @@ import { describeError } from '../app/errors.js';
import { appRootUrl } from '../share/shareLinkPath.js';
/**
* Tell the user when the globe stops drawing, rather than leave a black globe
* behind controls that still respond. Cesium stops its render loop after a
* render error, and a page whose WebGL context was lost never draws again.
*
* Retry restarts the loop, and the notice stays until a frame renders without
* an error; after a context loss Retry reloads the page. Reset opens the app's
* root address. With `allowReload: false`, Reset is left out, and so is Retry
* after a context loss. Returns a function that removes the notice.
* 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.
* Returns a function that removes the notice.
*/
export function installRenderFailureNotice(
viewer,
@@ -27,8 +24,8 @@ export function installRenderFailureNotice(
const canvas = viewer.canvas ?? scene.canvas;
let notice = null;
let contextLost = false;
// The error Scene raised as a render error and, with rethrowRenderErrors,
// throws on: the render() wrapper must not raise it a second time.
// With rethrowRenderErrors, Scene throws the error it has just raised;
// the render() wrapper must not raise it again.
let raisedByScene = null;
let frameFailed = false;
let removeFrameListeners = null;
@@ -104,14 +101,10 @@ export function installRenderFailureNotice(
};
const removeRenderError = scene.renderError.addEventListener(onRenderError);
// An error thrown outside Scene's own error handling (a preRender or
// postRender listener, the clock's onTick, a resize) also stops Cesium's
// render loop, but raises no renderError, and Cesium reports it only in the
// panel the app turns off. Raise it as a render error, so Cesium's handler,
// an inline panel's timer and this notice all see it. Such an error passes
// here before anything else runs, so finish the raises of frameEvents() it
// cut short first: listeners added or removed while the loop is stopped
// then apply at once, in order.
// An error thrown outside Scene's error handling (a frame listener, onTick,
// resize) stops the loop without a renderError. Raise one, so Cesium, an
// inline panel's timer and this notice all see it. First finish the event
// raises it cut short, so listeners added or removed meanwhile apply.
const unguard = ['resize', 'render'].map((name) =>
reportThrown(viewer.cesiumWidget, name, (error) => {
frameEvents(viewer).forEach(finishRaise);
@@ -130,11 +123,8 @@ export function installRenderFailureNotice(
}
/**
* The events Cesium raises in a frame outside Scene's own error handling, so
* that a listener's error cuts their raise short: Scene's frame events, the
* camera's and the clock's, and those raised after the frame renders, the
* globe's tile load progress and the load events of the scene's tilesets.
* Tilesets nested in a collection are not reached.
* Events Cesium raises in a frame outside Scene's error handling, which a
* listener's error can leave mid-raise. Only top-level tilesets are reached.
*/
function frameEvents(viewer) {
const { scene } = viewer;
@@ -164,10 +154,8 @@ function frameEvents(viewer) {
}
/**
* Finish the raise a throwing listener cut short. Cesium's `Event.raiseEvent`
* marks the event as raising until every listener returns, and until then it
* only queues the listeners added or removed. Apply the queue as the raise
* would have, so listeners added or removed from here on take effect at once.
* Finish a raise a throwing listener cut short. Until a raise ends, Cesium's
* `Event` only queues added and removed listeners; apply that queue.
*/
function finishRaise(event) {
if (!event?._invokingListeners) return;
@@ -187,15 +175,12 @@ function finishRaise(event) {
}
/**
* Report an error the widget's `name` method throws, and throw it on.
* Report an error the widget's `name` method throws, and rethrow it.
*
* A render error marks Cesium's render loop stopped, but a call made while
* the loop ran leaves the loop's next animation frame queued: the loop queues
* it after a call that returns, and a frame an inline panel's timer draws
* leaves it queued. That frame stops the loop itself, and a Retry or a shown
* tab that comes first must not start a second loop beside it, so the loop
* stays marked running. A call from the loop that throws is still marked
* stopped by the loop's own catch.
* A render error marks the loop stopped, but a call made while it ran leaves
* its next frame queued, and that frame stops it. Keep the loop marked
* running, so a Retry or a shown tab before that frame starts no second loop.
* After a throw, the loop's own catch still marks it stopped.
*/
function reportThrown(widget, name, report) {
const original = widget?.[name];
+3 -5
View File
@@ -1,8 +1,6 @@
// Retry after an error thrown by a scene event listener. Cesium's
// Event.raiseEvent has no try/finally: a throwing listener leaves the event
// with _invokingListeners = true, so listeners added or removed afterwards are
// only queued (_toAdd/_toRemove) until the next raise of that event completes.
// Drives Cesium's real Event and startRenderLoop (CesiumWidget.prototype).
// Retry after a scene event listener throws. Cesium's Event.raiseEvent has no
// try/finally, so the event stays mid-raise and only queues listener changes.
// Drives Cesium's real Event and startRenderLoop.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
+2 -4
View File
@@ -1,7 +1,5 @@
// Errors thrown in the render loop outside Scene's tryAndCatchError (a
// preRender/postRender listener, clock.onTick, resize). Drives Cesium's real
// startRenderLoop through the CesiumWidget.prototype.useDefaultRenderLoop
// setter.
// Errors thrown in the render loop outside Scene's tryAndCatchError. Drives
// Cesium's real startRenderLoop through the useDefaultRenderLoop setter.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
+3 -6
View File
@@ -1,9 +1,6 @@
// Retry after an error Scene catches itself (Scene.tryAndCatchError raises
// renderError and returns). CesiumWidget's renderError handler marks the loop
// stopped, but startRenderLoop still queues the loop's next animation frame.
// A Retry that lands before that frame runs must not start a second loop.
// Drives Cesium's real startRenderLoop and useDefaultRenderLoop setter
// (CesiumWidget.prototype).
// Retry after an error Scene catches itself: the loop is marked stopped but
// its next frame is still queued, so an early Retry must not start a second
// loop. Drives Cesium's real startRenderLoop and useDefaultRenderLoop setter.
import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
+1 -2
View File
@@ -1,5 +1,4 @@
// src/ui/renderFailureView.test.mjs — the scene chrome renders the render
// failure notice installRenderFailureNotice() publishes, with its actions.
// The scene chrome renders the render failure notice and its actions.
import '../testSupport/registerDom.mjs';
import test from 'node:test';
import assert from 'node:assert/strict';
+7 -11
View File
@@ -5,8 +5,7 @@ import { msg, t } from '../../../i18n/index.js';
import { useStatusStore } from '../../stores/status.js';
import UiButton from '../../kit/UiButton.vue';
// The notice installRenderFailureNotice() (src/ui/renderFailure.js) publishes
// while the globe cannot draw, with the actions it offers.
// Published by installRenderFailureNotice() (src/ui/renderFailure.js).
const { renderFailure: failure } = storeToRefs(useStatusStore());
const panel = ref(null);
const MESSAGES = {
@@ -17,10 +16,8 @@ const MESSAGES = {
'Rendering stopped after an error. Layers and controls still work, but the map is not updating.',
),
};
// Focus moves to the first action each time the notice appears, so a
// keyboard user can act on it at once. When the globe draws again, it goes
// back where it was, or to the map if that cannot take it, unless the user
// has moved it since.
// Focus goes to the first action when the notice appears. When the globe
// draws again, it goes back, or to the map, unless the user moved it.
let focusBefore;
watch(
() => Boolean(failure.value),
@@ -39,7 +36,7 @@ watch(
if (active && active !== document.body) return;
if (previous?.isConnected && previous !== document.body) {
previous.focus({ preventScroll: true });
// A control disabled, hidden or made inert since does not take it.
// A control disabled, hidden or made inert meanwhile does not take it.
if (document.activeElement === previous) return;
}
focusMap();
@@ -47,10 +44,9 @@ watch(
{ immediate: true },
);
// The globe's canvas, which the map's keys (Enter to finish a drawing, Space
// to talk) read as the map. It takes focus only this once: when focus moves
// on, or the globe is pressed, it goes back to a canvas that cannot take
// focus, and a press leaves focus on the page, as Cesium has it.
// The map's keys (Enter to finish a drawing, Space to talk) read the canvas
// as the map. It stays focusable only until focus leaves or the globe is
// pressed; a press leaves focus on the page, as in Cesium.
function focusMap() {
const canvas = document.querySelector(
'#cesiumContainer .cesium-widget canvas',