fix(main): hoist let bindings to top of start() to fix TDZ on anchorError

The applyMode() function reads/writes anchorError, lastSnapshot, and other
let-bound state. Function declarations are hoisted, so applyMode() can fire
from inside the dialog.mount() / applyMode() call sequence at the top of
start() — but the let bindings themselves are not initialized until
execution reaches their declaration line, which came later.

When prefs.mode === 'anchored' and findAnchorElement() returns null, the
new 'anchor missed' branch writes to anchorError and calls render(). Both
access anchorError before its let binding is initialized, throwing
ReferenceError: Cannot access 'anchorError' before initialization.

Move all four let declarations (lastSnapshot, lastAttr, lastDelta,
anchorError) to the top of start(), before dialog.mount() and applyMode().
Function declarations are unaffected — they are hoisted regardless.
This commit is contained in:
Claude
2026-06-01 22:22:29 -05:00
parent 578736a492
commit 501c6746eb
+110 -95
View File
@@ -35,113 +35,128 @@ function findAnchorElement() {
} }
function start() { function start() {
const store = new Store({ try {
storage: localStorage, const store = new Store({
onWarn: (m) => console.warn(m), storage: localStorage,
}); onWarn: (m) => console.warn(m),
const prefs = store.getPrefs(); });
const prefs = store.getPrefs();
const dialog = new Dialog({ // State that applyMode() and render() may touch on first call.
onTargetChange: (v) => { // Declared up-front to avoid TDZ ReferenceError if applyMode()'s
const attr = currentAttribute()?.attr; // anchor-miss branch fires before the natural declaration point.
if (!attr) return; let lastSnapshot = null;
store.setTarget(attr, v); let lastAttr = null;
render(); let lastDelta = 0;
}, let anchorError = null;
onModeChange: (m) => {
store.setMode(m);
prefs.mode = m;
applyMode();
},
onPosChange: (pos) => store.setPos(pos),
onClose: () => dialog.destroy(),
});
dialog.mount({ initialMode: prefs.mode, initialPos: prefs.pos }); // One-time migration: dialog now defaults to bottom-left, so reset any
applyMode(); // previously-saved position from the bottom-right era.
if (prefs.pos && (prefs.pos.x !== 0 || prefs.pos.y !== 0)) {
let lastSnapshot = null; console.info('[tat] resetting dialog position to new bottom-left default');
let lastAttr = null; prefs.pos = { x: 0, y: 0 };
let lastDelta = 0; store.setPos(prefs.pos);
let anchorError = null;
function snapshot() {
const a = currentAttribute();
if (!a) {
return { error: "Couldn't read attribute — Torn may have updated the page." };
} }
lastAttr = a.attr;
const summary = store.getSummary(a.attr);
return {
attr: a.attr,
gym: a.gym,
current: a.current,
target: store.getTarget(a.attr),
perTrain: lastDelta,
summary,
warn: store._saveDisabled ? 'saving disabled this session' : null,
anchorError: anchorError,
};
}
function render() { const dialog = new Dialog({
lastSnapshot = snapshot(); onTargetChange: (v) => {
dialog.render(lastSnapshot); const attr = currentAttribute()?.attr;
} if (!attr) return;
store.setTarget(attr, v);
render();
},
onModeChange: (m) => {
store.setMode(m);
prefs.mode = m;
applyMode();
},
onPosChange: (pos) => store.setPos(pos),
onClose: () => dialog.destroy(),
});
function applyMode() { dialog.mount({ initialMode: prefs.mode, initialPos: prefs.pos });
if (prefs.mode === 'anchored') { applyMode();
const el = findAnchorElement();
if (el) { function snapshot() {
const rect = el.getBoundingClientRect(); const a = currentAttribute();
dialog.setMode('anchored', { canAnchor: true }); if (!a) {
dialog._positionAnchored(rect); return { error: "Couldn't read attribute — Torn may have updated the page." };
// observe }
const ro = new ResizeObserver(() => { lastAttr = a.attr;
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect()); const summary = store.getSummary(a.attr);
}); return {
ro.observe(el); attr: a.attr,
anchorError = null; gym: a.gym,
current: a.current,
target: store.getTarget(a.attr),
perTrain: lastDelta,
summary,
warn: store._saveDisabled ? 'saving disabled this session' : null,
anchorError: anchorError,
};
}
function render() {
lastSnapshot = snapshot();
dialog.render(lastSnapshot);
}
function applyMode() {
if (prefs.mode === 'anchored') {
const el = findAnchorElement();
if (el) {
const rect = el.getBoundingClientRect();
dialog.setMode('anchored', { canAnchor: true });
dialog._positionAnchored(rect);
// observe
const ro = new ResizeObserver(() => {
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect());
});
ro.observe(el);
anchorError = null;
return;
}
// Anchor selector missed — don't snap to default, just keep current
// position and show a note.
anchorError = "Couldn't find the training form on this page.";
render();
return; return;
} }
// Anchor selector missed — don't snap to default, just keep current anchorError = null;
// position and show a note. dialog.setMode('free');
anchorError = "Couldn't find the training form on this page.";
render();
return;
} }
anchorError = null;
dialog.setMode('free');
}
// initial paint // initial paint
render(); render();
// watch DOM for attribute changes // watch DOM for attribute changes
let pending = false; let pending = false;
const observer = new MutationObserver(() => { const observer = new MutationObserver(() => {
if (pending) return; if (pending) return;
pending = true; pending = true;
requestAnimationFrame(() => { requestAnimationFrame(() => {
pending = false; pending = false;
const a = currentAttribute(); const a = currentAttribute();
if (a && (a.attr !== lastAttr || a.current !== lastSnapshot?.current)) render(); if (a && (a.attr !== lastAttr || a.current !== lastSnapshot?.current)) render();
});
}); });
}); observer.observe(document.body, { childList: true, subtree: true, characterData: true });
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
// intercept train requests // intercept train requests
const prev = currentAttribute()?.current ?? 0; const prev = currentAttribute()?.current ?? 0;
startRequestInterceptor({ startRequestInterceptor({
prevValue: prev, prevValue: prev,
currentAttr: lastAttr, currentAttr: lastAttr,
onTrain: ({ attr, delta, ts }) => { onTrain: ({ attr, delta, ts }) => {
store.recordTrain(attr, delta, ts); store.recordTrain(attr, delta, ts);
lastDelta = delta; lastDelta = delta;
render(); render();
}, },
onParseFail: (url) => console.warn('[tat] could not parse train response from', url), onParseFail: (url) => console.warn('[tat] could not parse train response from', url),
}); });
} catch (e) {
console.error('[tat] failed to start:', e);
}
} }
if (location.hash === '#tat-test') { if (location.hash === '#tat-test') {