refactor(live): dedupe _liveTestSeams and consolidate destroy() (#1514 S4+S8)

S4 — window._liveTestSeams was defined in two places. The earlier block
(near the test-seam exposure for #1207) wires wake -> wakeCanvasEngine
which respects the pause/empty-queue guards. The trailing block at the
bottom of the IIFE installed a separate wake that bypassed those guards,
so a test could wake an empty engine and burn rAF cycles on nothing.
Delete the trailing block; single source of truth at the earlier site.

S8 — destroy() had two cleanup blocks: one at the top (animation flags
+ stopReplay) and a duplicate at the bottom (canvas DOM + DPR MQL).
Functionally harmless but confusing. Consolidate into one ordered
teardown: drain animations -> stop loops -> cancel timers/WS -> tear
down canvas+DPR before nulling map -> reset module state. Documented
inline why the canvas teardown must precede map.remove().

Refs #1514
This commit is contained in:
OpenClaw Bot
2026-05-31 22:45:45 +00:00
parent 498a2dcbb5
commit a568c3613e
+22 -24
View File
@@ -4043,6 +4043,23 @@
regionFilterChangeHandler = null;
}
if (map) { map.remove(); map = null; }
// #1514 S8 — canvas teardown moved up here from a duplicate trailing block.
// Order matters: clear+detach the canvas BEFORE we null out the map (which
// already removed our pane) so we don't call clearRect on a context whose
// backing pane is gone. Also drop the DPR MQL listener so a late
// resolution change after destroy() can't fire updateAnimCanvas() against
// a null map.
if (animCtx && animCanvas) {
try { animCtx.clearRect(0, 0, animCanvas.clientWidth, animCanvas.clientHeight); } catch (_) {}
animCanvas.remove();
animCanvas = null;
animCtx = null;
}
if (_dprMedia && _dprChangeHandler) {
try { _dprMedia.removeEventListener('change', _dprChangeHandler); } catch (_) {}
_dprMedia = null;
_dprChangeHandler = null;
}
if (_onResize) {
window.removeEventListener('resize', _onResize);
window.removeEventListener('orientationchange', _onResize);
@@ -4076,23 +4093,6 @@
nodeActivity = {}; pktTimestamps = [];
feedDedup.clear();
VCR.buffer = []; VCR.playhead = -1; VCR.mode = 'LIVE'; VCR.missedCount = 0; VCR.speed = _initialSpeed; VCR.replayGen = 0;
// CLEANUP: Kill the canvas loop, dump the queue, and clear the screen
activeAnimations.length = 0;
activeFades.length = 0;
isAnimating = false;
isFading = false;
if (animCtx && animCanvas) {
animCtx.clearRect(0, 0, animCanvas.clientWidth, animCanvas.clientHeight);
animCanvas.remove();
animCanvas = null;
animCtx = null;
}
if (_dprMedia && _dprChangeHandler) {
_dprMedia.removeEventListener('change', _dprChangeHandler);
_dprMedia = null;
_dprChangeHandler = null;
}
}
let _themeRefreshHandler = null;
@@ -4104,13 +4104,11 @@
// across re-mounts. window.__liveMQLBindCount is a debug seam consumed by
// test-live-mql-leak-1180-e2e.js and otherwise unused.
var _liveNarrowMqlBound = false;
window._liveTestSeams = window._liveTestSeams || {};
window._liveTestSeams.wake = function() {
if (!isAnimating) {
isAnimating = true;
requestAnimationFrame(renderAnimations);
}
};
// #1514 S4 — single source of truth for window._liveTestSeams is at the
// earlier exposure block (search for `window._liveTestSeams = {`). The
// duplicate definition that lived here previously added a `_liveTestSeams.wake`
// that bypassed pause/empty-queue guards; tests must use the production
// `wakeCanvasEngine` exposed there.
registerPage('live', {
init: function(app, routeParam) {