speedhack.js modernization

AuthorKonata <konata@posteo.jp>
Date
Commit7428829ff96825231bad71aae56f0160a4e5387f
Parent93e4952
1 file changed, 579 insertions(+), 623 deletions(-)
Mspeedhack.js
@@ -1,6 +1,6 @@
11 // ==UserScript==
22 // @name Speedhack Panel
3-// @version 1.0.0
3+// @version 1.1.0
44 // @description Floating, movable, resizable, minimizable time-scaling panel with an autoclicker and a Cheat-Engine-style memory scanner. Scales the page's JS timing functions by a chosen factor. Runs inside iframes. By default only the TOP frame shows a panel and broadcasts settings to child frames; any frame can be detached for its own panel. The autoclicker clicks at the cursor and auto-targets whichever (i)frame the cursor is in. The Scan tab finds/edits values in a game's WebAssembly heap or JS object graph, and can target any (i)frame from the top panel. Only the per-URL "closed" state and saved scans are remembered.
55 // @match *://*/*
66 // @run-at document-start
@@ -13,7 +13,7 @@
1313 /* eslint-disable no-empty */
1414 /* eslint-disable no-unused-vars */
1515
16-(function () {
16+(() => {
1717 'use strict';
1818
1919 if (window.__SPEEDHACK_PANEL__) return; // guard against double-injection in the same frame
@@ -31,7 +31,7 @@
3131 * ------------------------------------------------------------------ */
3232 const RealDate = pageWin.Date;
3333 const origDateNow = RealDate.now.bind(RealDate);
34- const origPerfNow = (pageWin.performance && pageWin.performance.now)
34+ const origPerfNow = pageWin.performance?.now
3535 ? pageWin.performance.now.bind(pageWin.performance)
3636 : origDateNow;
3737 const origSetTimeout = pageWin.setTimeout.bind(pageWin);
@@ -39,7 +39,7 @@
3939 const origClearTimeout = pageWin.clearTimeout.bind(pageWin);
4040 const origClearInterval= pageWin.clearInterval.bind(pageWin);
4141 const origRAF = (pageWin.requestAnimationFrame ||
42- function (cb) { return origSetTimeout(function () { cb(origPerfNow()); }, 16); }
42+ (cb => origSetTimeout(() => cb(origPerfNow()), 16))
4343 ).bind(pageWin);
4444
4545 /* ------------------------------------------------------------------ *
@@ -55,55 +55,38 @@
5555 (function hookWasm() {
5656 const W = pageWin.WebAssembly;
5757 if (!W) return;
58- function record(m, label) {
58+ const record = (m, label) => {
5959 try {
6060 if (!(m instanceof W.Memory)) return;
61- for (let i = 0; i < wasmMemories.length; i++) if (wasmMemories[i].memory === m) return;
62- wasmMemories.push({ memory: m, label: (label || ('mem#' + wasmMemories.length)) });
61+ if (wasmMemories.some(e => e.memory === m)) return;
62+ wasmMemories.push({ memory: m, label: label || ('mem#' + wasmMemories.length) });
6363 } catch (e) {}
64- }
65- function scanExports(res) {
64+ };
65+ const scanExports = (res) => {
6666 // res is an instantiate result ({ module, instance }) or a bare Instance.
6767 try {
68- const inst = (res && res.instance) ? res.instance : res;
69- const ex = inst && inst.exports;
68+ const ex = (res?.instance ?? res)?.exports;
7069 if (ex) for (const k in ex) { try { if (ex[k] instanceof W.Memory) record(ex[k], k); } catch (e) {} }
7170 } catch (e) {}
7271 return res;
73- }
74- const origInstantiate = W.instantiate, origInstantiateStreaming = W.instantiateStreaming;
75- if (typeof origInstantiate === 'function') {
76- W.instantiate = function () {
77- const p = origInstantiate.apply(this, arguments);
78- return (p && typeof p.then === 'function') ? p.then(scanExports) : p;
79- };
80- }
81- if (typeof origInstantiateStreaming === 'function') {
82- W.instantiateStreaming = function () {
83- const p = origInstantiateStreaming.apply(this, arguments);
84- return (p && typeof p.then === 'function') ? p.then(scanExports) : p;
85- };
86- }
87- const OrigInstance = W.Instance;
88- if (typeof OrigInstance === 'function') {
89- function PatchedInstance() {
90- const inst = new (Function.prototype.bind.apply(OrigInstance, [null].concat(Array.prototype.slice.call(arguments))))();
91- scanExports(inst);
92- return inst;
93- }
94- PatchedInstance.prototype = OrigInstance.prototype;
95- try { W.Instance = PatchedInstance; } catch (e) {}
96- }
97- const OrigMemory = W.Memory;
98- if (typeof OrigMemory === 'function') {
99- function PatchedMemory() {
100- const mem = new (Function.prototype.bind.apply(OrigMemory, [null].concat(Array.prototype.slice.call(arguments))))();
101- record(mem, 'Memory()');
102- return mem;
72+ };
73+ const wrapInstantiate = (orig) => function () {
74+ const p = orig.apply(this, arguments);
75+ return (typeof p?.then === 'function') ? p.then(scanExports) : p;
76+ };
77+ if (typeof W.instantiate === 'function') W.instantiate = wrapInstantiate(W.instantiate);
78+ if (typeof W.instantiateStreaming === 'function') W.instantiateStreaming = wrapInstantiate(W.instantiateStreaming);
79+ const patchCtor = (Orig, onNew) => {
80+ function Patched(...args) {
81+ const obj = Reflect.construct(Orig, args, new.target || Patched);
82+ onNew(obj);
83+ return obj;
10384 }
104- PatchedMemory.prototype = OrigMemory.prototype;
105- try { W.Memory = PatchedMemory; } catch (e) {}
106- }
85+ Patched.prototype = Orig.prototype;
86+ return Patched;
87+ };
88+ if (typeof W.Instance === 'function') { try { W.Instance = patchCtor(W.Instance, scanExports); } catch (e) {} }
89+ if (typeof W.Memory === 'function') { try { W.Memory = patchCtor(W.Memory, m => record(m, 'Memory()')); } catch (e) {} }
10790 })();
10891
10992 /* ------------------------------------------------------------------ *
@@ -116,24 +99,24 @@
11699 * are untouched. Installed at document-start so we wrap before the game does.
117100 * `panelHost` is read at event time (set once the panel is built).
118101 * ------------------------------------------------------------------ */
119- const SHIELD_TYPES = { keydown:1, keyup:1, keypress:1, pointerdown:1, pointerup:1,
120- mousedown:1, mouseup:1, click:1, dblclick:1, contextmenu:1,
121- wheel:1, touchstart:1, touchend:1 };
102+ const SHIELD_TYPES = new Set(['keydown', 'keyup', 'keypress', 'pointerdown', 'pointerup',
103+ 'mousedown', 'mouseup', 'click', 'dblclick', 'contextmenu',
104+ 'wheel', 'touchstart', 'touchend']);
122105 const shieldedTargets = new WeakSet();
123106 function eventInPanel(ev) {
124- try { return !!(panelHost && ev && ev.composedPath && ev.composedPath().indexOf(panelHost) !== -1); } catch (e) { return false; }
107+ try { return !!(panelHost && ev?.composedPath && ev.composedPath().includes(panelHost)); } catch (e) { return false; }
125108 }
126109 function shieldInputTarget(target) {
127110 if (!target || typeof target.addEventListener !== 'function' || shieldedTargets.has(target)) return;
128111 shieldedTargets.add(target);
129112 const origAdd = target.addEventListener, origRemove = target.removeEventListener;
130113 const wrappers = new WeakMap(); // handler -> { typeKey -> wrapper }
114+ const captureOf = (opts) => (typeof opts === 'object' && opts) ? !!opts.capture : !!opts;
131115 try {
132116 target.addEventListener = function (type, handler, opts) {
133117 const usable = handler && (typeof handler === 'function' || typeof handler.handleEvent === 'function');
134- if (!usable || !SHIELD_TYPES[type] || handler.__shxNoShield) return origAdd.call(this, type, handler, opts);
135- const capture = (typeof opts === 'object' && opts) ? !!opts.capture : !!opts;
136- const key = type + '/' + (capture ? 1 : 0);
118+ if (!usable || !SHIELD_TYPES.has(type) || handler.__shxNoShield) return origAdd.call(this, type, handler, opts);
119+ const key = type + '/' + (captureOf(opts) ? 1 : 0);
137120 let per = wrappers.get(handler); if (!per) { per = Object.create(null); wrappers.set(handler, per); }
138121 let wrapper = per[key];
139122 if (!wrapper) {
@@ -143,16 +126,15 @@
143126 return origAdd.call(this, type, wrapper, opts);
144127 };
145128 target.removeEventListener = function (type, handler, opts) {
146- if (!handler || !SHIELD_TYPES[type]) return origRemove.call(this, type, handler, opts);
147- const capture = (typeof opts === 'object' && opts) ? !!opts.capture : !!opts;
148- const per = wrappers.get(handler); const wrapper = per && per[type + '/' + (capture ? 1 : 0)];
129+ if (!handler || !SHIELD_TYPES.has(type)) return origRemove.call(this, type, handler, opts);
130+ const wrapper = wrappers.get(handler)?.[type + '/' + (captureOf(opts) ? 1 : 0)];
149131 return origRemove.call(this, type, wrapper || handler, opts);
150132 };
151133 } catch (e) {}
152134 }
153135 try { shieldInputTarget(pageWin); } catch (e) {}
154136 try { shieldInputTarget(pageWin.document); } catch (e) {}
155- try { shieldInputTarget(pageWin.document && pageWin.document.documentElement); } catch (e) {}
137+ try { shieldInputTarget(pageWin.document?.documentElement); } catch (e) {}
156138 // <body> may not exist yet at document-start; it's shielded when the panel is built.
157139
158140 /* ------------------------------------------------------------------ *
@@ -162,11 +144,11 @@
162144 * ------------------------------------------------------------------ */
163145 const PAGE = location.href.split('#')[0]; // "exact" page URL, ignoring #hash
164146 const store = {
165- get: function (k, d) {
147+ get(k, d) {
166148 try { if (typeof GM_getValue === 'function') return GM_getValue(k, d); } catch (e) {}
167- try { var v = localStorage.getItem('shx_' + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; }
149+ try { const v = localStorage.getItem('shx_' + k); return v === null ? d : JSON.parse(v); } catch (e) { return d; }
168150 },
169- set: function (k, v) {
151+ set(k, v) {
170152 try { if (typeof GM_setValue === 'function') { GM_setValue(k, v); return; } } catch (e) {}
171153 try { localStorage.setItem('shx_' + k, JSON.stringify(v)); } catch (e) {}
172154 }
@@ -185,10 +167,10 @@
185167 function makeClock(realFn) {
186168 let aReal = realFn(), aFake = aReal, s = 1;
187169 return {
188- now: function () { return aFake + (realFn() - aReal) * s; },
189- setScale: function (ns) { const r = realFn(); aFake = aFake + (r - aReal) * s; aReal = r; s = ns; },
190- reanchor: function () { const r = realFn(); aReal = r; aFake = r; },
191- setTo: function (v) { aReal = realFn(); aFake = v; } // jump the fake timeline to absolute v
170+ now: () => aFake + (realFn() - aReal) * s,
171+ setScale: (ns) => { const r = realFn(); aFake = aFake + (r - aReal) * s; aReal = r; s = ns; },
172+ reanchor: () => { const r = realFn(); aReal = r; aFake = r; },
173+ setTo: (v) => { aReal = realFn(); aFake = v; } // jump the fake timeline to absolute v
192174 };
193175 }
194176 const dateClock = makeClock(origDateNow);
@@ -202,7 +184,7 @@
202184 dateClock.setScale(ns);
203185 perfClock.setScale(ns);
204186 }
205- function perfRead() { return (turboNow !== null) ? turboNow : perfClock.now(); }
187+ const perfRead = () => (turboNow !== null) ? turboNow : perfClock.now();
206188
207189 /* ------------------------------------------------------------------ *
208190 * Pause (for the Scan tab). scale=0 is normally rejected by applyScale,
@@ -231,14 +213,13 @@
231213 /* ------------------------------------------------------------------ *
232214 * Fake Date
233215 * ------------------------------------------------------------------ */
234- function FakeDate() {
235- const args = Array.prototype.slice.call(arguments);
216+ function FakeDate(...args) {
236217 if (new.target === undefined) return new RealDate(dateClock.now()).toString();
237218 if (args.length === 0) return new RealDate(dateClock.now());
238- return new (Function.prototype.bind.apply(RealDate, [null].concat(args)))();
219+ return new RealDate(...args);
239220 }
240221 FakeDate.prototype = RealDate.prototype;
241- FakeDate.now = function () { return Math.floor(dateClock.now()); };
222+ FakeDate.now = () => Math.floor(dateClock.now());
242223 FakeDate.parse = RealDate.parse.bind(RealDate);
243224 FakeDate.UTC = RealDate.UTC.bind(RealDate);
244225 try { Object.setPrototypeOf(FakeDate, RealDate); } catch (e) {}
@@ -264,8 +245,8 @@
264245 if (clearHooksInstalled) return;
265246 clearHooksInstalled = true;
266247 // Transparent: only our string handles are intercepted; native numeric ids fall through.
267- pageWin.clearInterval = function (id) { if (clearFake(id)) return; return origClearInterval(id); };
268- pageWin.clearTimeout = function (id) { if (clearFake(id)) return; return origClearTimeout(id); };
248+ pageWin.clearInterval = (id) => { if (clearFake(id)) return; return origClearInterval(id); };
249+ pageWin.clearTimeout = (id) => { if (clearFake(id)) return; return origClearTimeout(id); };
269250 }
270251
271252 /* ------------------------------------------------------------------ *
@@ -274,39 +255,37 @@
274255 const hooks = {
275256 date: {
276257 label: 'Date (Date.now / new Date)',
277- install: function () { dateClock.reanchor(); pageWin.Date = FakeDate; },
278- uninstall: function () { pageWin.Date = RealDate; }
258+ install: () => { dateClock.reanchor(); pageWin.Date = FakeDate; },
259+ uninstall: () => { pageWin.Date = RealDate; }
279260 },
280261 performance: {
281262 label: 'performance.now',
282- install: function () { perfClock.reanchor(); if (pageWin.performance) pageWin.performance.now = function () { return perfRead(); }; },
283- uninstall: function () { if (pageWin.performance) pageWin.performance.now = origPerfNow; }
263+ install: () => { perfClock.reanchor(); if (pageWin.performance) pageWin.performance.now = () => perfRead(); },
264+ uninstall: () => { if (pageWin.performance) pageWin.performance.now = origPerfNow; }
284265 },
285266 setTimeout: {
286267 label: 'setTimeout',
287- install: function () {
288- pageWin.setTimeout = function (fn, delay) {
289- const rest = Array.prototype.slice.call(arguments, 2);
268+ install: () => {
269+ pageWin.setTimeout = (fn, delay, ...rest) => {
290270 if (typeof delay === 'number' && isFinite(delay)) delay = delay / scale;
291- return origSetTimeout.apply(null, [fn, delay].concat(rest));
271+ return origSetTimeout(fn, delay, ...rest);
292272 };
293273 },
294- uninstall: function () { pageWin.setTimeout = origSetTimeout; }
274+ uninstall: () => { pageWin.setTimeout = origSetTimeout; }
295275 },
296276 setInterval: {
297277 label: 'setInterval',
298- install: function () {
278+ install: () => {
299279 installClearHooks();
300- pageWin.setInterval = function (fn, delay) {
301- const rest = Array.prototype.slice.call(arguments, 2);
280+ pageWin.setInterval = (fn, delay, ...rest) => {
302281 // Non-function callback or non-finite delay → defer to native semantics.
303282 if (typeof fn !== 'function' || typeof delay !== 'number' || !isFinite(delay)) {
304- return origSetInterval.apply(null, arguments);
283+ return origSetInterval(fn, delay, ...rest);
305284 }
306285 const id = 'shx_int_' + (intervalSeq++);
307286 const rec = { timer: 0, cancelled: false };
308287 fakeIntervals.set(id, rec);
309- function tick() {
288+ const tick = () => {
310289 if (rec.cancelled) return;
311290 // Re-read scale every tick so slider changes take effect on a live interval.
312291 // When the hook is toggled OFF, eff=1 → the interval keeps running at native
@@ -314,7 +293,7 @@
314293 const eff = state.setInterval ? scale : 1;
315294 rec.timer = origSetTimeout(tick, delay / eff); // schedule next BEFORE the call,
316295 try { fn.apply(pageWin, rest); } catch (e) {} // so a clear() inside fn cancels it
317- }
296+ };
318297 const eff0 = state.setInterval ? scale : 1;
319298 rec.timer = origSetTimeout(tick, delay / eff0);
320299 return id;
@@ -322,44 +301,42 @@
322301 },
323302 // Running fake intervals keep ticking after uninstall, but at scale 1 (see `eff`),
324303 // so toggling the hook off unscales them rather than freezing the page.
325- uninstall: function () { pageWin.setInterval = origSetInterval; }
304+ uninstall: () => { pageWin.setInterval = origSetInterval; }
326305 },
327306 raf: {
328307 label: 'requestAnimationFrame',
329- install: function () {
330- pageWin.requestAnimationFrame = function (cb) {
331- return origRAF(function (realT) {
332- if (!turbo) {
333- // Feed one scaled timestamp. If the callback throws, swallow it — do NOT
334- // re-invoke with a different time, which would double-run frame side effects.
335- try { cb(perfRead()); } catch (e) {}
336- return;
337- }
338- // turbo: run the frame callback k times per real frame on ONE monotonic
339- // timeline, ~1 normal frame apart, so engines that CLAMP delta-time still
340- // advance k frames. Fake time advances by exactly k*step here — it does not
341- // also track real elapsed time, which is what used to cause discontinuities.
342- const k = Math.max(1, Math.min(20, Math.round(scale))); // capped so the tab can't lock up
343- const step = 1000 / 60;
344- if (turboT === null) turboT = perfClock.now(); // seed once from current fake time
345- let currentCbs = [cb];
346- for (let i = 0; i < k && currentCbs.length; i++) {
347- turboT += step; // advance the single timeline
348- turboNow = turboT;
349- const nextCbs = []; // collect EVERY rAF re-registered this sub-step,
350- const prev = pageWin.requestAnimationFrame; // not just the last (a callback may schedule several)
351- pageWin.requestAnimationFrame = function (next) { if (typeof next === 'function') nextCbs.push(next); return 0; };
352- for (let j = 0; j < currentCbs.length; j++) { try { currentCbs[j](turboT); } catch (e) {} }
353- pageWin.requestAnimationFrame = prev; // restore our wrapper
354- currentCbs = nextCbs; // chain all re-registered callbacks
355- }
356- turboNow = null;
357- perfClock.setTo(turboT); // keep performance.now() continuous after the burst
358- for (let j = 0; j < currentCbs.length; j++) pageWin.requestAnimationFrame(currentCbs[j]); // next REAL frame
359- });
360- };
308+ install: () => {
309+ pageWin.requestAnimationFrame = (cb) => origRAF(() => {
310+ if (!turbo) {
311+ // Feed one scaled timestamp. If the callback throws, swallow it — do NOT
312+ // re-invoke with a different time, which would double-run frame side effects.
313+ try { cb(perfRead()); } catch (e) {}
314+ return;
315+ }
316+ // turbo: run the frame callback k times per real frame on ONE monotonic
317+ // timeline, ~1 normal frame apart, so engines that CLAMP delta-time still
318+ // advance k frames. Fake time advances by exactly k*step here — it does not
319+ // also track real elapsed time, which is what used to cause discontinuities.
320+ const k = Math.max(1, Math.min(20, Math.round(scale))); // capped so the tab can't lock up
321+ const step = 1000 / 60;
322+ if (turboT === null) turboT = perfClock.now(); // seed once from current fake time
323+ let currentCbs = [cb];
324+ for (let i = 0; i < k && currentCbs.length; i++) {
325+ turboT += step; // advance the single timeline
326+ turboNow = turboT;
327+ const nextCbs = []; // collect EVERY rAF re-registered this sub-step,
328+ const prev = pageWin.requestAnimationFrame; // not just the last (a callback may schedule several)
329+ pageWin.requestAnimationFrame = (next) => { if (typeof next === 'function') nextCbs.push(next); return 0; };
330+ for (const c of currentCbs) { try { c(turboT); } catch (e) {} }
331+ pageWin.requestAnimationFrame = prev; // restore our wrapper
332+ currentCbs = nextCbs; // chain all re-registered callbacks
333+ }
334+ turboNow = null;
335+ perfClock.setTo(turboT); // keep performance.now() continuous after the burst
336+ for (const c of currentCbs) pageWin.requestAnimationFrame(c); // next REAL frame
337+ });
361338 },
362- uninstall: function () { pageWin.requestAnimationFrame = origRAF; }
339+ uninstall: () => { pageWin.requestAnimationFrame = origRAF; }
363340 }
364341 };
365342
@@ -369,7 +346,7 @@
369346 state[name] = on;
370347 try { on ? hooks[name].install() : hooks[name].uninstall(); } catch (e) {}
371348 }
372- Object.keys(hooks).forEach(function (n) { if (state[n]) { try { hooks[n].install(); } catch (e) {} } });
349+ for (const n of Object.keys(hooks)) if (state[n]) { try { hooks[n].install(); } catch (e) {} }
373350
374351 /* ------------------------------------------------------------------ *
375352 * Multi-frame coordination.
@@ -379,7 +356,7 @@
379356 * from a host within 5s promotes itself (covers a top frame the manager
380357 * didn't inject into). Hooks are installed in every frame regardless.
381358 * ------------------------------------------------------------------ */
382- const isTop = (function () { try { return window.top === window.self; } catch (e) { return true; } })();
359+ const isTop = (() => { try { return window.top === window.self; } catch (e) { return true; } })();
383360 let isHost = isTop; // top frame hosts by default; a stranded child may promote
384361 let attached = !isTop; // children follow the host until detached
385362 let hostWin = null; // a child's link back to its host window
@@ -392,42 +369,35 @@
392369 // which can be null/unpostable for cross-origin iframes in an isolated world.
393370 const SELF_ID = 'shx-' + Math.random().toString(36).slice(2) + Math.random().toString(36).slice(2);
394371
395- function currentSettings() {
396- return {
397- scale: scale,
398- turbo: turbo,
399- hooks: { date: state.date, performance: state.performance, setTimeout: state.setTimeout,
400- setInterval: state.setInterval, raf: state.raf }
401- };
402- }
403- function settingsMsg(type) {
404- const s = currentSettings();
405- return { type: type, scale: s.scale, turbo: s.turbo, hooks: s.hooks };
406- }
372+ const settingsMsg = (type) => ({ type, scale, turbo, hooks: { ...state } });
407373 function applySettings(s) {
408- if (s.hooks) Object.keys(s.hooks).forEach(function (n) {
374+ if (s.hooks) for (const n of Object.keys(s.hooks)) {
409375 if (hooks[n] && state[n] !== s.hooks[n]) setHook(n, s.hooks[n]);
410- });
376+ }
411377 if (typeof s.turbo === 'boolean' && s.turbo !== turbo) { turbo = s.turbo; turboT = null; }
412378 if (typeof s.scale === 'number') applyScale(s.scale);
413- if (panelCtl) panelCtl.sync();
379+ panelCtl?.sync();
414380 }
415381 function postTo(win, msg) { try { msg.__shx = 1; win.postMessage(msg, '*'); } catch (e) {} }
416382
417- // Deliver a scan message to EVERY frame in the tree (root = window.top, plus all
418- // descendants), the same live-frame walk rollcall uses — which is proven to reach
419- // cross-origin children. Only the frame whose SELF_ID matches `targetFrame` acts on
420- // it; the rest ignore it. This sidesteps stored-`e.source` references entirely.
383+ // The one cross-origin-safe recursive walk over the live frame tree (root =
384+ // window.top by default, i.e. every frame on the page except the root itself).
385+ const topWin = () => { try { return window.top; } catch (e) { return window; } };
386+ function forEachFrame(cb, root = topWin()) {
387+ let list; try { list = root.frames; } catch (e) { return; }
388+ for (let i = 0; i < list.length; i++) {
389+ try { cb(list[i]); } catch (e) {}
390+ forEachFrame(cb, list[i]);
391+ }
392+ }
393+
394+ // Deliver a scan message to EVERY frame in the tree (root + all descendants),
395+ // the same live-frame walk rollcall uses — which is proven to reach cross-origin
396+ // children. Only the frame whose SELF_ID matches `targetFrame` acts on it; the
397+ // rest ignore it. This sidesteps stored-`e.source` references entirely.
421398 function broadcastScanMsg(msg) {
422- let root; try { root = window.top; } catch (e) { root = window; }
423- try { postTo(root, msg); } catch (e) {}
424- (function visit(win) {
425- let list; try { list = win.frames; } catch (e) { return; }
426- for (let i = 0; i < list.length; i++) {
427- try { postTo(list[i], msg); } catch (e) {}
428- try { visit(list[i]); } catch (e) {}
429- }
430- })(root);
399+ postTo(topWin(), msg);
400+ forEachFrame(w => postTo(w, msg));
431401 }
432402
433403 function pruneFrames() {
@@ -435,29 +405,20 @@
435405 // entries whose iframe has been removed from the tree, else they accumulate (phantom
436406 // frame-list rows + dead postMessage targets) on long-lived / SPA pages.
437407 const live = new Set();
438- (function visit(win) {
439- let list; try { list = win.frames; } catch (e) { return; }
440- for (let i = 0; i < list.length; i++) { try { live.add(list[i]); visit(list[i]); } catch (e) {} }
441- })(window.top);
408+ forEachFrame(w => live.add(w));
442409 let changed = false;
443- frames.forEach(function (f, src) { if (!live.has(src)) { frames.delete(src); changed = true; } });
410+ frames.forEach((f, src) => { if (!live.has(src)) { frames.delete(src); changed = true; } });
444411 return changed;
445412 }
446413 function broadcastSettings() {
447414 if (!isHost) return; // only a host pushes settings out
448415 pruneFrames();
449- frames.forEach(function (f, src) { if (f.attached) postTo(src, settingsMsg('settings')); });
416+ frames.forEach((f, src) => { if (f.attached) postTo(src, settingsMsg('settings')); });
450417 }
451418 function rollcall() {
452419 // Ask every descendant frame to (re-)announce itself — covers a host that
453420 // booted after its children. Reaching cross-origin frames via postMessage is fine.
454- (function visit(win) {
455- let list; try { list = win.frames; } catch (e) { return; }
456- for (let i = 0; i < list.length; i++) {
457- try { postTo(list[i], { type: 'rollcall' }); } catch (e) {}
458- try { visit(list[i]); } catch (e) {}
459- }
460- })(window.top);
421+ forEachFrame(w => postTo(w, { type: 'rollcall' }));
461422 }
462423 function promoteToHost() {
463424 if (isHost) return;
@@ -468,100 +429,90 @@
468429 function detachFrame(src) {
469430 const f = frames.get(src); if (!f) return;
470431 f.attached = false; postTo(src, { type: 'detach' });
471- if (panelCtl) panelCtl.refreshFrames();
432+ panelCtl?.refreshFrames();
472433 }
473434 function reattachFrame(src) {
474435 const f = frames.get(src); if (!f) return;
475436 f.attached = true; postTo(src, settingsMsg('attach'));
476- if (panelCtl) panelCtl.refreshFrames();
437+ panelCtl?.refreshFrames();
477438 }
478439
479- window.addEventListener('message', function (e) {
440+ // One named handler per message type; hostOnly/childOnly guards applied by the dispatcher.
441+ const MSG = {
442+ hello: { hostOnly: true, fn(m, e) { // host: a child announced itself
443+ if (hostClosed) { postTo(e.source, { type: 'host-closing' }); return; } // late frame after close → go standalone
444+ const f = frames.get(e.source);
445+ if (f) { f.url = m.url; if (m.frameId) f.id = m.frameId; }
446+ else frames.set(e.source, { url: m.url, attached: true, id: m.frameId });
447+ postTo(e.source, settingsMsg('settings')); // sync the newcomer immediately
448+ postTo(e.source, clickerConfigMsg()); // ...including autoclicker config
449+ postTo(e.source, { type: 'clicker-run', on: clicker.running });
450+ panelCtl?.refreshFrames();
451+ } },
452+ rollcall: { childOnly: true, fn(m, e) { // child: a host is probing for frames
453+ gotHost = true; hostWin = e.source;
454+ postTo(e.source, { type: 'hello', url: location.href, frameId: SELF_ID });
455+ } },
456+ settings: { childOnly: true, fn(m, e) { // child: host pushed settings
457+ gotHost = true; hostWin = e.source;
458+ if (attached) applySettings(m);
459+ } },
460+ detach: { childOnly: true, fn(m, e) { // child: host detached us → own panel
461+ hostWin = e.source; attached = false;
462+ ensurePanel('detached');
463+ } },
464+ 'host-closing': { childOnly: true, fn() { // child: the host closed → become standalone
465+ promoteToHost(); // host mode (no re-attach button), self-sufficient
466+ } },
467+ attach: { childOnly: true, fn(m) { // child: host folded us back in
468+ attached = true; destroyPanel(); applySettings(m);
469+ } },
470+ reattach: { hostOnly: true, fn(m, e) { // host: a detached child asked to fold back
471+ const f = frames.get(e.source);
472+ if (f) { f.attached = true; postTo(e.source, settingsMsg('settings')); panelCtl?.refreshFrames(); }
473+ } },
474+ 'clicker-config': { childOnly: true, fn(m) { // child: host pushed clicker settings
475+ applyClickerConfig(m);
476+ } },
477+ 'clicker-run': { fn(m) { // shared autoclicker running state
478+ // host: a child toggled it → adopt + fan out to all; child: host pushed it down.
479+ setClickerRunning(m.on, isHost);
480+ } },
481+ 'scan-cmd': { fn(m, e) { // any frame: if addressed to me, run + reply
482+ if (m.targetFrame && m.targetFrame !== SELF_ID) return; // broadcast not meant for this frame
483+ const dkey = (m.from || '') + ':' + m.reqId;
484+ if (scanHandled.has(dkey)) return; // duplicate — arrived via both channels
485+ scanHandled.add(dkey); scanHandledQ.push(dkey);
486+ if (scanHandledQ.length > 400) scanHandled.delete(scanHandledQ.shift());
487+ const replyWin = e.source;
488+ const scmd = m.cmd || m; // command payload is nested under `cmd` (avoids type-field collision)
489+ let lastFrac = -2; // throttle determinate progress to ~2% steps (indeterminate always relayed)
490+ runScanCommand(scmd, (frac) => {
491+ if (frac >= 0 && frac !== 1 && frac - lastFrac < 0.02) return;
492+ lastFrac = frac;
493+ try { if (replyWin) postTo(replyWin, { type: 'scan-progress', reqId: m.reqId, targetFrame: m.from, frac }); } catch (e2) {}
494+ }).then((res) => {
495+ res.type = 'scan-result'; res.reqId = m.reqId; res.targetFrame = m.from;
496+ try { if (replyWin) postTo(replyWin, res); } catch (e2) {} // reply to the sender directly...
497+ broadcastScanMsg(res); // ...and via window.top (reliable upward)
498+ });
499+ } },
500+ 'scan-progress': { fn(m) { // controller: forward incremental progress to the UI
501+ if (m.targetFrame && m.targetFrame !== SELF_ID) return;
502+ scanProgress.get(m.reqId)?.(m.frac);
503+ } },
504+ 'scan-result': { fn(m) { // controller: resolve the matching pending request
505+ if (m.targetFrame && m.targetFrame !== SELF_ID) return;
506+ scanProgress.delete(m.reqId);
507+ const resolve = scanPending.get(m.reqId);
508+ if (resolve) { scanPending.delete(m.reqId); resolve(m); }
509+ } }
510+ };
511+ window.addEventListener('message', (e) => {
480512 const m = e.data; if (!m || m.__shx !== 1) return;
481- switch (m.type) {
482- case 'hello': // host: a child announced itself
483- if (!isHost) break;
484- if (hostClosed) { postTo(e.source, { type: 'host-closing' }); break; } // late frame after close → go standalone
485- if (frames.has(e.source)) { frames.get(e.source).url = m.url; if (m.frameId) frames.get(e.source).id = m.frameId; }
486- else frames.set(e.source, { url: m.url, attached: true, id: m.frameId });
487- postTo(e.source, settingsMsg('settings')); // sync the newcomer immediately
488- postTo(e.source, clickerConfigMsg()); // ...including autoclicker config
489- postTo(e.source, { type: 'clicker-run', on: clicker.running });
490- if (panelCtl) panelCtl.refreshFrames();
491- break;
492- case 'rollcall': // child: a host is probing for frames
493- if (isHost) break;
494- gotHost = true; hostWin = e.source;
495- postTo(e.source, { type: 'hello', url: location.href, frameId: SELF_ID });
496- break;
497- case 'settings': // child: host pushed settings
498- if (isHost) break;
499- gotHost = true; hostWin = e.source;
500- if (attached) applySettings(m);
501- break;
502- case 'detach': // child: host detached us → own panel
503- if (isHost) break;
504- hostWin = e.source; attached = false;
505- ensurePanel('detached');
506- break;
507- case 'host-closing': // child: the host closed → become standalone
508- if (isHost) break;
509- promoteToHost(); // host mode (no re-attach button), self-sufficient
510- break;
511- case 'attach': // child: host folded us back in
512- if (isHost) break;
513- attached = true; destroyPanel(); applySettings(m);
514- break;
515- case 'reattach': // host: a detached child asked to fold back
516- if (!isHost) break;
517- { const f = frames.get(e.source);
518- if (f) { f.attached = true; postTo(e.source, settingsMsg('settings')); if (panelCtl) panelCtl.refreshFrames(); } }
519- break;
520- case 'clicker-config': // child: host pushed clicker settings
521- if (isHost) break;
522- applyClickerConfig(m);
523- break;
524- case 'clicker-run': // shared autoclicker running state
525- if (isHost) { // a child toggled it → adopt + fan out to all
526- setClickerRunning(m.on, true);
527- } else { // host pushed the state down
528- setClickerRunning(m.on, false);
529- }
530- break;
531- case 'scan-cmd': { // any frame: if addressed to me, run + reply
532- if (m.targetFrame && m.targetFrame !== SELF_ID) break; // broadcast not meant for this frame
533- const dkey = (m.from || '') + ':' + m.reqId;
534- if (scanHandled.has(dkey)) break; // duplicate — arrived via both channels
535- scanHandled.add(dkey); scanHandledQ.push(dkey);
536- if (scanHandledQ.length > 400) scanHandled.delete(scanHandledQ.shift());
537- const replyWin = e.source;
538- const scmd = m.cmd || m; // command payload is nested under `cmd` (avoids type-field collision)
539- let lastFrac = -2; // throttle determinate progress to ~2% steps (indeterminate always relayed)
540- runScanCommand(scmd, function (frac) {
541- if (frac >= 0 && frac !== 1 && frac - lastFrac < 0.02) return;
542- lastFrac = frac;
543- try { if (replyWin) postTo(replyWin, { type: 'scan-progress', reqId: m.reqId, targetFrame: m.from, frac: frac }); } catch (e2) {}
544- }).then(function (res) {
545- res.type = 'scan-result'; res.reqId = m.reqId; res.targetFrame = m.from;
546- try { if (replyWin) postTo(replyWin, res); } catch (e2) {} // reply to the sender directly...
547- broadcastScanMsg(res); // ...and via window.top (reliable upward)
548- });
549- break;
550- }
551- case 'scan-progress': { // controller: forward incremental progress to the UI
552- if (m.targetFrame && m.targetFrame !== SELF_ID) break;
553- const cb = scanProgress.get(m.reqId);
554- if (cb) cb(m.frac);
555- break;
556- }
557- case 'scan-result': { // controller: resolve the matching pending request
558- if (m.targetFrame && m.targetFrame !== SELF_ID) break;
559- scanProgress.delete(m.reqId);
560- const resolve = scanPending.get(m.reqId);
561- if (resolve) { scanPending.delete(m.reqId); resolve(m); }
562- break;
563- }
564- }
513+ const h = MSG[m.type]; if (!h) return;
514+ if ((h.hostOnly && !isHost) || (h.childOnly && isHost)) return;
515+ h.fn(m, e);
565516 });
566517
567518 /* ------------------------------------------------------------------ *
@@ -589,11 +540,11 @@
589540 const MAX_CPS = 100;
590541 const ctxDoc = pageWin.document || document;
591542
592- function cursorInside() { return clicker.enteredDoc && !clicker.overChildFrame; }
543+ const cursorInside = () => clicker.enteredDoc && !clicker.overChildFrame;
593544 function trackPointer(e) {
594545 clicker.lastX = e.clientX; clicker.lastY = e.clientY;
595546 clicker.enteredDoc = true;
596- const t = e.target, tag = t && t.tagName;
547+ const tag = e.target?.tagName;
597548 clicker.overChildFrame = (tag === 'IFRAME' || tag === 'FRAME');
598549 }
599550 try {
@@ -601,7 +552,7 @@
601552 ctxDoc.addEventListener('mousemove', trackPointer, true); // fallback where PointerEvents are absent
602553 ctxDoc.addEventListener('mouseover', trackPointer, true); // updates overChildFrame even without movement
603554 // relatedTarget == null on mouseout means the cursor left the window entirely.
604- ctxDoc.addEventListener('mouseout', function (e) { if (!e.relatedTarget) clicker.enteredDoc = false; }, true);
555+ ctxDoc.addEventListener('mouseout', (e) => { if (!e.relatedTarget) clicker.enteredDoc = false; }, true);
605556 } catch (e) {}
606557
607558 function fireClick() {
@@ -610,16 +561,16 @@
610561 if (!el) return;
611562 const base = { bubbles: true, cancelable: true, composed: true, view: pageWin, clientX: x, clientY: y, button: 0 };
612563 function dispatch(type, buttons, pointer) {
613- const opts = Object.assign({}, base, { buttons: buttons });
564+ const opts = { ...base, buttons };
614565 let ev;
615566 if (pointer && pageWin.PointerEvent) {
616- try { ev = new pageWin.PointerEvent(type, Object.assign(opts, { pointerId: 1, pointerType: 'mouse', isPrimary: true })); } catch (e) {}
567+ try { ev = new pageWin.PointerEvent(type, { ...opts, pointerId: 1, pointerType: 'mouse', isPrimary: true }); } catch (e) {}
617568 }
618569 if (!ev) { try { ev = new pageWin.MouseEvent(type, opts); } catch (e) { return; } }
619570 try { el.dispatchEvent(ev); } catch (e) {}
620571 }
621572 dispatch('pointerdown', 1, true); dispatch('mousedown', 1, false);
622- const up = function () {
573+ const up = () => {
623574 clicker.upTimer = 0;
624575 dispatch('pointerup', 0, true); dispatch('mouseup', 0, false); dispatch('click', 0, false);
625576 };
@@ -650,20 +601,20 @@
650601 function setClickerRunning(on, propagate) {
651602 on = !!on;
652603 if (clicker.running !== on) { clicker.running = on; on ? startClickerLoop() : stopClickerLoop(); }
653- if (panelCtl) panelCtl.syncClicker();
604+ panelCtl?.syncClicker();
654605 if (!propagate) return;
655- if (isHost) frames.forEach(function (f, src) { postTo(src, { type: 'clicker-run', on: on }); });
656- else if (hostWin) postTo(hostWin, { type: 'clicker-run', on: on });
606+ if (isHost) frames.forEach((f, src) => postTo(src, { type: 'clicker-run', on }));
607+ else if (hostWin) postTo(hostWin, { type: 'clicker-run', on });
657608 }
658609
659610 function clickerConfigMsg() {
660- return { type: 'clicker-config', mode: clicker.mode, hotkey: clicker.hotkey,
661- swallowHotkey: clicker.swallowHotkey, cps: clicker.cps, jitterMs: clicker.jitterMs, holdMs: clicker.holdMs };
611+ const { mode, hotkey, swallowHotkey, cps, jitterMs, holdMs } = clicker;
612+ return { type: 'clicker-config', mode, hotkey, swallowHotkey, cps, jitterMs, holdMs };
662613 }
663614 function broadcastClickerConfig() {
664615 if (!isHost) return; // config flows host → all frames (not just attached)
665616 pruneFrames();
666- frames.forEach(function (f, src) { postTo(src, clickerConfigMsg()); });
617+ frames.forEach((f, src) => postTo(src, clickerConfigMsg()));
667618 }
668619 function applyClickerConfig(c) {
669620 if (c.mode === 'toggle' || c.mode === 'hold') clicker.mode = c.mode;
@@ -672,7 +623,7 @@
672623 if (typeof c.cps === 'number') clicker.cps = Math.min(MAX_CPS, Math.max(0.1, c.cps));
673624 if (typeof c.jitterMs === 'number') clicker.jitterMs = Math.max(0, c.jitterMs);
674625 if (typeof c.holdMs === 'number') clicker.holdMs = Math.max(0, c.holdMs);
675- if (panelCtl) panelCtl.syncClicker();
626+ panelCtl?.syncClicker();
676627 }
677628
678629 function hotkeyMatches(e, hk) {
@@ -680,15 +631,15 @@
680631 !!e.shiftKey === !!hk.shift && !!e.metaKey === !!hk.meta;
681632 }
682633 function eventFromPanel(e) {
683- return panelHost && e.composedPath && e.composedPath().indexOf(panelHost) !== -1;
634+ return panelHost && e.composedPath && e.composedPath().includes(panelHost);
684635 }
685636 function onClickerKeyDown(e) {
686637 if (clicker.listening) { // capturing a new binding (panel frame)
687- if (e.key === 'Control' || e.key === 'Alt' || e.key === 'Shift' || e.key === 'Meta') return; // await a real key
638+ if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) return; // await a real key
688639 e.preventDefault(); e.stopPropagation();
689640 clicker.hotkey = { key: e.key, ctrl: e.ctrlKey, alt: e.altKey, shift: e.shiftKey, meta: e.metaKey };
690641 clicker.listening = false;
691- if (panelCtl) panelCtl.syncClicker();
642+ panelCtl?.syncClicker();
692643 broadcastClickerConfig();
693644 return;
694645 }
@@ -714,7 +665,7 @@
714665 pageWin.addEventListener('keyup', onClickerKeyUp, true);
715666 // Safeguard: in hold mode a keyup can land in a different frame than the keydown;
716667 // losing focus then releasing would otherwise leave it stuck on.
717- pageWin.addEventListener('blur', function () { if (clicker.mode === 'hold' && clicker.running) setClickerRunning(false, true); }, true);
668+ pageWin.addEventListener('blur', () => { if (clicker.mode === 'hold' && clicker.running) setClickerRunning(false, true); }, true);
718669 } catch (e) {}
719670
720671 /* ================================================================== *
@@ -728,16 +679,18 @@
728679 * The engine runs LOCALLY in each frame and owns its own candidate state;
729680 * the panel drives it directly (this frame) or over postMessage (iframes).
730681 * ================================================================== */
682+ // `arr` is the TypedArray used for bulk scanning (platform-endian == little-endian
683+ // on every supported browser, matching the explicit-LE DataView used for R/W).
731684 const SCAN_TYPES = {
732- i8: { size: 1, get: function (d, o) { return d.getInt8(o); }, set: function (d, o, v) { d.setInt8(o, v); } },
733- u8: { size: 1, get: function (d, o) { return d.getUint8(o); }, set: function (d, o, v) { d.setUint8(o, v); } },
734- i16: { size: 2, get: function (d, o) { return d.getInt16(o, true); }, set: function (d, o, v) { d.setInt16(o, v, true); } },
735- u16: { size: 2, get: function (d, o) { return d.getUint16(o, true); }, set: function (d, o, v) { d.setUint16(o, v, true); } },
736- i32: { size: 4, get: function (d, o) { return d.getInt32(o, true); }, set: function (d, o, v) { d.setInt32(o, v, true); } },
737- u32: { size: 4, get: function (d, o) { return d.getUint32(o, true); }, set: function (d, o, v) { d.setUint32(o, v >>> 0, true); } },
738- i64: { size: 8, big: true, get: function (d, o) { return d.getBigInt64(o, true); }, set: function (d, o, v) { d.setBigInt64(o, v, true); } },
739- f32: { size: 4, float: true, get: function (d, o) { return d.getFloat32(o, true); }, set: function (d, o, v) { d.setFloat32(o, v, true); } },
740- f64: { size: 8, float: true, get: function (d, o) { return d.getFloat64(o, true); }, set: function (d, o, v) { d.setFloat64(o, v, true); } }
685+ i8: { size: 1, arr: Int8Array, get: (d, o) => d.getInt8(o), set: (d, o, v) => d.setInt8(o, v) },
686+ u8: { size: 1, arr: Uint8Array, get: (d, o) => d.getUint8(o), set: (d, o, v) => d.setUint8(o, v) },
687+ i16: { size: 2, arr: Int16Array, get: (d, o) => d.getInt16(o, true), set: (d, o, v) => d.setInt16(o, v, true) },
688+ u16: { size: 2, arr: Uint16Array, get: (d, o) => d.getUint16(o, true), set: (d, o, v) => d.setUint16(o, v, true) },
689+ i32: { size: 4, arr: Int32Array, get: (d, o) => d.getInt32(o, true), set: (d, o, v) => d.setInt32(o, v, true) },
690+ u32: { size: 4, arr: Uint32Array, get: (d, o) => d.getUint32(o, true), set: (d, o, v) => d.setUint32(o, v >>> 0, true) },
691+ i64: { size: 8, arr: BigInt64Array, big: true, get: (d, o) => d.getBigInt64(o, true), set: (d, o, v) => d.setBigInt64(o, v, true) },
692+ f32: { size: 4, arr: Float32Array, float: true, get: (d, o) => d.getFloat32(o, true), set: (d, o, v) => d.setFloat32(o, v, true) },
693+ f64: { size: 8, arr: Float64Array, float: true, get: (d, o) => d.getFloat64(o, true), set: (d, o, v) => d.setFloat64(o, v, true) }
741694 };
742695 const SCAN_STORE_CAP = 500000; // max candidates TRACKED locally (refine works on all of these)
743696 const FLOAT_EPS = 1e-4;
@@ -749,10 +702,8 @@
749702 allint: ['i8', 'u8', 'i16', 'u16', 'i32', 'u32', 'i64'],
750703 allfloat: ['f32', 'f64']
751704 };
752- function expandTypes(sel) {
753- if (SCAN_TYPE_GROUPS[sel]) return SCAN_TYPE_GROUPS[sel].slice();
754- return SCAN_TYPES[sel] ? [sel] : ['i32'];
755- }
705+ const expandTypes = (sel) =>
706+ SCAN_TYPE_GROUPS[sel] ? SCAN_TYPE_GROUPS[sel].slice() : (SCAN_TYPES[sel] ? [sel] : ['i32']);
756707
757708 // Parse the user's typed value into the JS form the type compares with.
758709 function parseScanValue(type, raw) {
@@ -786,12 +737,12 @@
786737 // "-1.5" → (-1.6, -1.5]. So magnitude grows symmetrically for either sign.
787738 function makeExactMatcher(type, raw) {
788739 const t = SCAN_TYPES[type]; if (!t) return null;
789- if (t.big) { const target = parseScanValue(type, raw); if (target === null) return null; return { value: target, match: function (x) { return x === target; } }; }
740+ if (t.big) { const target = parseScanValue(type, raw); if (target === null) return null; return { value: target, match: x => x === target }; }
790741 const v = Number(raw); if (!isFinite(v)) return null;
791- if (!t.float) return { value: v, match: function (x) { return x === v; } };
742+ if (!t.float) return { value: v, match: x => x === v };
792743 const p = precisionFromString(raw), base = Math.round(v / p) * p; // snap to the precision grid
793- if (base < 0) { const lo = base - p, hi = base; return { value: v, match: function (x) { return x > lo && x <= hi; } }; }
794- const lo = base, hi = base + p; return { value: v, match: function (x) { return x >= lo && x < hi; } };
744+ if (base < 0) { const lo = base - p, hi = base; return { value: v, match: x => x > lo && x <= hi }; }
745+ const lo = base, hi = base + p; return { value: v, match: x => x >= lo && x < hi };
795746 }
796747 // Union matcher for a type group ('all'/'allint'/'allfloat'): matches if ANY expanded
797748 // type's exact matcher does. Object-graph values are all f64, so this widens a whole
@@ -799,9 +750,9 @@
799750 // already does per-type — otherwise a group would collapse to typeList[0]'s int matcher.
800751 function makeGroupMatcher(typeList, raw) {
801752 if (raw == null) return null;
802- const ms = typeList.map(function (t) { return makeExactMatcher(t, raw); }).filter(Boolean);
753+ const ms = typeList.map(t => makeExactMatcher(t, raw)).filter(Boolean);
803754 if (!ms.length) return null;
804- return { match: function (x) { for (let i = 0; i < ms.length; i++) if (ms[i].match(x)) return true; return false; } };
755+ return { match: x => ms.some(m => m.match(x)) };
805756 }
806757 // refine criteria: 'exact' uses the typed-precision matcher; the rest compare a
807758 // fresh read `cur` against the stored previous value `prev`.
@@ -815,19 +766,18 @@
815766 default: return false;
816767 }
817768 }
818- function scanValueToWire(v) { return (typeof v === 'bigint') ? v.toString() : v; }
769+ const scanValueToWire = (v) => (typeof v === 'bigint') ? v.toString() : v;
819770
820771 // Run `body()` in slices, yielding to the event loop between them so a scan
821772 // never freezes the frame and can be cancelled mid-flight. body() returns true
822773 // while more work remains; onFinish(cancelled) fires once at the end.
823774 function chunkLoop(job, body, onFinish) {
824- function tick() {
775+ (function tick() {
825776 if (job.cancelled) { onFinish(true); return; }
826777 let more = false;
827778 try { more = body(); } catch (e) { onFinish(false); return; }
828779 if (more) origSetTimeout(tick, 0); else onFinish(false);
829- }
830- tick();
780+ })();
831781 }
832782
833783 /* ---- WASM backend -------------------------------------------------- *
@@ -838,7 +788,7 @@
838788 * cancellable. Up to SCAN_STORE_CAP matches are tracked, so refining a large
839789 * first scan narrows the WHOLE set — not just the first rows shown.
840790 * ------------------------------------------------------------------- */
841- const wasmScan = (function () {
791+ const wasmScan = (() => {
842792 let memIndex = 0; // which wasmMemories entry we're scanning
843793 let types = ['i32']; // type list for the current scan
844794 let candidates = null; // [{ off, val, ty }] or null
@@ -846,43 +796,51 @@
846796 const CHUNK = 8 * 1024 * 1024; // bytes scanned per event-loop slice
847797 const REFINE_BUDGET = 200000; // candidates re-checked per slice during refine
848798
849- function handle() { return wasmMemories[memIndex] || null; }
850- function view() { const h = handle(); try { return h ? new DataView(h.memory.buffer) : null; } catch (e) { return null; } }
851- function matcherCache(raw) { const c = {}; return function (t) { if (!(t in c)) c[t] = (raw != null ? makeExactMatcher(t, raw) : null); return c[t]; }; }
799+ const handle = () => wasmMemories[memIndex] || null;
800+ const buffer = () => { const h = handle(); try { return h ? h.memory.buffer : null; } catch (e) { return null; } };
801+ const view = () => { const b = buffer(); return b ? new DataView(b) : null; };
802+ const matcherCache = (raw) => { const c = {}; return (t) => { if (!(t in c)) c[t] = (raw != null ? makeExactMatcher(t, raw) : null); return c[t]; }; };
852803
853- function reset() { candidates = null; snapshot = null; }
804+ const reset = () => { candidates = null; snapshot = null; };
854805
855806 // Scan the whole buffer for each type in `types`, keeping matches for whichever
856- // `accept(ty, cur, prev)` returns true (prev is the snapshot byte value, or the
857- // same as cur for a fresh exact scan). One CHUNK of one type per slice.
858- function scanAll(accept, prevDv, prevLen, job, done) {
807+ // `accept(ty, cur, prev)` returns true (prev is the snapshot value at the same
808+ // offset, or the same as cur for a fresh exact scan). One CHUNK of one type per
809+ // slice. Bulk reads go through TypedArray views (recreated per slice, since
810+ // memory.grow() detaches the buffer) — several× faster than per-call DataView
811+ // getters, and every scanned offset is type-aligned because we step from 0 by size.
812+ function scanAll(accept, prevBuf, prevLen, job, done) {
859813 const h = handle(); if (!h) { done({ error: 'no WASM memory' }); return; }
860814 let len = 0; try { len = h.memory.buffer.byteLength; } catch (e) {}
861815 const out = []; let count = 0, capped = false, ti = 0, off = 0;
862- chunkLoop(job, function () {
816+ chunkLoop(job, () => {
863817 if (ti >= types.length) return false;
864- const dv = view(); if (!dv) return false;
865- const t = types[ti], sz = SCAN_TYPES[t].size;
866- const cap = prevDv ? Math.min(len, dv.byteLength, prevLen) : Math.min(len, dv.byteLength);
818+ const buf = buffer(); if (!buf) return false;
819+ const t = types[ti], { size: sz, arr: Arr } = SCAN_TYPES[t];
820+ const cap = prevBuf ? Math.min(len, buf.byteLength, prevLen) : Math.min(len, buf.byteLength);
867821 const end = Math.min(cap, off + CHUNK);
868- for (; off + sz <= end; off += sz) {
869- let cur, prev; try { cur = SCAN_TYPES[t].get(dv, off); prev = prevDv ? SCAN_TYPES[t].get(prevDv, off) : cur; } catch (e) { continue; }
870- if (accept(t, cur, prev)) { count++; if (out.length < SCAN_STORE_CAP) out.push({ off: off, val: cur, ty: t }); else capped = true; }
822+ const ta = new Arr(buf, 0, Math.floor(cap / sz)); // may throw on detach → chunkLoop finishes
823+ const pa = prevBuf ? new Arr(prevBuf, 0, Math.floor(cap / sz)) : null;
824+ const stop = Math.floor(end / sz);
825+ for (let i = off / sz; i < stop; i++) {
826+ const cur = ta[i], prev = pa ? pa[i] : cur;
827+ if (accept(t, cur, prev)) { count++; if (out.length < SCAN_STORE_CAP) out.push({ off: i * sz, val: cur, ty: t }); else capped = true; }
871828 }
829+ off = stop * sz;
872830 if (off + sz > cap) { ti++; off = 0; }
873- if (job.onProgress) job.onProgress(Math.min(1, (ti + (cap > 0 ? Math.min(1, off / cap) : 1)) / types.length));
831+ job.onProgress?.(Math.min(1, (ti + (cap > 0 ? Math.min(1, off / cap) : 1)) / types.length));
874832 return ti < types.length;
875- }, function (cancelled) {
833+ }, (cancelled) => {
876834 if (cancelled) { done({ cancelled: true }); return; }
877- candidates = out; done({ count: count, capped: capped, out: out });
835+ candidates = out; done({ count, capped, out });
878836 });
879837 }
880838
881839 function firstExact(typeList, raw, job, done) {
882840 types = typeList.slice(); snapshot = null; candidates = null;
883841 const mfor = matcherCache(raw);
884- if (types.every(function (t) { return !mfor(t); })) { done({ error: 'bad value' }); return; }
885- scanAll(function (t, cur) { const m = mfor(t); return m && m.match(cur); }, null, 0, job, done);
842+ if (types.every(t => !mfor(t))) { done({ error: 'bad value' }); return; }
843+ scanAll((t, cur) => { const m = mfor(t); return m && m.match(cur); }, null, 0, job, done);
886844 }
887845
888846 function firstUnknown(typeList, job, done) {
@@ -895,8 +853,8 @@
895853 // Build the first candidate list from the unknown-scan snapshot by diffing the buffer.
896854 function materialize(criteria, raw, job, done) {
897855 if (!snapshot) { done({ error: 'no snapshot' }); return; }
898- const mfor = matcherCache(raw), prevDv = new DataView(snapshot.buffer), snapLen = snapshot.byteLength;
899- scanAll(function (t, cur, prev) { return passesCriteria(t, criteria, cur, prev, mfor(t)); }, prevDv, snapLen, job, function (r) {
856+ const mfor = matcherCache(raw);
857+ scanAll((t, cur, prev) => passesCriteria(t, criteria, cur, prev, mfor(t)), snapshot.buffer, snapshot.byteLength, job, (r) => {
900858 if (!r.cancelled && !r.error) { try { snapshot = new Uint8Array(handle().memory.buffer.slice(0)); } catch (e) {} } // re-baseline
901859 done(r);
902860 });
@@ -905,20 +863,19 @@
905863 function refine(criteria, raw, job, done) {
906864 if (candidates === null && snapshot !== null) { materialize(criteria, raw, job, done); return; }
907865 if (candidates === null) { done({ error: 'no scan in progress' }); return; }
908- const dv = view(); if (!dv) { done({ error: 'no WASM memory' }); return; }
866+ if (!view()) { done({ error: 'no WASM memory' }); return; }
909867 const mfor = matcherCache(raw), src = candidates, kept = []; let i = 0;
910- chunkLoop(job, function () {
868+ chunkLoop(job, () => {
911869 const d = view(); if (!d) return false;
912- let n = 0;
913- for (; i < src.length && n < REFINE_BUDGET; i++, n++) {
914- const c = src[i], sz = SCAN_TYPES[c.ty].size;
870+ for (let n = 0; i < src.length && n < REFINE_BUDGET; i++, n++) {
871+ const c = src[i], { size: sz, get } = SCAN_TYPES[c.ty];
915872 if (c.off + sz > d.byteLength) continue;
916- let cur; try { cur = SCAN_TYPES[c.ty].get(d, c.off); } catch (e) { continue; }
873+ let cur; try { cur = get(d, c.off); } catch (e) { continue; }
917874 if (passesCriteria(c.ty, criteria, cur, c.val, mfor(c.ty))) kept.push({ off: c.off, val: cur, ty: c.ty });
918875 }
919- if (job.onProgress) job.onProgress(src.length ? i / src.length : 1);
876+ job.onProgress?.(src.length ? i / src.length : 1);
920877 return i < src.length;
921- }, function (cancelled) {
878+ }, (cancelled) => {
922879 if (cancelled) { done({ cancelled: true }); return; }
923880 candidates = kept; done({ count: kept.length, capped: false });
924881 });
@@ -934,24 +891,23 @@
934891 return out;
935892 }
936893 function readAddress(address, t) {
937- const p = String(address).split(':'); const mi = +p[0], off = +p[1], ty = p[2] || t;
938- const h = wasmMemories[mi]; if (!h || !SCAN_TYPES[ty]) return null;
939- try { return scanValueToWire(SCAN_TYPES[ty].get(new DataView(h.memory.buffer), off)); } catch (e) { return null; }
894+ const [mi, off, aty] = String(address).split(':'); const ty = aty || t;
895+ const h = wasmMemories[+mi]; if (!h || !SCAN_TYPES[ty]) return null;
896+ try { return scanValueToWire(SCAN_TYPES[ty].get(new DataView(h.memory.buffer), +off)); } catch (e) { return null; }
940897 }
941898 function writeAddress(address, t, raw) {
942- const p = String(address).split(':'); const mi = +p[0], off = +p[1], ty = p[2] || t;
943- const h = wasmMemories[mi]; if (!h || !SCAN_TYPES[ty]) return false;
899+ const [mi, off, aty] = String(address).split(':'); const ty = aty || t;
900+ const h = wasmMemories[+mi]; if (!h || !SCAN_TYPES[ty]) return false;
944901 const v = parseScanValue(ty, raw); if (v === null) return false;
945- try { SCAN_TYPES[ty].set(new DataView(h.memory.buffer), off, v); return true; } catch (e) { return false; }
902+ try { SCAN_TYPES[ty].set(new DataView(h.memory.buffer), +off, v); return true; } catch (e) { return false; }
946903 }
947- function memories() { return wasmMemories.map(function (m) { let mb = 0; try { mb = m.memory.buffer.byteLength >> 20; } catch (e) {} return { label: m.label + ' (' + mb + ' MB)' }; }); }
948- function setMem(i) { memIndex = i | 0; reset(); }
904+ const memories = () => wasmMemories.map(m => {
905+ let mb = 0; try { mb = m.memory.buffer.byteLength >> 20; } catch (e) {}
906+ return { label: m.label + ' (' + mb + ' MB)' };
907+ });
908+ const setMem = (i) => { memIndex = i | 0; reset(); };
949909
950- return {
951- memories: memories, setMem: setMem, reset: reset,
952- firstExact: firstExact, firstUnknown: firstUnknown, refine: refine,
953- rows: rows, readAddress: readAddress, writeAddress: writeAddress
954- };
910+ return { memories, setMem, reset, firstExact, firstUnknown, refine, rows, readAddress, writeAddress };
955911 })();
956912
957913 /* ---- Object-graph backend ----------------------------------------- *
@@ -960,27 +916,35 @@
960916 * keys). JS numbers are all f64, so the int/float type only tunes equality
961917 * (integer match vs. epsilon) here — noted in the UI.
962918 * ------------------------------------------------------------------- */
963- const objectScan = (function () {
919+ const objectScan = (() => {
964920 const NODE_CAP = 200000, DEPTH_CAP = 12, BUDGET = 15000; // nodes per event-loop slice
965921 let type = 'f64'; // display / default-write type (typeList[0]); values are all f64
966922 let types = ['f64']; // full expanded type list of the current scan (for group matchers)
967923 let candidates = null; // [{ path:[...], val }]
968924 let snapshot = null; // Map(pathKey -> { path, val }) for "unknown initial value"
969925
970- function pathKey(path) { return path.join(' '); }
926+ const pathKey = (path) => path.join(' ');
971927 function resolve(path) {
972928 let o = pageWin;
973929 for (let i = 0; i < path.length; i++) { if (o == null) return undefined; o = o[path[i]]; }
974930 return o;
975931 }
976932 // Iterative, chunked DFS over numeric leaves. cb(path, value); match(value) filters.
933+ // Stack frames are parent-pointer nodes [obj, key, parentNode, depth]; the full path
934+ // array is only materialized for leaves that actually match (path.concat per node
935+ // used to allocate hundreds of thousands of throwaway arrays on a full walk).
977936 function walkAsync(cb, match, job, done) {
978937 const seen = new WeakSet(); let nodes = 0;
979- const stack = [[pageWin, [], 0]];
980- chunkLoop(job, function () {
938+ const stack = [[pageWin, null, null, 0]];
939+ const pathOf = (node, leafKey) => {
940+ const p = [leafKey];
941+ for (let n = node; n && n[1] !== null; n = n[2]) p.push(n[1]);
942+ return p.reverse();
943+ };
944+ chunkLoop(job, () => {
981945 let processed = 0;
982946 while (stack.length && processed < BUDGET && nodes < NODE_CAP) {
983- const fr = stack.pop(); const obj = fr[0], path = fr[1], depth = fr[2]; processed++;
947+ const fr = stack.pop(); const obj = fr[0], depth = fr[3]; processed++;
984948 if (obj == null || depth > DEPTH_CAP) continue;
985949 let keys; try { keys = Object.keys(obj); } catch (e) { continue; }
986950 for (let i = 0; i < keys.length; i++) {
@@ -988,23 +952,22 @@
988952 const k = keys[i]; let v;
989953 try { v = obj[k]; } catch (e) { continue; }
990954 const tv = typeof v;
991- if (tv === 'number') { if (isFinite(v) && (!match || match(v))) cb(path.concat(k), v); }
955+ if (tv === 'number') { if (isFinite(v) && (!match || match(v))) cb(pathOf(fr, k), v); }
992956 else if (tv === 'object' || tv === 'function') {
993957 if (v === null || seen.has(v) || v === pageWin || v === window) continue;
994958 try { if (v.nodeType && v.nodeName) continue; } catch (e) {} // DOM nodes
995959 try { if (ArrayBuffer.isView(v) || v instanceof ArrayBuffer) continue; } catch (e) {}
996960 seen.add(v); nodes++;
997- stack.push([v, path.concat(k), depth + 1]);
961+ stack.push([v, k, fr, depth + 1]);
998962 }
999963 }
1000964 }
1001- if (job.onProgress) job.onProgress(-1); // total is unknown up front → indeterminate
965+ job.onProgress?.(-1); // total is unknown up front → indeterminate
1002966 return stack.length > 0 && nodes < NODE_CAP;
1003967 }, done);
1004968 }
1005969
1006- function reset() { candidates = null; snapshot = null; }
1007- function setMem() {} // n/a for object graph
970+ const reset = () => { candidates = null; snapshot = null; };
1008971
1009972 // All JS numbers are f64, so the width only tunes equality (exact int vs. float
1010973 // cell); a multi-type selection just uses the first type's matcher here.
@@ -1013,56 +976,50 @@
1013976 const matcher = makeGroupMatcher(types, raw);
1014977 if (!matcher) { done({ error: 'bad value' }); return; }
1015978 const out = []; let count = 0, capped = false;
1016- walkAsync(function (path, v) { count++; if (out.length < SCAN_STORE_CAP) out.push({ path: path, val: v }); else capped = true; },
1017- function (v) { return matcher.match(v); }, job, function (cancelled) {
979+ walkAsync((path, v) => { count++; if (out.length < SCAN_STORE_CAP) out.push({ path, val: v }); else capped = true; },
980+ v => matcher.match(v), job, (cancelled) => {
1018981 if (cancelled) { done({ cancelled: true }); return; }
1019- candidates = out; done({ count: count, capped: capped });
982+ candidates = out; done({ count, capped });
1020983 });
1021984 }
1022985 function firstUnknown(typeList, job, done) {
1023986 types = typeList.slice(); type = typeList[0] || 'f64'; candidates = null;
1024987 const snap = new Map();
1025- walkAsync(function (path, v) { snap.set(pathKey(path), { path: path, val: v }); }, null, job, function (cancelled) {
988+ walkAsync((path, v) => snap.set(pathKey(path), { path, val: v }), null, job, (cancelled) => {
1026989 if (cancelled) { done({ cancelled: true }); return; }
1027990 snapshot = snap; done({ count: -1, capped: true });
1028991 });
1029992 }
1030- function materialize(criteria, raw, job, done) {
1031- if (!snapshot) { done({ error: 'no snapshot' }); return; }
993+ // Re-check `entries` ({ path, val }) against a fresh read, chunked so huge candidate
994+ // sets never freeze the frame; used by both materialize (from snapshot, re-baselining)
995+ // and refine (from candidates).
996+ function recheck(entries, criteria, raw, rebaseline, job, done) {
1032997 const matcher = (raw != null) ? makeGroupMatcher(types, raw) : null;
1033- const entries = Array.from(snapshot.values());
1034998 const out = []; let count = 0, capped = false, i = 0;
1035- chunkLoop(job, function () {
1036- let processed = 0;
1037- for (; i < entries.length && processed < BUDGET; i++, processed++) {
999+ chunkLoop(job, () => {
1000+ for (let processed = 0; i < entries.length && processed < BUDGET; i++, processed++) {
10381001 const entry = entries[i]; const cur = resolve(entry.path);
10391002 if (typeof cur !== 'number' || !isFinite(cur)) continue;
10401003 if (passesCriteria(type, criteria, cur, entry.val, matcher)) { count++; if (out.length < SCAN_STORE_CAP) out.push({ path: entry.path, val: cur }); else capped = true; }
1041- entry.val = cur; // re-baseline
1004+ if (rebaseline) entry.val = cur;
10421005 }
1043- if (job.onProgress) job.onProgress(entries.length ? i / entries.length : 1);
1006+ job.onProgress?.(entries.length ? i / entries.length : 1);
10441007 return i < entries.length;
1045- }, function (cancelled) {
1008+ }, (cancelled) => {
10461009 if (cancelled) { done({ cancelled: true }); return; }
1047- candidates = out; done({ count: count, capped: capped });
1010+ candidates = out; done({ count, capped });
10481011 });
10491012 }
10501013 function refine(criteria, raw, job, done) {
1051- if (candidates === null && snapshot !== null) { materialize(criteria, raw, job, done); return; }
1014+ if (candidates === null && snapshot !== null) { recheck(Array.from(snapshot.values()), criteria, raw, true, job, done); return; }
10521015 if (candidates === null) { done({ error: 'no scan in progress' }); return; }
1053- const matcher = (raw != null) ? makeGroupMatcher(types, raw) : null;
1054- const kept = [];
1055- for (let i = 0; i < candidates.length; i++) {
1056- const c = candidates[i]; const cur = resolve(c.path);
1057- if (typeof cur === 'number' && isFinite(cur) && passesCriteria(type, criteria, cur, c.val, matcher)) kept.push({ path: c.path, val: cur });
1058- }
1059- candidates = kept; done({ count: kept.length, capped: false });
1016+ recheck(candidates, criteria, raw, false, job, done);
10601017 }
10611018 function rows(limit) {
10621019 const out = [], list = candidates || [];
10631020 for (let i = 0; i < list.length && i < limit; i++) {
10641021 const cur = resolve(list[i].path);
1065- out.push({ address: JSON.stringify(list[i].path), value: (typeof cur === 'number' ? cur : null), type: type });
1022+ out.push({ address: JSON.stringify(list[i].path), value: (typeof cur === 'number' ? cur : null), type });
10661023 }
10671024 return out;
10681025 }
@@ -1076,22 +1033,19 @@
10761033 try { const parent = resolve(path.slice(0, -1)); if (parent == null) return false; parent[path[path.length - 1]] = (typeof v === 'bigint' ? Number(v) : v); return true; }
10771034 catch (e) { return false; }
10781035 }
1079- function memories() { return []; }
1036+ const memories = () => [];
1037+ const setMem = () => {}; // n/a for object graph
10801038
1081- return {
1082- memories: memories, setMem: setMem, reset: reset,
1083- firstExact: firstExact, firstUnknown: firstUnknown, refine: refine,
1084- rows: rows, readAddress: readAddress, writeAddress: writeAddress
1085- };
1039+ return { memories, setMem, reset, firstExact, firstUnknown, refine, rows, readAddress, writeAddress };
10861040 })();
10871041
1088- function scanEngine(name) { return name === 'object' ? objectScan : wasmScan; }
1042+ const scanEngine = (name) => name === 'object' ? objectScan : wasmScan;
10891043
10901044 // Address labels for display (wasm "mi:off:ty" → "mem#mi +0x.. (ty)"; object path → "window...").
10911045 function scanAddressLabel(engine, address) {
10921046 if (engine === 'object') { try { return 'window.' + JSON.parse(address).join('.'); } catch (e) { return String(address); } }
1093- const p = String(address).split(':');
1094- return 'mem#' + p[0] + ' +0x' + (Number(p[1]) || 0).toString(16) + (p[2] ? ' (' + p[2] + ')' : '');
1047+ const [mi, off, ty] = String(address).split(':');
1048+ return 'mem#' + mi + ' +0x' + (Number(off) || 0).toString(16) + (ty ? ' (' + ty + ')' : '');
10951049 }
10961050
10971051 // Execute one scan command against the LOCAL engines; resolves a wire-safe result.
@@ -1100,42 +1054,42 @@
11001054 const SCAN_ROW_LIMIT = 200; // most rows we ship/render at once
11011055 let scanJob = null;
11021056 function runScanCommand(cmd, onProgress) {
1103- return new Promise(function (resolve) {
1057+ return new Promise((resolve) => {
11041058 try {
11051059 const eng = scanEngine(cmd.engine);
1060+ const cancelJob = () => { if (scanJob) scanJob.cancelled = true; };
11061061 switch (cmd.op) {
11071062 case 'ping': resolve({ ok: true, pong: true }); return;
11081063 case 'list-memories': resolve({ ok: true, memories: wasmScan.memories() }); return;
1109- case 'set-mem': if (scanJob) scanJob.cancelled = true; eng.setMem(cmd.mem | 0); resolve({ ok: true }); return;
1110- case 'reset': if (scanJob) scanJob.cancelled = true; eng.reset(); resolve({ ok: true, count: 0, rows: [] }); return;
1111- case 'cancel': if (scanJob) scanJob.cancelled = true; resolve({ ok: true, cancelled: true }); return;
1112- case 'set-paused': setPaused(cmd.value); resolve({ ok: true, paused: paused }); return;
1113- case 'read': {
1114- const vals = (cmd.addresses || []).map(function (a) { return { address: a, value: scanValueToWire(eng.readAddress(a, cmd.type)) }; });
1115- resolve({ ok: true, values: vals }); return;
1116- }
1064+ case 'set-mem': cancelJob(); eng.setMem(cmd.mem | 0); resolve({ ok: true }); return;
1065+ case 'reset': cancelJob(); eng.reset(); resolve({ ok: true, count: 0, rows: [] }); return;
1066+ case 'cancel': cancelJob(); resolve({ ok: true, cancelled: true }); return;
1067+ case 'set-paused': setPaused(cmd.value); resolve({ ok: true, paused }); return;
1068+ case 'read':
1069+ resolve({ ok: true, values: (cmd.addresses || []).map(a => ({ address: a, value: scanValueToWire(eng.readAddress(a, cmd.type)) })) });
1070+ return;
11171071 case 'write': {
11181072 const ok = eng.writeAddress(cmd.address, cmd.type, cmd.value);
1119- resolve({ ok: ok, value: scanValueToWire(eng.readAddress(cmd.address, cmd.type)) }); return;
1073+ resolve({ ok, value: scanValueToWire(eng.readAddress(cmd.address, cmd.type)) }); return;
11201074 }
11211075 case 'first-exact': case 'first-unknown': case 'refine': {
1122- if (scanJob) scanJob.cancelled = true; // supersede any prior scan
1076+ cancelJob(); // supersede any prior scan
11231077 const job = { cancelled: false, onProgress: onProgress || null }; scanJob = job;
1124- const done = function (r) {
1078+ const done = (r) => {
11251079 if (scanJob === job) scanJob = null;
1126- if (!r || r.error) { resolve({ ok: false, error: (r && r.error) || 'scan failed' }); return; }
1080+ if (!r || r.error) { resolve({ ok: false, error: r?.error || 'scan failed' }); return; }
11271081 if (r.cancelled) { resolve({ ok: true, cancelled: true }); return; }
11281082 resolve({ ok: true, count: r.count, capped: r.capped, rows: eng.rows(SCAN_ROW_LIMIT) });
11291083 };
11301084 const types = expandTypes(cmd.type);
11311085 if (cmd.op === 'first-exact') eng.firstExact(types, cmd.value, job, done);
11321086 else if (cmd.op === 'first-unknown') eng.firstUnknown(types, job, done);
1133- else eng.refine(cmd.criteria, (cmd.value != null ? cmd.value : null), job, done);
1087+ else eng.refine(cmd.criteria, cmd.value ?? null, job, done);
11341088 return;
11351089 }
11361090 default: resolve({ ok: false, error: 'unknown op' }); return;
11371091 }
1138- } catch (e) { resolve({ ok: false, error: String(e && e.message || e) }); }
1092+ } catch (e) { resolve({ ok: false, error: String(e?.message || e) }); }
11391093 });
11401094 }
11411095
@@ -1155,7 +1109,7 @@
11551109 const scanHandledQ = []; // FIFO to bound scanHandled
11561110 function sendScan(targetId, targetWin, cmd, onProgress) {
11571111 if (!targetId) return runScanCommand(cmd, onProgress); // null → this frame (local engine, no messaging)
1158- return new Promise(function (resolve) {
1112+ return new Promise((resolve) => {
11591113 const reqId = scanSeq++;
11601114 scanPending.set(reqId, resolve);
11611115 if (onProgress) scanProgress.set(reqId, onProgress);
@@ -1163,12 +1117,11 @@
11631117 // the scan value-type field is also called `type` and would otherwise overwrite
11641118 // the envelope's `type: 'scan-cmd'`, so the target saw an unknown message type
11651119 // and silently dropped every remote scan (v1.4.5 fix).
1166- const msg = { type: 'scan-cmd', reqId: reqId, targetFrame: targetId, from: SELF_ID, cmd: cmd };
1167- if (targetWin) { try { postTo(targetWin, msg); } catch (e) {} } // proven channel (clicker/settings use it)
1168- broadcastScanMsg(msg); // + frame-tree broadcast as backup
1120+ const msg = { type: 'scan-cmd', reqId, targetFrame: targetId, from: SELF_ID, cmd };
1121+ if (targetWin) postTo(targetWin, msg); // proven channel (clicker/settings use it)
1122+ broadcastScanMsg(msg); // + frame-tree broadcast as backup
11691123 });
11701124 }
1171-
11721125 /* ------------------------------------------------------------------ *
11731126 * UI (Shadow DOM)
11741127 * ------------------------------------------------------------------ */
@@ -1447,104 +1400,105 @@
14471400 // listener registered on window/document in the CAPTURE phase still sees the event —
14481401 // nothing in the same DOM can prevent that; detaching into the game's own frame, or
14491402 // an input-isolating iframe, would be the only full fix.)
1450- ['pointerdown','pointerup','mousedown','mouseup','click','dblclick','contextmenu',
1451- 'keydown','keyup','keypress','wheel','touchstart','touchend','pointermove','mousemove'
1452- ].forEach(function (t) { host.addEventListener(t, function (e) { e.stopPropagation(); }, false); });
1453-
1454- const $ = function (s) { return root.querySelector(s); };
1455- const panel = $('#panel'), bar = $('#bar'), badge = $('#badge'), title = $('#title');
1456- const range = $('#scaleRange'), num = $('#scaleNum'), out = $('#scaleOut');
1457- const btnMin = $('#min'), btnClose = $('#close'), dlg = $('#dlg'), status = $('#status');
1458- const framesBox = $('#framesBox'), frameList = $('#frameList'), reattachBtn = $('#reattach');
1403+ for (const t of ['pointerdown', 'pointerup', 'mousedown', 'mouseup', 'click', 'dblclick', 'contextmenu',
1404+ 'keydown', 'keyup', 'keypress', 'wheel', 'touchstart', 'touchend', 'pointermove', 'mousemove'])
1405+ host.addEventListener(t, e => e.stopPropagation(), false);
1406+
1407+ // Every [id] in the template, keyed by id: ui.panel, ui.scaleRange, ui.clkCps, …
1408+ const ui = {};
1409+ root.querySelectorAll('[id]').forEach(n => { ui[n.id] = n; });
1410+ const el = (tag, cls, text) => {
1411+ const n = document.createElement(tag);
1412+ if (cls) n.className = cls;
1413+ if (text != null) n.textContent = text;
1414+ return n;
1415+ };
1416+ const { panel, bar } = ui;
14591417
14601418 // status line — confirms we reached the page's real context
1461- if (hasUnsafe) { status.className = 'status ok'; status.textContent = '✓ patching page context (unsafeWindow)'; }
1462- else { status.className = 'status warn'; status.textContent = '⚠ unsafeWindow not found — using this window. If nothing speeds up, the manager is sandboxing the script.'; }
1419+ if (hasUnsafe) { ui.status.className = 'status ok'; ui.status.textContent = '✓ patching page context (unsafeWindow)'; }
1420+ else { ui.status.className = 'status warn'; ui.status.textContent = '⚠ unsafeWindow not found — using this window. If nothing speeds up, the manager is sandboxing the script.'; }
14631421
14641422 // toggles (the 5 hooks + turbo)
1465- const tg = $('#toggles');
1466- const hookInputs = {};
1467- Object.keys(hooks).forEach(function (name) {
1468- const lab = document.createElement('label');
1469- lab.className = 'tg';
1470- lab.innerHTML = '<input type="checkbox" ' + (state[name] ? 'checked' : '') + '><span>' + hooks[name].label + '</span>';
1423+ const makeToggle = (text, checked, onChange) => {
1424+ const lab = el('label', 'tg');
1425+ lab.innerHTML = '<input type="checkbox"><span></span>';
14711426 const inp = lab.querySelector('input');
1472- hookInputs[name] = inp;
1473- inp.addEventListener('change', function (e) {
1474- setHook(name, e.target.checked);
1427+ inp.checked = checked;
1428+ lab.querySelector('span').textContent = text;
1429+ inp.addEventListener('change', e => onChange(e.target.checked));
1430+ ui.toggles.appendChild(lab);
1431+ return { lab, inp };
1432+ };
1433+ const hookInputs = {};
1434+ for (const name of Object.keys(hooks)) {
1435+ hookInputs[name] = makeToggle(hooks[name].label, state[name], (on) => {
1436+ setHook(name, on);
14751437 if (name === 'raf') updateTurboEnabled();
14761438 broadcastSettings();
1477- });
1478- tg.appendChild(lab);
1479- });
1480- const tlab = document.createElement('label');
1481- tlab.className = 'tg';
1482- tlab.innerHTML = '<input type="checkbox"><span>rAF turbo — multi-step (experimental)</span>';
1483- const turboInput = tlab.querySelector('input');
1484- turboInput.checked = turbo;
1485- turboInput.addEventListener('change', function (e) { turbo = e.target.checked; turboT = null; broadcastSettings(); });
1486- tg.appendChild(tlab);
1439+ }).inp;
1440+ }
1441+ const turboTg = makeToggle('rAF turbo — multi-step (experimental)', turbo, (on) => { turbo = on; turboT = null; broadcastSettings(); });
1442+ const turboInput = turboTg.inp;
14871443
14881444 // turbo only does anything while the rAF hook is installed
14891445 function updateTurboEnabled() {
14901446 const on = !!state.raf;
14911447 turboInput.disabled = !on;
1492- tlab.classList.toggle('disabled', !on);
1448+ turboTg.lab.classList.toggle('disabled', !on);
14931449 }
14941450 updateTurboEnabled();
14951451
14961452 // scale
1497- function reflect(v) { out.textContent = v + '×'; badge.textContent = v + '×'; }
1453+ function reflect(v) { ui.scaleOut.textContent = v + '×'; ui.badge.textContent = v + '×'; }
14981454 const MAX_SCALE = 1000, SLIDER_MAX = 100, SLIDER_MIN = 0.1;
1455+ const clampSlider = (v) => Math.min(SLIDER_MAX, Math.max(SLIDER_MIN, v));
14991456 // writeNum=false while the user is typing into the number field, so we don't
15001457 // clobber the caret / intermediate input — that field is normalized on commit.
15011458 function onScale(v, writeNum) {
15021459 v = Number(v); if (!isFinite(v) || v <= 0) return;
15031460 if (v > MAX_SCALE) v = MAX_SCALE; // hard cap, incl. typed-in numbers
15041461 applyScale(v);
1505- range.value = Math.min(SLIDER_MAX, Math.max(SLIDER_MIN, v));
1506- if (writeNum !== false) num.value = v;
1462+ ui.scaleRange.value = clampSlider(v);
1463+ if (writeNum !== false) ui.scaleNum.value = v;
15071464 reflect(v);
15081465 broadcastSettings();
15091466 }
1510- range.addEventListener('input', function (e) { onScale(e.target.value); });
1511- num.addEventListener('input', function (e) { onScale(e.target.value, false); });
1512- num.addEventListener('change', function (e) { onScale(e.target.value); }); // normalize + cap on commit
1513- root.querySelectorAll('.presets button').forEach(function (b) {
1514- b.addEventListener('click', function () { onScale(b.dataset.s); });
1467+ ui.scaleRange.addEventListener('input', e => onScale(e.target.value));
1468+ ui.scaleNum.addEventListener('input', e => onScale(e.target.value, false));
1469+ ui.scaleNum.addEventListener('change', e => onScale(e.target.value)); // normalize + cap on commit
1470+ root.querySelectorAll('.presets button').forEach(b => {
1471+ b.addEventListener('click', () => onScale(b.dataset.s));
15151472 });
15161473
15171474 // pull controls back in line with current state (used when settings arrive remotely)
15181475 function sync() {
1519- Object.keys(hookInputs).forEach(function (n) { hookInputs[n].checked = !!state[n]; });
1476+ for (const n of Object.keys(hookInputs)) hookInputs[n].checked = !!state[n];
15201477 turboInput.checked = turbo;
15211478 updateTurboEnabled();
1522- range.value = Math.min(SLIDER_MAX, Math.max(SLIDER_MIN, scale));
1523- num.value = scale;
1479+ ui.scaleRange.value = clampSlider(scale);
1480+ ui.scaleNum.value = scale;
15241481 reflect(scale);
15251482 }
15261483
15271484 // frame list (host mode) — one row per child frame that has announced itself
15281485 function refreshFrames() {
15291486 refreshScanTargets();
1530- if (curMode !== 'host') { framesBox.hidden = true; return; }
1487+ if (curMode !== 'host') { ui.framesBox.hidden = true; return; }
15311488 pruneFrames();
1532- frameList.textContent = '';
1533- if (frames.size === 0) { framesBox.hidden = true; return; }
1534- framesBox.hidden = false;
1535- frames.forEach(function (f, src) {
1536- const row = document.createElement('div'); row.className = 'frow';
1537- const label = document.createElement('span'); label.className = 'furl';
1538- label.textContent = shortUrl(f.url); label.title = f.url;
1539- const btn = document.createElement('button');
1540- btn.textContent = f.attached ? 'Detach' : 'Re-attach';
1541- btn.addEventListener('click', function () { f.attached ? detachFrame(src) : reattachFrame(src); });
1542- row.appendChild(label); row.appendChild(btn);
1543- frameList.appendChild(row);
1489+ ui.frameList.textContent = '';
1490+ ui.framesBox.hidden = frames.size === 0;
1491+ frames.forEach((f, src) => {
1492+ const row = el('div', 'frow');
1493+ const label = el('span', 'furl', shortUrl(f.url)); label.title = f.url;
1494+ const btn = el('button', '', f.attached ? 'Detach' : 'Re-attach');
1495+ btn.addEventListener('click', () => { f.attached ? detachFrame(src) : reattachFrame(src); });
1496+ row.append(label, btn);
1497+ ui.frameList.appendChild(row);
15441498 });
15451499 }
15461500
1547- reattachBtn.addEventListener('click', function () {
1501+ ui.reattach.addEventListener('click', () => {
15481502 if (hostWin) postTo(hostWin, { type: 'reattach' });
15491503 attached = true;
15501504 destroyPanel(); // back to headless; host will resend settings
@@ -1552,17 +1506,15 @@
15521506
15531507 // tabs
15541508 const panes = {};
1555- root.querySelectorAll('.pane').forEach(function (p) { panes[p.dataset.pane] = p; });
1509+ root.querySelectorAll('.pane').forEach(p => { panes[p.dataset.pane] = p; });
15561510 const tabBtns = root.querySelectorAll('.tab');
15571511 function setTab(name) {
1558- tabBtns.forEach(function (b) { b.classList.toggle('active', b.dataset.tab === name); });
1559- Object.keys(panes).forEach(function (n) { panes[n].hidden = (n !== name); });
1512+ tabBtns.forEach(b => b.classList.toggle('active', b.dataset.tab === name));
1513+ for (const n of Object.keys(panes)) panes[n].hidden = (n !== name);
15601514 }
1561- tabBtns.forEach(function (b) { b.addEventListener('click', function () { setTab(b.dataset.tab); }); });
1515+ tabBtns.forEach(b => b.addEventListener('click', () => setTab(b.dataset.tab)));
15621516
15631517 /* ----- clicker controls ----- */
1564- const clkStatus = $('#clkStatus'), clkKey = $('#clkKey'), clkSet = $('#clkSet'), clkClear = $('#clkClear');
1565- const clkSwallow = $('#clkSwallow'), clkCps = $('#clkCps'), clkJitter = $('#clkJitter'), clkHold = $('#clkHold');
15661518 const clkModeBtns = root.querySelectorAll('#clkMode button');
15671519
15681520 function keyLabel(hk) {
@@ -1571,60 +1523,50 @@
15711523 (hk.key === ' ' ? 'Space' : hk.key);
15721524 }
15731525 function syncClicker() {
1574- clkModeBtns.forEach(function (b) { b.classList.toggle('on', b.dataset.mode === clicker.mode); });
1575- clkKey.textContent = clicker.listening ? 'press a key…' : keyLabel(clicker.hotkey);
1576- clkKey.classList.toggle('listening', clicker.listening);
1577- clkSwallow.checked = clicker.swallowHotkey;
1526+ clkModeBtns.forEach(b => b.classList.toggle('on', b.dataset.mode === clicker.mode));
1527+ ui.clkKey.textContent = clicker.listening ? 'press a key…' : keyLabel(clicker.hotkey);
1528+ ui.clkKey.classList.toggle('listening', clicker.listening);
1529+ ui.clkSwallow.checked = clicker.swallowHotkey;
15781530 // root.activeElement (not document.activeElement) sees focus *inside* the shadow root,
15791531 // so we don't overwrite a field the user is currently typing into.
1580- if (root.activeElement !== clkCps) clkCps.value = clicker.cps;
1581- if (root.activeElement !== clkJitter) clkJitter.value = clicker.jitterMs;
1582- if (root.activeElement !== clkHold) clkHold.value = clicker.holdMs;
1583- clkStatus.className = 'status ' + (clicker.running ? 'run' : 'idle');
1584- clkStatus.textContent = clicker.running
1532+ if (root.activeElement !== ui.clkCps) ui.clkCps.value = clicker.cps;
1533+ if (root.activeElement !== ui.clkJitter) ui.clkJitter.value = clicker.jitterMs;
1534+ if (root.activeElement !== ui.clkHold) ui.clkHold.value = clicker.holdMs;
1535+ ui.clkStatus.className = 'status ' + (clicker.running ? 'run' : 'idle');
1536+ ui.clkStatus.textContent = clicker.running
15851537 ? '● clicking — ' + Math.round(clicker.cps) + '/s at cursor'
15861538 : (clicker.hotkey ? '○ idle — press ' + keyLabel(clicker.hotkey) + ' to ' + (clicker.mode === 'hold' ? 'hold' : 'toggle')
15871539 : '○ idle — set a hotkey to start');
15881540 }
1589- clkModeBtns.forEach(function (b) {
1590- b.addEventListener('click', function () {
1541+ clkModeBtns.forEach(b => {
1542+ b.addEventListener('click', () => {
15911543 clicker.mode = b.dataset.mode;
15921544 if (clicker.running) setClickerRunning(false, true); // mode switch is a clean stop
15931545 syncClicker(); broadcastClickerConfig();
15941546 });
15951547 });
1596- clkSet.addEventListener('click', function () { clicker.listening = true; syncClicker(); });
1597- clkClear.addEventListener('click', function () {
1548+ ui.clkSet.addEventListener('click', () => { clicker.listening = true; syncClicker(); });
1549+ ui.clkClear.addEventListener('click', () => {
15981550 if (clicker.running) setClickerRunning(false, true);
15991551 clicker.hotkey = null; clicker.listening = false; syncClicker(); broadcastClickerConfig();
16001552 });
1601- clkSwallow.addEventListener('change', function (e) { clicker.swallowHotkey = e.target.checked; broadcastClickerConfig(); });
1602- function commitNum(input, key, min, max) {
1603- let v = Number(input.value);
1604- if (!isFinite(v)) return;
1605- v = Math.min(max, Math.max(min, v));
1606- clicker[key] = v;
1607- broadcastClickerConfig();
1553+ ui.clkSwallow.addEventListener('change', e => { clicker.swallowHotkey = e.target.checked; broadcastClickerConfig(); });
1554+ // Each numeric field commits live on input (clamped) and snaps back to the
1555+ // accepted value on change (blur/Enter).
1556+ for (const [input, key, min, max] of [[ui.clkCps, 'cps', 0.1, MAX_CPS], [ui.clkJitter, 'jitterMs', 0, 2000], [ui.clkHold, 'holdMs', 0, 2000]]) {
1557+ input.addEventListener('input', () => {
1558+ const v = Number(input.value);
1559+ if (!isFinite(v)) return;
1560+ clicker[key] = Math.min(max, Math.max(min, v));
1561+ broadcastClickerConfig();
1562+ });
1563+ input.addEventListener('change', () => { input.value = clicker[key]; });
16081564 }
1609- clkCps.addEventListener('input', function () { commitNum(clkCps, 'cps', 0.1, MAX_CPS); });
1610- clkCps.addEventListener('change', function () { clkCps.value = clicker.cps; });
1611- clkJitter.addEventListener('input', function () { commitNum(clkJitter, 'jitterMs', 0, 2000); });
1612- clkJitter.addEventListener('change', function () { clkJitter.value = clicker.jitterMs; });
1613- clkHold.addEventListener('input', function () { commitNum(clkHold, 'holdMs', 0, 2000); });
1614- clkHold.addEventListener('change', function () { clkHold.value = clicker.holdMs; });
16151565 syncClicker();
16161566
16171567 /* ----- scan controls ----- */
1618- const scTarget = $('#scTarget'), scPause = $('#scPause'), scStatus = $('#scStatus');
1619- const scMem = $('#scMem'), scMemDetect = $('#scMemDetect'), scType = $('#scType'), scValue = $('#scValue');
1620- const scFirst = $('#scFirst'), scCancel = $('#scCancel');
1621- const scProgress = $('#scProgress'), scBar = $('#scBar');
16221568 const scModeInputs = root.querySelectorAll('#scMode input');
1623- const scRefine = $('#scRefine'), scCount = $('#scCount'), scResults = $('#scResults'), scSaved = $('#scSaved');
1624- function getScMode() {
1625- for (let i = 0; i < scModeInputs.length; i++) if (scModeInputs[i].checked) return scModeInputs[i].value;
1626- return 'exact';
1627- }
1569+ const getScMode = () => [...scModeInputs].find(r => r.checked)?.value || 'exact';
16281570 const scEngineBtns = root.querySelectorAll('#scEngine button');
16291571 const scRefineBtns = root.querySelectorAll('#scRefine button');
16301572
@@ -1638,36 +1580,34 @@
16381580 let pausedLocalView = false;
16391581
16401582 function scCmd(extra, onProgress) {
1641- const cmd = { engine: scEngineName, type: scType.value };
1642- for (const k in extra) cmd[k] = extra[k];
1643- return sendScan(scTargetId, scTargetWin, cmd, onProgress);
1583+ return sendScan(scTargetId, scTargetWin, { engine: scEngineName, type: ui.scType.value, ...extra }, onProgress);
16441584 }
16451585
16461586 // Two independent status lines so they never clobber each other:
16471587 // - setConn(): frame / WASM-memory / pause state (top, #scStatus)
16481588 // - setScan(): scan progress + result counts (#scCount)
1649- function setConn(kind, text) { scStatus.className = 'status ' + kind; scStatus.textContent = text; }
1650- function setScan(text) { scCount.textContent = text; }
1589+ function setConn(kind, text) { ui.scStatus.className = 'status ' + kind; ui.scStatus.textContent = text; }
1590+ function setScan(text) { ui.scCount.textContent = text; }
16511591
16521592 function refreshScanTargets() {
1653- const prev = scTarget.value;
1593+ const prev = ui.scTarget.value;
16541594 scTargetList.length = 0;
1655- scTarget.textContent = '';
1656- const self = document.createElement('option'); self.value = 'self'; self.textContent = 'This frame';
1657- scTarget.appendChild(self);
1595+ ui.scTarget.textContent = '';
1596+ const self = el('option', '', 'This frame'); self.value = 'self';
1597+ ui.scTarget.appendChild(self);
16581598 // `frames` is only populated in host mode; a detached child just sees itself.
1659- frames.forEach(function (f, src) {
1599+ frames.forEach((f, src) => {
16601600 if (!f.id) return;
16611601 const idx = scTargetList.push({ id: f.id, win: src }) - 1;
1662- const opt = document.createElement('option');
1663- opt.value = 'f' + idx; opt.textContent = shortUrl(f.url); opt.title = f.url;
1664- scTarget.appendChild(opt);
1602+ const opt = el('option', '', shortUrl(f.url));
1603+ opt.value = 'f' + idx; opt.title = f.url;
1604+ ui.scTarget.appendChild(opt);
16651605 });
1666- scTarget.value = Array.prototype.some.call(scTarget.options, function (o) { return o.value === prev; }) ? prev : 'self';
1606+ ui.scTarget.value = [...ui.scTarget.options].some(o => o.value === prev) ? prev : 'self';
16671607 applyTargetSelection();
16681608 }
16691609 function applyTargetSelection() {
1670- const v = scTarget.value;
1610+ const v = ui.scTarget.value;
16711611 const ent = (v === 'self') ? null : scTargetList[+v.slice(1)];
16721612 scTargetId = ent ? ent.id : null;
16731613 scTargetWin = ent ? ent.win : null;
@@ -1675,27 +1615,27 @@
16751615
16761616 // Reflect scanActive / scanRunning onto the buttons.
16771617 function updateButtons() {
1678- scFirst.textContent = scanActive ? 'New scan' : 'First scan';
1679- scFirst.disabled = scanRunning;
1618+ ui.scFirst.textContent = scanActive ? 'New scan' : 'First scan';
1619+ ui.scFirst.disabled = scanRunning;
16801620 // Scan mode (Known value / Unknown initial) is locked once a scan exists — it only
16811621 // applies to starting a NEW scan. Start a new scan to change it.
1682- scModeInputs.forEach(function (r) { r.disabled = scanRunning || scanActive; });
1683- scCancel.hidden = !scanRunning;
1684- scRefine.hidden = !scanActive;
1685- scRefineBtns.forEach(function (b) { b.disabled = scanRunning || !scanActive; });
1686- scType.disabled = scanRunning;
1622+ scModeInputs.forEach(r => { r.disabled = scanRunning || scanActive; });
1623+ ui.scCancel.hidden = !scanRunning;
1624+ ui.scRefine.hidden = !scanActive;
1625+ scRefineBtns.forEach(b => { b.disabled = scanRunning || !scanActive; });
1626+ ui.scType.disabled = scanRunning;
16871627 // The value box stays editable during refine (so "Exact" refine can take a new value);
16881628 // it's disabled only while a scan runs, or for a fresh "Unknown initial" scan (no value).
1689- scValue.disabled = scanRunning || (!scanActive && getScMode() === 'unknown');
1690- scMemDetect.disabled = scanRunning;
1629+ ui.scValue.disabled = scanRunning || (!scanActive && getScMode() === 'unknown');
1630+ ui.scMemDetect.disabled = scanRunning;
16911631 }
16921632
16931633 function setEngine(name) {
16941634 scEngineName = name;
1695- scEngineBtns.forEach(function (b) { b.classList.toggle('on', b.dataset.engine === name); });
1635+ scEngineBtns.forEach(b => b.classList.toggle('on', b.dataset.engine === name));
16961636 const wasm = (name === 'wasm');
1697- scMemDetect.hidden = !wasm;
1698- scMem.hidden = true;
1637+ ui.scMemDetect.hidden = !wasm;
1638+ ui.scMem.hidden = true;
16991639 newScan();
17001640 if (wasm) { refreshMemList(); return; }
17011641 // object-graph engine: confirm a remote frame is reachable (same short
@@ -1703,13 +1643,13 @@
17031643 if (!scTargetId) { setConn('idle', 'Walking values reachable from this frame’s window.'); return; }
17041644 const id = scTargetId; let settled = false;
17051645 setConn('idle', 'Connecting to frame…');
1706- origSetTimeout(function () {
1646+ origSetTimeout(() => {
17071647 if (settled || scTargetId !== id || scEngineName !== 'object') return;
17081648 settled = true; setConn('warn', 'No response from that frame. If it just loaded, re-select it; otherwise open the frame’s own panel and scan there.');
17091649 }, 2500);
1710- scCmd({ op: 'ping' }).then(function (res) {
1650+ scCmd({ op: 'ping' }).then((res) => {
17111651 if (settled || scTargetId !== id || scEngineName !== 'object') return;
1712- settled = true; setConn(res && res.ok ? 'ok' : 'warn', res && res.ok ? 'Frame connected — walking values reachable from its window.' : 'Frame not responding.');
1652+ settled = true; setConn(res?.ok ? 'ok' : 'warn', res?.ok ? 'Frame connected — walking values reachable from its window.' : 'Frame not responding.');
17131653 });
17141654 }
17151655
@@ -1721,22 +1661,22 @@
17211661 if (scEngineName !== 'wasm') return;
17221662 const id = scTargetId; let settled = false;
17231663 setConn('idle', 'Detecting WASM memory…');
1724- if (id) origSetTimeout(function () {
1664+ if (id) origSetTimeout(() => {
17251665 if (settled || scTargetId !== id || scEngineName !== 'wasm') return;
1726- settled = true; scMem.hidden = true;
1666+ settled = true; ui.scMem.hidden = true;
17271667 setConn('warn', 'No response from that frame. If the game is still loading, click “Detect” again; otherwise open the frame’s own panel and scan there.');
17281668 }, 2500);
1729- scCmd({ op: 'list-memories' }).then(function (res) {
1669+ scCmd({ op: 'list-memories' }).then((res) => {
17301670 if (settled || scEngineName !== 'wasm' || scTargetId !== id) return; // stale/superseded
17311671 settled = true;
1732- if (!res || res.ok === false) { scMem.hidden = true; setConn('warn', id ? 'Target frame not responding — try detaching its panel and scanning there.' : 'Frame not responding.'); return; }
1672+ if (!res || res.ok === false) { ui.scMem.hidden = true; setConn('warn', id ? 'Target frame not responding — try detaching its panel and scanning there.' : 'Frame not responding.'); return; }
17331673 const mems = res.memories || [];
1734- const prev = scMem.value;
1735- scMem.textContent = '';
1736- mems.forEach(function (m, i) { const o = document.createElement('option'); o.value = String(i); o.textContent = m.label; scMem.appendChild(o); });
1737- if (prev && mems[+prev]) scMem.value = prev;
1738- scMem.hidden = mems.length === 0;
1739- scMemDetect.textContent = mems.length ? '↻ Re-detect WASM memory' : '↻ Detect WASM memory';
1674+ const prev = ui.scMem.value;
1675+ ui.scMem.textContent = '';
1676+ mems.forEach((m, i) => { const o = el('option', '', m.label); o.value = String(i); ui.scMem.appendChild(o); });
1677+ if (prev && mems[+prev]) ui.scMem.value = prev;
1678+ ui.scMem.hidden = mems.length === 0;
1679+ ui.scMemDetect.textContent = mems.length ? '↻ Re-detect WASM memory' : '↻ Detect WASM memory';
17401680 if (mems.length === 0) setConn('warn', 'No WASM memory' + (id ? ' in that frame' : '') + ' yet. If the game is still loading, click “Detect” again.');
17411681 else setConn('ok', mems.length + ' WASM memor' + (mems.length === 1 ? 'y' : 'ies') + ' found.');
17421682 });
@@ -1744,7 +1684,7 @@
17441684
17451685 function newScan() {
17461686 scanActive = false;
1747- scResults.textContent = '';
1687+ ui.scResults.textContent = '';
17481688 setScan('No scan yet.');
17491689 showProgress(false);
17501690 updateButtons();
@@ -1755,7 +1695,7 @@
17551695
17561696 function onScanDone(res) {
17571697 setScanRunning(false);
1758- if (!res || !res.ok) { setScan('Scan failed: ' + ((res && res.error) || 'unknown')); return; }
1698+ if (!res || !res.ok) { setScan('Scan failed: ' + (res?.error || 'unknown')); return; }
17591699 if (res.cancelled) { setScan('Scan cancelled.'); updateButtons(); return; }
17601700 scanActive = true;
17611701 updateButtons();
@@ -1774,51 +1714,51 @@
17741714 setScanRunning(true);
17751715 setScan(progress);
17761716 showProgress(true);
1777- scCmd(cmd, onScanProgress).then(function (res) { showProgress(false); onScanDone(res); });
1717+ scCmd(cmd, onScanProgress).then((res) => { showProgress(false); onScanDone(res); });
17781718 }
17791719 // Progress bar: starts indeterminate (animated stripe); flips to a determinate fill the
17801720 // first time the engine reports a real fraction (frac >= 0). frac < 0 stays indeterminate
17811721 // (the object-graph walk, whose total isn't known up front).
17821722 function showProgress(on) {
1783- scProgress.hidden = !on;
1784- if (on) { scBar.classList.add('indet'); scBar.style.width = '0%'; }
1723+ ui.scProgress.hidden = !on;
1724+ if (on) { ui.scBar.classList.add('indet'); ui.scBar.style.width = '0%'; }
17851725 }
17861726 function onScanProgress(frac) {
17871727 if (typeof frac !== 'number' || frac < 0) return;
1788- scBar.classList.remove('indet');
1789- scBar.style.width = Math.max(0, Math.min(1, frac)) * 100 + '%';
1728+ ui.scBar.classList.remove('indet');
1729+ ui.scBar.style.width = Math.max(0, Math.min(1, frac)) * 100 + '%';
17901730 }
17911731
17921732 function renderRows(rows) {
1793- scResults.textContent = '';
1794- rows.forEach(function (r) {
1795- const row = document.createElement('div'); row.className = 'sc-row'; row.dataset.addr = r.address;
1796- const addr = document.createElement('span'); addr.className = 'sc-addr';
1797- addr.textContent = scanAddressLabel(scEngineName, r.address); addr.title = addr.textContent;
1798- const val = document.createElement('input'); val.className = 'sc-val'; val.value = (r.value == null ? '?' : r.value);
1799- val.addEventListener('change', function () { scCmd({ op: 'write', address: r.address, value: val.value }); });
1800- const save = document.createElement('button'); save.className = 'sc-mini'; save.textContent = '★';
1801- save.title = 'Save this result';
1802- save.addEventListener('click', function () { addSaved(r.address, r.type); });
1803- row.appendChild(addr); row.appendChild(val); row.appendChild(save);
1804- scResults.appendChild(row);
1805- });
1733+ ui.scResults.textContent = '';
1734+ for (const r of rows) {
1735+ const row = el('div', 'sc-row'); row.dataset.addr = r.address;
1736+ const addr = el('span', 'sc-addr', scanAddressLabel(scEngineName, r.address)); addr.title = addr.textContent;
1737+ const val = el('input', 'sc-val'); val.value = (r.value == null ? '?' : r.value);
1738+ val.addEventListener('change', () => scCmd({ op: 'write', address: r.address, value: val.value }));
1739+ const save = el('button', 'sc-mini', '★'); save.title = 'Save this result';
1740+ save.addEventListener('click', () => addSaved(r.address, r.type));
1741+ row.append(addr, val, save);
1742+ ui.scResults.appendChild(row);
1743+ }
18061744 }
18071745
18081746 // live re-read of shown result rows + saved rows (real-time; unaffected by pause)
1809- const scPane = root.querySelector('.pane[data-pane="scan"]');
1747+ const scPane = panes.scan;
18101748 function pollValues() {
1811- if (scPane && scPane.hidden) return; // only poll while the Scan tab is open
1812- const rowEls = Array.prototype.slice.call(scResults.querySelectorAll('.sc-row'));
1813- const addrs = rowEls.map(function (el) { return el.dataset.addr; });
1749+ // Only poll while the Scan tab is actually visible (not tab-hidden, not minimized).
1750+ if ((scPane && scPane.hidden) || panel.classList.contains('min')) return;
1751+ const rowEls = [...ui.scResults.querySelectorAll('.sc-row')];
1752+ const addrs = rowEls.map(e2 => e2.dataset.addr);
18141753 if (addrs.length) {
1815- scCmd({ op: 'read', addresses: addrs }).then(function (res) {
1754+ scCmd({ op: 'read', addresses: addrs }).then((res) => {
18161755 if (!res || !res.values) return;
1817- const byAddr = {}; res.values.forEach(function (v) { byAddr[v.address] = v.value; });
1818- rowEls.forEach(function (el) {
1819- const inp = el.querySelector('.sc-val');
1820- if (inp && root.activeElement !== inp) inp.value = (byAddr[el.dataset.addr] == null ? '?' : byAddr[el.dataset.addr]);
1821- });
1756+ const byAddr = new Map(res.values.map(v => [v.address, v.value]));
1757+ for (const rowEl of rowEls) {
1758+ const inp = rowEl.querySelector('.sc-val');
1759+ const v = byAddr.get(rowEl.dataset.addr);
1760+ if (inp && root.activeElement !== inp) inp.value = (v == null ? '?' : v);
1761+ }
18221762 });
18231763 }
18241764 pollSaved();
@@ -1827,70 +1767,81 @@
18271767 /* ----- saved list ----- */
18281768 function persistSaved() { store.set('scan:' + PAGE, savedScans); }
18291769 function addSaved(address, ty) {
1830- savedScans.push({ name: scanAddressLabel(scEngineName, address), engine: scEngineName, type: (ty || scType.value), address: address });
1770+ savedScans.push({ name: scanAddressLabel(scEngineName, address), engine: scEngineName, type: (ty || ui.scType.value), address });
18311771 persistSaved(); renderSaved();
18321772 }
18331773 function renderSaved() {
1834- scSaved.textContent = '';
1835- savedScans.forEach(function (s, i) {
1836- const row = document.createElement('div'); row.className = 'sc-srow'; row.dataset.idx = i;
1837- const name = document.createElement('input'); name.className = 'sc-name'; name.value = s.name;
1774+ ui.scSaved.textContent = '';
1775+ savedScans.forEach((s, i) => {
1776+ const row = el('div', 'sc-srow'); row.dataset.idx = i;
1777+ const name = el('input', 'sc-name'); name.value = s.name;
18381778 name.title = scanAddressLabel(s.engine, s.address) + ' (' + s.type + ')';
1839- name.addEventListener('change', function () { s.name = name.value; persistSaved(); });
1840- const val = document.createElement('input'); val.className = 'sc-val'; val.value = '?';
1841- val.addEventListener('change', function () { sendScan(scTargetId, scTargetWin, { engine: s.engine, type: s.type, op: 'write', address: s.address, value: val.value }); });
1842- const del = document.createElement('button'); del.className = 'sc-mini'; del.textContent = '×'; del.title = 'Delete';
1843- del.addEventListener('click', function () { savedScans.splice(i, 1); persistSaved(); renderSaved(); });
1844- row.appendChild(name); row.appendChild(val); row.appendChild(del);
1845- scSaved.appendChild(row);
1779+ name.addEventListener('change', () => { s.name = name.value; persistSaved(); });
1780+ const val = el('input', 'sc-val'); val.value = '?';
1781+ val.addEventListener('change', () => sendScan(scTargetId, scTargetWin, { engine: s.engine, type: s.type, op: 'write', address: s.address, value: val.value }));
1782+ const del = el('button', 'sc-mini', '×'); del.title = 'Delete';
1783+ del.addEventListener('click', () => { savedScans.splice(i, 1); persistSaved(); renderSaved(); });
1784+ row.append(name, val, del);
1785+ ui.scSaved.appendChild(row);
18461786 });
18471787 }
1788+ // One batched `read` per (engine, type) group instead of a postMessage round-trip
1789+ // per saved row every poll tick.
18481790 function pollSaved() {
1849- const rowEls = Array.prototype.slice.call(scSaved.querySelectorAll('.sc-srow'));
1850- rowEls.forEach(function (el) {
1851- const s = savedScans[+el.dataset.idx]; if (!s) return;
1852- sendScan(scTargetId, scTargetWin, { engine: s.engine, type: s.type, op: 'read', addresses: [s.address] }).then(function (res) {
1853- const inp = el.querySelector('.sc-val'); if (!inp || root.activeElement === inp) return;
1854- const v = res && res.values && res.values[0] ? res.values[0].value : null;
1855- inp.value = (v == null ? '∅' : v);
1856- el.querySelector('.sc-name').classList.toggle('stale', v == null);
1791+ const groups = new Map(); // "engine type" -> { engine, type, addrs, els }
1792+ for (const rowEl of ui.scSaved.querySelectorAll('.sc-srow')) {
1793+ const s = savedScans[+rowEl.dataset.idx]; if (!s) continue;
1794+ const key = s.engine + ' ' + s.type;
1795+ let g = groups.get(key);
1796+ if (!g) { g = { engine: s.engine, type: s.type, addrs: [], els: [] }; groups.set(key, g); }
1797+ g.addrs.push(s.address); g.els.push(rowEl);
1798+ }
1799+ groups.forEach((g) => {
1800+ sendScan(scTargetId, scTargetWin, { engine: g.engine, type: g.type, op: 'read', addresses: g.addrs }).then((res) => {
1801+ const byAddr = new Map((res?.values || []).map(v => [v.address, v.value]));
1802+ g.els.forEach((rowEl, i) => {
1803+ const inp = rowEl.querySelector('.sc-val'); if (!inp || root.activeElement === inp) return;
1804+ const v = byAddr.get(g.addrs[i]);
1805+ inp.value = (v == null ? '∅' : v);
1806+ rowEl.querySelector('.sc-name').classList.toggle('stale', v == null);
1807+ });
18571808 });
18581809 });
18591810 }
18601811
18611812 // wire scan controls
1862- scEngineBtns.forEach(function (b) { b.addEventListener('click', function () { setEngine(b.dataset.engine); }); });
1863- scTarget.addEventListener('change', function () { applyTargetSelection(); setEngine(scEngineName); });
1864- scMem.addEventListener('change', function () { scCmd({ op: 'set-mem', mem: +scMem.value }).then(newScan); });
1865- scMemDetect.addEventListener('click', function () { refreshMemList(); });
1866- scType.addEventListener('change', function () { const o = scType.options[scType.selectedIndex]; scType.title = o ? o.title : ''; });
1867- scModeInputs.forEach(function (r) { r.addEventListener('change', updateButtons); });
1868- scFirst.addEventListener('click', function () {
1813+ scEngineBtns.forEach(b => b.addEventListener('click', () => setEngine(b.dataset.engine)));
1814+ ui.scTarget.addEventListener('change', () => { applyTargetSelection(); setEngine(scEngineName); });
1815+ ui.scMem.addEventListener('change', () => { scCmd({ op: 'set-mem', mem: +ui.scMem.value }).then(newScan); });
1816+ ui.scMemDetect.addEventListener('click', refreshMemList);
1817+ ui.scType.addEventListener('change', () => { ui.scType.title = ui.scType.options[ui.scType.selectedIndex]?.title || ''; });
1818+ scModeInputs.forEach(r => r.addEventListener('change', updateButtons));
1819+ ui.scFirst.addEventListener('click', () => {
18691820 if (scanActive) { newScan(); return; } // acts as "New scan" once a scan exists
18701821 if (getScMode() === 'unknown') { startScan({ op: 'first-unknown' }, 'Snapshotting…'); return; }
1871- const raw = scValue.value.trim();
1822+ const raw = ui.scValue.value.trim();
18721823 if (raw === '') { setScan('Enter a value, or choose “Unknown initial value”.'); return; }
18731824 startScan({ op: 'first-exact', value: raw }, 'Scanning…');
18741825 });
1875- scCancel.addEventListener('click', function () { setScan('Cancelling…'); scCmd({ op: 'cancel' }); });
1876- scRefineBtns.forEach(function (b) {
1877- b.addEventListener('click', function () {
1878- const crit = b.dataset.crit, raw = scValue.value.trim();
1826+ ui.scCancel.addEventListener('click', () => { setScan('Cancelling…'); scCmd({ op: 'cancel' }); });
1827+ scRefineBtns.forEach(b => {
1828+ b.addEventListener('click', () => {
1829+ const crit = b.dataset.crit, raw = ui.scValue.value.trim();
18791830 startScan({ op: 'refine', criteria: crit, value: (crit === 'exact' && raw !== '') ? raw : undefined }, 'Refining…');
18801831 });
18811832 });
1882- scPause.addEventListener('click', function () {
1883- scCmd({ op: 'set-paused', value: !pausedLocalView }).then(function (res) {
1884- pausedLocalView = !!(res && res.paused);
1833+ ui.scPause.addEventListener('click', () => {
1834+ scCmd({ op: 'set-paused', value: !pausedLocalView }).then((res) => {
1835+ pausedLocalView = !!res?.paused;
18851836 reflectPause();
18861837 });
18871838 });
18881839 function reflectPause() {
1889- scPause.textContent = pausedLocalView ? '▶ Resume' : '⏸ Pause';
1890- scPause.classList.toggle('on', pausedLocalView);
1840+ ui.scPause.textContent = pausedLocalView ? '▶ Resume' : '⏸ Pause';
1841+ ui.scPause.classList.toggle('on', pausedLocalView);
18911842 }
18921843
1893- scType.title = scType.options[scType.selectedIndex] ? scType.options[scType.selectedIndex].title : '';
1844+ ui.scType.title = ui.scType.options[ui.scType.selectedIndex]?.title || '';
18941845 renderSaved();
18951846 setEngine('wasm');
18961847 reflectPause();
@@ -1899,66 +1850,71 @@
18991850 let curMode;
19001851 function setMode(m) {
19011852 curMode = m;
1902- if (m === 'host') { title.textContent = 'Speedhack'; reattachBtn.hidden = true; refreshFrames(); }
1903- else { title.textContent = 'Speedhack (frame)'; framesBox.hidden = true; reattachBtn.hidden = false; }
1853+ if (m === 'host') { ui.title.textContent = 'Speedhack'; ui.reattach.hidden = true; refreshFrames(); }
1854+ else { ui.title.textContent = 'Speedhack (frame)'; ui.framesBox.hidden = true; ui.reattach.hidden = false; }
19041855 }
19051856 setMode(mode || 'host');
19061857
19071858 // minimize (start minimized)
1908- function setMin(m) { panel.classList.toggle('min', m); btnMin.textContent = m ? '▢' : '–'; }
1859+ function setMin(m) { panel.classList.toggle('min', m); ui.min.textContent = m ? '▢' : '–'; }
19091860 setMin(true);
1910- btnMin.addEventListener('click', function (e) { e.stopPropagation(); setMin(!panel.classList.contains('min')); });
1861+ ui.min.addEventListener('click', (e) => { e.stopPropagation(); setMin(!panel.classList.contains('min')); });
19111862
19121863 // close + remember dialog
1913- btnClose.addEventListener('click', function (e) { e.stopPropagation(); $('#dlgurl').textContent = PAGE; dlg.hidden = false; });
1914- $('#dlgCancel').addEventListener('click', function () { dlg.hidden = true; });
1915- $('#dlgNo').addEventListener('click', function () { doClose(false); });
1916- $('#dlgYes').addEventListener('click', function () { doClose(true); });
1864+ ui.close.addEventListener('click', (e) => { e.stopPropagation(); ui.dlgurl.textContent = PAGE; ui.dlg.hidden = false; });
1865+ ui.dlgCancel.addEventListener('click', () => { ui.dlg.hidden = true; });
1866+ ui.dlgNo.addEventListener('click', () => doClose(false));
1867+ ui.dlgYes.addEventListener('click', () => doClose(true));
19171868 function doClose(remember) {
19181869 if (remember) store.set(CLOSED_KEY, true);
1919- Object.keys(hooks).forEach(function (n) { try { hooks[n].uninstall(); } catch (e) {} });
1870+ for (const n of Object.keys(hooks)) { try { hooks[n].uninstall(); } catch (e) {} }
19201871 // The host is going away, so there's nothing left to re-attach to. Tell EVERY
19211872 // frame (attached or already-detached) to become its own standalone main panel
19221873 // rather than a detached one with a dead "re-attach" button. `hostClosed` also
19231874 // makes us hand off any iframe that announces itself AFTER this point.
19241875 if (isHost) {
19251876 hostClosed = true;
1926- frames.forEach(function (f, src) { f.attached = false; postTo(src, { type: 'host-closing' }); });
1877+ frames.forEach((f, src) => { f.attached = false; postTo(src, { type: 'host-closing' }); });
19271878 }
19281879 destroyPanel();
19291880 }
19301881
19311882 // drag (and tap-to-expand when minimized)
19321883 let dragging = false, moved = false, sx, sy, ox, oy;
1933- bar.addEventListener('pointerdown', function (e) {
1884+ bar.addEventListener('pointerdown', (e) => {
19341885 if (e.target.closest('button')) return;
19351886 dragging = true; moved = false;
19361887 const r = panel.getBoundingClientRect();
19371888 panel.style.left = r.left + 'px'; panel.style.top = r.top + 'px';
19381889 panel.style.right = 'auto'; panel.style.bottom = 'auto';
19391890 sx = e.clientX; sy = e.clientY; ox = r.left; oy = r.top;
1940- try { bar.setPointerCapture(e.pointerId); } catch (_) {}
1891+ try { bar.setPointerCapture(e.pointerId); } catch (e2) {}
19411892 });
1942- bar.addEventListener('pointermove', function (e) {
1893+ bar.addEventListener('pointermove', (e) => {
19431894 if (!dragging) return;
19441895 const dx = e.clientX - sx, dy = e.clientY - sy;
19451896 if (Math.abs(dx) > 4 || Math.abs(dy) > 4) moved = true;
19461897 panel.style.left = Math.max(0, Math.min(window.innerWidth - 30, ox + dx)) + 'px';
19471898 panel.style.top = Math.max(0, Math.min(window.innerHeight - 20, oy + dy)) + 'px';
19481899 });
1949- bar.addEventListener('pointerup', function (e) {
1900+ bar.addEventListener('pointerup', (e) => {
19501901 if (!dragging) return; dragging = false;
1951- try { bar.releasePointerCapture(e.pointerId); } catch (_) {}
1902+ try { bar.releasePointerCapture(e.pointerId); } catch (e2) {}
19521903 if (!moved && panel.classList.contains('min')) setMin(false);
19531904 });
19541905
19551906 return {
1956- removeNode: function () { try { origClearInterval(scPoll); } catch (e) {} try { host.remove(); } catch (e) {} if (panelHost === host) panelHost = null; clicker.listening = false; },
1957- refreshFrames: refreshFrames,
1958- refreshScan: refreshScanTargets,
1959- setMode: setMode,
1960- sync: sync,
1961- syncClicker: syncClicker
1907+ removeNode() {
1908+ try { origClearInterval(scPoll); } catch (e) {}
1909+ try { host.remove(); } catch (e) {}
1910+ if (panelHost === host) panelHost = null;
1911+ clicker.listening = false;
1912+ },
1913+ refreshFrames,
1914+ refreshScan: refreshScanTargets,
1915+ setMode,
1916+ sync,
1917+ syncClicker
19621918 };
19631919 }
19641920
@@ -1970,14 +1926,14 @@
19701926 let pendingMode = 'host';
19711927 function whenBody(fn) {
19721928 if (document.body) { fn(); return; }
1973- const obs = new MutationObserver(function () { if (document.body) { obs.disconnect(); fn(); } });
1929+ const obs = new MutationObserver(() => { if (document.body) { obs.disconnect(); fn(); } });
19741930 try { obs.observe(document.documentElement, { childList: true, subtree: true }); } catch (e) {}
1975- document.addEventListener('DOMContentLoaded', function () { try { obs.disconnect(); } catch (e) {} fn(); }, { once: true });
1931+ document.addEventListener('DOMContentLoaded', () => { try { obs.disconnect(); } catch (e) {} fn(); }, { once: true });
19761932 }
19771933 function ensurePanel(mode) {
19781934 pendingMode = mode;
19791935 if (panelCtl) { panelCtl.setMode(mode); return; }
1980- whenBody(function () {
1936+ whenBody(() => {
19811937 if (panelCtl) { panelCtl.setMode(pendingMode); return; }
19821938 if (!(document.body || document.documentElement)) return;
19831939 panelCtl = buildUI(pendingMode);
@@ -1997,6 +1953,6 @@
19971953 } else {
19981954 postTo(window.top, { type: 'hello', url: location.href, frameId: SELF_ID });
19991955 // origSetTimeout (real time) so scaling can't distort the 5s fallback window.
2000- origSetTimeout(function () { if (!gotHost) promoteToHost(); }, 5000);
1956+ origSetTimeout(() => { if (!gotHost) promoteToHost(); }, 5000);
20011957 }
20021958 })();