src/ext/hx-live.js's runaway-recompute guard (schedule(), lines ~39–51) tracks i, start, and warned as module-level state shared across every hx-live/:expr binding on the page:
let swaps = 0;
let i = 0;
let start = 0;
let warned = false;
function schedule() {
if (pending) return;
if (swaps > 0) return;
let now = Date.now();
if (now - start > 1000) {
start = now;
i = 0;
warned = false;
}
if (++i > 50 && !warned) {
console.warn('htmx: hx-live recompute exceeded 50/sec.');
warned = true;
}
...
}
warned is a one-shot latch per rolling 1-second window, shared globally. Once any expression on the page trips it, console.warn is suppressed for every other expression until the window rolls over (now - start > 1000).
Impact: if two independently-broken hx-live expressions both start looping out of control within the same second, only the first gets a console warning. The second burns CPU in a runaway loop with zero diagnostic signal, because warned is already true and stays that way until the window resets. schedule() has no notion of which expression/element triggered a given recompute, so there's no way to tell offenders apart with the current state shape.
Where I hit this:
I'm running test/tests/ext/hx-live.js (the vendored htmx test suite) against a custom test runner — a mocha-shim executing inside a V8 runtime with happy-dom, rather than a real browser. That runner follows the same structure as the upstream test file itself: the extension script is loaded once in the suite's before() hook, so all module-level state in hx-live.js persists across every it() for the rest of the file, not just the currently-running test.
Concretely, the iteration cap warns on runaway test (in the hx-live extension describe block) asserts:
it('iteration cap warns on runaway', async function() {
let warned = false;
console.warn = (...args) => { if (/* matches message */) warned = true; ... };
for (let i = 0; i < 100; i++) {
document.body.setAttribute('data-runaway-test-live', String(i));
await htmx.timeout(5);
}
warned.should.equal(true);
});
This test passes reliably when run in isolation. But run as part of the full suite, it fails deterministically: expected false to equal true. Root cause is exactly the shared-latch design above — several earlier tests in the same file (coalesces recomputes during a swap, multiple hx-live elements all run, etc.) collectively push the recompute counter past 50 and set warned = true within the same 1-second window this test starts in, so its own 100-iteration burst never gets a fresh warning. The underlying reactivity is unaffected — window.__runawayCountLive still reaches ~101 in the failing run, confirming every mutation still triggered a recompute — only the console.warn side channel is lost.
I'm currently working around this on my side by skipping that one test (it's an order-dependent false negative, not a functional bug), but it seemed worth reporting upstream since the same shared-state design would silently swallow a warning for a second real runaway loop in production, not just in a test harness.
src/ext/hx-live.js's runaway-recompute guard (schedule(), lines ~39–51) tracksi,start, andwarnedas module-level state shared across everyhx-live/:exprbinding on the page:warnedis a one-shot latch per rolling 1-second window, shared globally. Once any expression on the page trips it,console.warnis suppressed for every other expression until the window rolls over (now - start > 1000).Impact: if two independently-broken
hx-liveexpressions both start looping out of control within the same second, only the first gets a console warning. The second burns CPU in a runaway loop with zero diagnostic signal, becausewarnedis alreadytrueand stays that way until the window resets.schedule()has no notion of which expression/element triggered a given recompute, so there's no way to tell offenders apart with the current state shape.Where I hit this:
I'm running
test/tests/ext/hx-live.js(the vendored htmx test suite) against a custom test runner — a mocha-shim executing inside a V8 runtime withhappy-dom, rather than a real browser. That runner follows the same structure as the upstream test file itself: the extension script is loaded once in the suite'sbefore()hook, so all module-level state inhx-live.jspersists across everyit()for the rest of the file, not just the currently-running test.Concretely, the
iteration cap warns on runawaytest (in thehx-live extensiondescribe block) asserts:This test passes reliably when run in isolation. But run as part of the full suite, it fails deterministically:
expected false to equal true. Root cause is exactly the shared-latch design above — several earlier tests in the same file (coalesces recomputes during a swap,multiple hx-live elements all run, etc.) collectively push the recompute counter past 50 and setwarned = truewithin the same 1-second window this test starts in, so its own 100-iteration burst never gets a fresh warning. The underlying reactivity is unaffected —window.__runawayCountLivestill reaches ~101 in the failing run, confirming every mutation still triggered a recompute — only theconsole.warnside channel is lost.I'm currently working around this on my side by skipping that one test (it's an order-dependent false negative, not a functional bug), but it seemed worth reporting upstream since the same shared-state design would silently swallow a warning for a second real runaway loop in production, not just in a test harness.