fix(tagFactory): stamp owner node on reactive class/style bindings so… - #62
Merged
Conversation
… ErrorBoundary can claim their failures
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Reactive
classandstylebindings created bytagFactorywere registered with a baretrack(commit). A one-argumenttrackdelegates toreactiveBinding(commit), which correctly stamps_errorPhase = "binding"but leaves_errorNode = undefinedbecause no owner node was supplied.Consequence: when a reactive class/style getter threw on a later scheduled rerun, the drain reported it with
node: undefined.reportErrorthen had no DOM position to dispatch its boundary-propagation event from,claimedByBoundarybailed at itsif (!raw)guard, and the failure skipped the nearest enclosingErrorBoundaryentirely — falling straight through to the global runtime handler orconsole.error. Every other reactive attribute goes throughbindAttribute, which passes the element, soclassandstylewere the only reactive props with unroutable errors.Affected paths, all in
src/core/rendering/tagFactory.ts:applyStyle()— whole reactive style getterapplyStyle()— individual reactive style propertiesapplyClass()— reactive class getterapplyClass()— conditional class object containing reactive gettersFix: use the existing DOM-binding primitive directly —
reactiveBinding(commit, el)— at those four call sites.track()'s signature is untouched (its second parameter is an explicit subscriber, not an owner node), no behaviour is routed throughbindAttribute, and all fourregisterDisposer(el, teardown)calls are preserved.Production diff is 4 call sites + 1 import + an explanatory comment (+21/−8). The rest of the change is regression coverage.
Baseline reproduction (all four forms, before the fix): initial render succeeds and reports nothing; a later signal-driven rerun reports
phase: "binding"✓ withnode: undefined✗. A class getter failing inside a mountedErrorBoundaryrendered no fallback while the global handler fired once. After the fix, all four carry the owning element and the boundary claims the error with zero global-handler calls.Regression coverage added (44 tests in
tests/tagFactory-class-style-error-owner-node.test.ts) — 15 of them fail against the unmodified baseline:phase === "binding"andnode === el; exactly one report carrying the originalErrorby reference identity; the previously committed class/style value survives the failed commit; unrelated subscribers of the same signal keep running }.console.errorexactly once with no handler installed, plus a detached-element case.dispose(el)→ subscriber removed → a later signal update neither reruns the getter nor reports anything; a two-binding subtree released together; and_errorNodeconfirmed cleared on disposal via the existinginspectSignal+forEachSubscriberseams (no new production internals exposed).url(javascript:…); per-run dependency switching still subscribes newly read signals and prunes stale ones (class and style); static values create no subscriber; synchronous initial-render throws still propagate to the caller.Preserved: public API signatures, bundle entry points, browser support floor, Node compatibility, class/style sanitization, reactive batching and scheduling, initial-render semantics, error object identity, runtime error phases, cleanup registration, per-run dependency tracking, duplicate-instance behaviour, tree-shaking. No version bump.
Verification — every gate green:
npm cinpm run lintnpm run typechecknpm run typecheck:testsnpm test -- --runnpm run buildgit diff --checknpm run test:browsernpm run certify:rcnpm run test:soakCertification detail: build, TS(src), lint, full suite, TS(tests+entry), wrapper invariants 26, browser floor 24, query/router/SSR fuzzing 21, browser matrix 309, lifecycle+SSR soak 25, npm pack + tarball install, packed subpath exports 157/157 across 16 subpaths, bundler matrix 12/12 builds and 12/12 runtime (tree-shake 8/12, unchanged from baseline), Node matrix 22.3.0 / 22 / 24 all PASS.
Related Issue
Closes #
Type of Change
Checklist