fix(live): replace fragile DPR listener self-rebind with race-free pattern (#1514 M1)

The previous DPR change handler removed and re-added itself inside a
try/finally block. Two problems:

1. A throw inside updateAnimCanvas() would still trigger the re-bind, but
   leave the canvas in a half-resized state.
2. The remove → matchMedia() → add sequence is not atomic; a synchronous
   DPR change between remove and add would be silently dropped.

Switch to addEventListener with {once: true}: the runtime drops the
listener atomically before our handler runs, so re-binding is race-free
and a thrown updateAnimCanvas() doesn't double-bind.

Refs #1514
This commit is contained in:
OpenClaw Bot
2026-05-31 22:44:22 +00:00
parent 976ccf6db5
commit 0d32f06300
+20 -12
View File
@@ -1258,19 +1258,27 @@
map.on('moveend zoomend resize', updateAnimCanvas);
updateAnimCanvas();
_dprChangeHandler = () => {
try {
updateAnimCanvas();
} finally {
if (_dprMedia) {
_dprMedia.removeEventListener('change', _dprChangeHandler);
}
_dprMedia = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
_dprMedia.addEventListener('change', _dprChangeHandler);
// #1514 M1+S10 — DPR change handling.
//
// matchMedia(`(resolution: ${dpr}dppx)`) is a STRICT-MATCH query: it only
// fires when the current DPR stops matching. After it fires, we must rebind
// a new MQL keyed to the new DPR. Older code did remove → re-add inside a
// try/finally which was fragile (a throw in updateAnimCanvas would still
// re-bind, and a synchronous re-entry between remove and add could lose
// the handler). The {once: true} pattern below removes the listener
// atomically before our handler runs, so re-binding is race-free.
function _rebindDPRListener() {
if (_dprMedia && _dprChangeHandler) {
try { _dprMedia.removeEventListener('change', _dprChangeHandler); } catch (_) {}
}
};
_dprMedia = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
_dprMedia.addEventListener('change', _dprChangeHandler);
_dprMedia = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
_dprChangeHandler = () => {
updateAnimCanvas();
_rebindDPRListener();
};
_dprMedia.addEventListener('change', _dprChangeHandler, { once: true });
}
_rebindDPRListener();
const isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
(document.documentElement.getAttribute('data-theme') !== 'light' && window.matchMedia('(prefers-color-scheme: dark)').matches);