Symptom
After editing the transcript (e.g., deleting a word or paragraph), the karaoke word highlight stops updating. The browser console shows:
Unhandled Promise Rejection: TypeError: null is not an object (evaluating 'word.n.parentNode.classList')
at updateTranscriptVisualState (hyperaudio-lite.js:742)
at forEach
at checkStatus (hyperaudio-lite.js:641)
at checkPlayHead (hyperaudio-lite.js:600)
Subsequent polling never recovers because the rejection happens inside the setTimeout callback that schedules the next poll, so the chain dies silently.
Root cause
hyperaudio-lite caches a wordArr of { n: <element>, m, p } entries built from transcript.querySelectorAll('[data-m]'). Its poll chain (checkPlayHead → checkStatus → updateTranscriptVisualState) iterates wordArr on a setTimeout schedule. When the editor deletes a span, the corresponding wordArr entry's n is now a detached node — n.parentNode === null — and updateTranscriptVisualState dereferences .classList on it.
The editor already debounces a wordArr refresh (via refreshHyperaudioInstance in registerStrikeThrus) at 150ms after the last edit, but the library's poll can fire inside that window and crash before the refresh runs.
Why fix it here (not upstream)
The race only exists in editor-style consumers that mutate the transcript DOM at runtime. hyperaudio-lite is otherwise designed for read-only playback contexts, where this never happens. Rather than push a defensive change into the library, we should harden the editor's use of it — that keeps the library minimal and removes a library-version dependency.
Proposed fix
Monkeypatch the HyperaudioLite instance right after it's created in hyperaudio() (index.html:737):
const original = hyperaudioInstance.updateTranscriptVisualState.bind(hyperaudioInstance);
hyperaudioInstance.updateTranscriptVisualState = function (currentTime) {
try {
return original(currentTime);
} catch (e) {
// The cached wordArr referenced a deleted span. Rebuild against the
// live DOM and retry, so the polling chain doesn't die silently.
refreshHyperaudioInstance();
return original(currentTime);
}
};
This is self-healing: under normal conditions the patch is a single try/catch with no overhead; under the edit-race it rebuilds and recovers without breaking the polling chain.
Acceptance criteria
Out of scope
A defensive null check in hyperaudio-lite itself (if (!word.n.parentNode) return; inside the forEach at line 740) would be a one-line upstream fix and might still be worth filing — but isn't required for this issue.
Symptom
After editing the transcript (e.g., deleting a word or paragraph), the karaoke word highlight stops updating. The browser console shows:
Subsequent polling never recovers because the rejection happens inside the
setTimeoutcallback that schedules the next poll, so the chain dies silently.Root cause
hyperaudio-litecaches awordArrof{ n: <element>, m, p }entries built fromtranscript.querySelectorAll('[data-m]'). Its poll chain (checkPlayHead→checkStatus→updateTranscriptVisualState) iterateswordArron asetTimeoutschedule. When the editor deletes a span, the correspondingwordArrentry'snis now a detached node —n.parentNode === null— andupdateTranscriptVisualStatedereferences.classListon it.The editor already debounces a
wordArrrefresh (viarefreshHyperaudioInstanceinregisterStrikeThrus) at 150ms after the last edit, but the library's poll can fire inside that window and crash before the refresh runs.Why fix it here (not upstream)
The race only exists in editor-style consumers that mutate the transcript DOM at runtime.
hyperaudio-liteis otherwise designed for read-only playback contexts, where this never happens. Rather than push a defensive change into the library, we should harden the editor's use of it — that keeps the library minimal and removes a library-version dependency.Proposed fix
Monkeypatch the
HyperaudioLiteinstance right after it's created inhyperaudio()(index.html:737):This is self-healing: under normal conditions the patch is a single try/catch with no overhead; under the edit-race it rebuilds and recovers without breaking the polling chain.
Acceptance criteria
Out of scope
A defensive null check in hyperaudio-lite itself (
if (!word.n.parentNode) return;inside the forEach at line 740) would be a one-line upstream fix and might still be worth filing — but isn't required for this issue.