mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Let Retry finish a frame event a throwing listener cut short
Cesium's Event.raiseEvent leaves an event mid-raise when a listener throws, so listeners added or removed afterwards stay queued. Retry now applies that queue for the frame and clock events: the notice's frame watchers see the next frame, and a listener removed since, such as a layer turned off, is not called again.
This commit is contained in:
1 parent
628de8386d
commit
02a8442e6e
4 files changed
+175
No files matched your search
@@ -1599,6 +1599,11 @@ render error before Cesium's loop handles it as before.
|
|||||||
- **Retry** after a render error restarts the render loop. The notice stays
|
- **Retry** after a render error restarts the render loop. The notice stays
|
||||||
until a frame starts and ends without a render error (Cesium raises
|
until a frame starts and ends without a render error (Cesium raises
|
||||||
`postRender` after a failed frame too), and a loop that fails again keeps it.
|
`postRender` after a failed frame too), and a loop that fails again keeps it.
|
||||||
|
A listener that throws leaves its Cesium event mid-raise, with listeners
|
||||||
|
added or removed since only queued, so Retry first applies the queue of the
|
||||||
|
frame and clock events (`finishRaise()`): the notice's frame watchers start
|
||||||
|
with the next frame, and a listener removed since, for a layer turned off,
|
||||||
|
is not called again.
|
||||||
- **Reload** after a lost context reloads the page: Cesium cannot draw into a
|
- **Reload** after a lost context reloads the page: Cesium cannot draw into a
|
||||||
context it has lost, and the address keeps the view.
|
context it has lost, and the address keeps the view.
|
||||||
- **Reset view** opens the app's root address (`appRootUrl()`), without the
|
- **Reset view** opens the app's root address (`appRootUrl()`), without the
|
||||||
|
|||||||
@@ -548,6 +548,7 @@
|
|||||||
"src/ui/renderFailure.test.mjs",
|
"src/ui/renderFailure.test.mjs",
|
||||||
"src/ui/renderFailureView.test.mjs",
|
"src/ui/renderFailureView.test.mjs",
|
||||||
"src/ui/renderFailureLoopErrors.test.mjs",
|
"src/ui/renderFailureLoopErrors.test.mjs",
|
||||||
|
"src/ui/renderFailureEventRetry.test.mjs",
|
||||||
"src/app/renderFailureInlineRetry.test.mjs",
|
"src/app/renderFailureInlineRetry.test.mjs",
|
||||||
"src/layers/cctv/lifecycle.js",
|
"src/layers/cctv/lifecycle.js",
|
||||||
"src/layers/cctv/controls.js",
|
"src/layers/cctv/controls.js",
|
||||||
|
|||||||
@@ -42,6 +42,17 @@ export function installRenderFailureNotice(
|
|||||||
locationRef.reload();
|
locationRef.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
[
|
||||||
|
scene.preUpdate,
|
||||||
|
scene.postUpdate,
|
||||||
|
scene.preRender,
|
||||||
|
scene.postRender,
|
||||||
|
scene.camera?.changed,
|
||||||
|
scene.camera?.moveStart,
|
||||||
|
scene.camera?.moveEnd,
|
||||||
|
viewer.clock?.onTick,
|
||||||
|
viewer.clock?.onStop,
|
||||||
|
].forEach(finishRaise);
|
||||||
viewer.useDefaultRenderLoop = true;
|
viewer.useDefaultRenderLoop = true;
|
||||||
scene.requestRender?.();
|
scene.requestRender?.();
|
||||||
}
|
}
|
||||||
@@ -124,6 +135,30 @@ export function installRenderFailureNotice(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: the frame watchers the notice
|
||||||
|
* adds, or the throwing listener of a layer turned off since. Apply the queue
|
||||||
|
* as the raise would have.
|
||||||
|
*/
|
||||||
|
function finishRaise(event) {
|
||||||
|
if (!event?._invokingListeners) return;
|
||||||
|
event._invokingListeners = false;
|
||||||
|
for (const [listener, scopes] of event._toAdd) {
|
||||||
|
const added = event._listeners.get(listener) ?? new Set();
|
||||||
|
scopes.forEach((scope) => added.add(scope));
|
||||||
|
event._listeners.set(listener, added);
|
||||||
|
}
|
||||||
|
event._toAdd.clear();
|
||||||
|
for (const [listener, scopes] of event._toRemove) {
|
||||||
|
const left = event._listeners.get(listener);
|
||||||
|
scopes.forEach((scope) => left?.delete(scope));
|
||||||
|
if (left?.size === 0) event._listeners.delete(listener);
|
||||||
|
}
|
||||||
|
event._toRemove.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrap `target[name]` so an error it throws is reported and thrown on, for
|
* Wrap `target[name]` so an error it throws is reported and thrown on, for
|
||||||
* the caller to handle as before. Returns a function that unwraps it.
|
* the caller to handle as before. Returns a function that unwraps it.
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
// 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).
|
||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import * as Cesium from 'cesium';
|
||||||
|
import { installRenderFailureNotice } from './renderFailure.js';
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
const frames = [];
|
||||||
|
const previousRaf = globalThis.requestAnimationFrame;
|
||||||
|
globalThis.requestAnimationFrame = (fn) => frames.push(fn);
|
||||||
|
// Scene.render as in Scene.js:4369: preUpdate always, preRender/postRender
|
||||||
|
// only when a frame is rendered (requestRenderMode idle skips them).
|
||||||
|
const scene = {
|
||||||
|
renderError: new Cesium.Event(),
|
||||||
|
preUpdate: new Cesium.Event(),
|
||||||
|
preRender: new Cesium.Event(),
|
||||||
|
postRender: new Cesium.Event(),
|
||||||
|
requestRenderMode: true,
|
||||||
|
_renderRequested: false,
|
||||||
|
rendered: 0,
|
||||||
|
requestRender() {
|
||||||
|
this._renderRequested = true;
|
||||||
|
},
|
||||||
|
initializeFrame() {},
|
||||||
|
render(time) {
|
||||||
|
this.preUpdate.raiseEvent(this, time);
|
||||||
|
const shouldRender = !this.requestRenderMode || this._renderRequested;
|
||||||
|
if (!shouldRender) return;
|
||||||
|
this._renderRequested = false;
|
||||||
|
this.preRender.raiseEvent(this, time);
|
||||||
|
this.rendered += 1;
|
||||||
|
this.postRender.raiseEvent(this, time);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const widget = Object.create(Cesium.CesiumWidget.prototype);
|
||||||
|
Object.assign(widget, {
|
||||||
|
_scene: scene,
|
||||||
|
_clock: { tick() {} },
|
||||||
|
_canRender: true,
|
||||||
|
_useDefaultRenderLoop: false,
|
||||||
|
_renderLoopRunning: false,
|
||||||
|
_showRenderLoopErrors: false,
|
||||||
|
_targetFrameRate: undefined,
|
||||||
|
isDestroyed: () => false,
|
||||||
|
resize() {},
|
||||||
|
});
|
||||||
|
scene.renderError.addEventListener(() => {
|
||||||
|
widget._useDefaultRenderLoop = false;
|
||||||
|
widget._renderLoopRunning = false;
|
||||||
|
});
|
||||||
|
const viewer = {
|
||||||
|
scene,
|
||||||
|
cesiumWidget: widget,
|
||||||
|
canvas: { addEventListener() {}, removeEventListener() {} },
|
||||||
|
get useDefaultRenderLoop() {
|
||||||
|
return widget.useDefaultRenderLoop;
|
||||||
|
},
|
||||||
|
set useDefaultRenderLoop(value) {
|
||||||
|
widget.useDefaultRenderLoop = value;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const store = {
|
||||||
|
renderFailure: null,
|
||||||
|
publishRenderFailure(next) {
|
||||||
|
this.renderFailure = next;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const uninstall = installRenderFailureNotice(viewer, {
|
||||||
|
store,
|
||||||
|
locationRef: { href: 'https://gev.example/', reload() {}, assign() {} },
|
||||||
|
logError() {},
|
||||||
|
});
|
||||||
|
const runFrames = (n) => {
|
||||||
|
for (let i = 0; i < n; i += 1)
|
||||||
|
for (const fn of frames.splice(0)) fn(16 * (i + 1));
|
||||||
|
};
|
||||||
|
const restore = () => {
|
||||||
|
uninstall();
|
||||||
|
globalThis.requestAnimationFrame = previousRaf;
|
||||||
|
};
|
||||||
|
return { frames, scene, widget, viewer, store, runFrames, restore };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const eventName of ['postRender', 'preUpdate']) {
|
||||||
|
test(`idle: a ${eventName} listener that threw once; a successful Retry clears the notice`, () => {
|
||||||
|
const { scene, viewer, store, runFrames, restore } = setup();
|
||||||
|
try {
|
||||||
|
let broken = true;
|
||||||
|
scene[eventName].addEventListener(() => {
|
||||||
|
if (broken) throw new TypeError('listener failed');
|
||||||
|
});
|
||||||
|
scene.requestRender();
|
||||||
|
viewer.useDefaultRenderLoop = true;
|
||||||
|
runFrames(1);
|
||||||
|
assert.equal(store.renderFailure?.reason, 'render-error');
|
||||||
|
assert.equal(viewer.useDefaultRenderLoop, false);
|
||||||
|
broken = false; // the cause goes away
|
||||||
|
store.renderFailure.retry();
|
||||||
|
const before = scene.rendered;
|
||||||
|
runFrames(10); // idle: no camera move, no render request
|
||||||
|
assert.ok(scene.rendered > before, 'the globe drew again');
|
||||||
|
assert.equal(store.renderFailure, null, 'the notice is cleared');
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a throwing postRender listener removed before Retry is not called again', () => {
|
||||||
|
const { scene, viewer, store, runFrames, restore } = setup();
|
||||||
|
try {
|
||||||
|
let calls = 0;
|
||||||
|
const remove = scene.postRender.addEventListener(() => {
|
||||||
|
calls += 1;
|
||||||
|
throw new TypeError('layer listener failed');
|
||||||
|
});
|
||||||
|
scene.requestRender();
|
||||||
|
viewer.useDefaultRenderLoop = true;
|
||||||
|
runFrames(1);
|
||||||
|
assert.equal(store.renderFailure?.reason, 'render-error');
|
||||||
|
remove(); // the user turns the layer off
|
||||||
|
store.renderFailure.retry();
|
||||||
|
runFrames(5);
|
||||||
|
assert.equal(calls, 1, 'the removed listener ran again after Retry');
|
||||||
|
assert.equal(viewer.useDefaultRenderLoop, true, 'the loop keeps running');
|
||||||
|
assert.equal(store.renderFailure, null, 'the notice is cleared');
|
||||||
|
} finally {
|
||||||
|
restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user