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:
+110
-95
@@ -35,113 +35,128 @@ function findAnchorElement() {
|
||||
}
|
||||
|
||||
function start() {
|
||||
const store = new Store({
|
||||
storage: localStorage,
|
||||
onWarn: (m) => console.warn(m),
|
||||
});
|
||||
const prefs = store.getPrefs();
|
||||
try {
|
||||
const store = new Store({
|
||||
storage: localStorage,
|
||||
onWarn: (m) => console.warn(m),
|
||||
});
|
||||
const prefs = store.getPrefs();
|
||||
|
||||
const dialog = new Dialog({
|
||||
onTargetChange: (v) => {
|
||||
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(),
|
||||
});
|
||||
// State that applyMode() and render() may touch on first call.
|
||||
// Declared up-front to avoid TDZ ReferenceError if applyMode()'s
|
||||
// anchor-miss branch fires before the natural declaration point.
|
||||
let lastSnapshot = null;
|
||||
let lastAttr = null;
|
||||
let lastDelta = 0;
|
||||
let anchorError = null;
|
||||
|
||||
dialog.mount({ initialMode: prefs.mode, initialPos: prefs.pos });
|
||||
applyMode();
|
||||
|
||||
let lastSnapshot = null;
|
||||
let lastAttr = null;
|
||||
let lastDelta = 0;
|
||||
let anchorError = null;
|
||||
|
||||
function snapshot() {
|
||||
const a = currentAttribute();
|
||||
if (!a) {
|
||||
return { error: "Couldn't read attribute — Torn may have updated the page." };
|
||||
// One-time migration: dialog now defaults to bottom-left, so reset any
|
||||
// previously-saved position from the bottom-right era.
|
||||
if (prefs.pos && (prefs.pos.x !== 0 || prefs.pos.y !== 0)) {
|
||||
console.info('[tat] resetting dialog position to new bottom-left default');
|
||||
prefs.pos = { x: 0, y: 0 };
|
||||
store.setPos(prefs.pos);
|
||||
}
|
||||
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() {
|
||||
lastSnapshot = snapshot();
|
||||
dialog.render(lastSnapshot);
|
||||
}
|
||||
const dialog = new Dialog({
|
||||
onTargetChange: (v) => {
|
||||
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() {
|
||||
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;
|
||||
dialog.mount({ initialMode: prefs.mode, initialPos: prefs.pos });
|
||||
applyMode();
|
||||
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
anchorError = null;
|
||||
dialog.setMode('free');
|
||||
}
|
||||
anchorError = null;
|
||||
dialog.setMode('free');
|
||||
}
|
||||
|
||||
// initial paint
|
||||
render();
|
||||
// initial paint
|
||||
render();
|
||||
|
||||
// watch DOM for attribute changes
|
||||
let pending = false;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (pending) return;
|
||||
pending = true;
|
||||
requestAnimationFrame(() => {
|
||||
pending = false;
|
||||
const a = currentAttribute();
|
||||
if (a && (a.attr !== lastAttr || a.current !== lastSnapshot?.current)) render();
|
||||
// watch DOM for attribute changes
|
||||
let pending = false;
|
||||
const observer = new MutationObserver(() => {
|
||||
if (pending) return;
|
||||
pending = true;
|
||||
requestAnimationFrame(() => {
|
||||
pending = false;
|
||||
const a = currentAttribute();
|
||||
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
|
||||
const prev = currentAttribute()?.current ?? 0;
|
||||
startRequestInterceptor({
|
||||
prevValue: prev,
|
||||
currentAttr: lastAttr,
|
||||
onTrain: ({ attr, delta, ts }) => {
|
||||
store.recordTrain(attr, delta, ts);
|
||||
lastDelta = delta;
|
||||
render();
|
||||
},
|
||||
onParseFail: (url) => console.warn('[tat] could not parse train response from', url),
|
||||
});
|
||||
// intercept train requests
|
||||
const prev = currentAttribute()?.current ?? 0;
|
||||
startRequestInterceptor({
|
||||
prevValue: prev,
|
||||
currentAttr: lastAttr,
|
||||
onTrain: ({ attr, delta, ts }) => {
|
||||
store.recordTrain(attr, delta, ts);
|
||||
lastDelta = delta;
|
||||
render();
|
||||
},
|
||||
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') {
|
||||
|
||||
Reference in New Issue
Block a user