Bugfix/runtime defects - #66
Merged
Merged
Conversation
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
Two runtime defects where a value of the right shape was judged by the wrong test, so the framework confidently did the wrong thing and said nothing — plus a third found while fixing the first. All predate 4.2: the
instanceof Promisecheck dates to the first commit, the island registration union to the reactive-islands release.An island loader that was never wrapped in
lazyIsland()was run as a setup. A setup and a loader are both plain functions, so nothing can tell them apart before one is called. Invoked as a setup, the loader ignored itsctxand returned the import promise. The module was fetched —import()ran — but nobody awaited it, so the setup it resolved to was discarded and never ran.enhance()then returned normally and the element was stampeddata-sibu-enhanced="true": a marker asserting an enhancement whose real setup had never executed. Nothing downstream can recover from the framework reporting success for work it never did.The guard lives in
enhance(), notmountIslands, becauseenhance()andenhanceAll()are public and reach the same defect directly. A setup returning a thenable throws before the commit that records ownership and sets the marker, so the transaction rolls back and the root is left exactly as unenhanced as it started.A rolled-back enhancement could still be mutated afterwards. Detecting the thenable and unwinding was only half of it — the async setup keeps running after its first
await, still holdingctx, and could register listeners, bindings and cleanups into an enhancement that no longer existed. The root carried no marker and the disposer had already drained, so nothing could ever release them. A setup that queued a microtask and then threw synchronously escaped identically.The context is now closed once its transaction unwinds; every mutating method refuses afterwards with a dev warning naming the method the consumer called. Closing happens after the teardowns drain, because a teardown may legitimately register another cleanup while unwinding — documented behaviour that still works, and pinned by a test.
Suspensedecided "is this async?" withinstanceof Promise. That asks which realm built the object, not what it can do. A promise from an iframe, avmcontext, a worker bridge or a polyfill failed the test and was treated as a DOM node:insertBeforethrew, the boundary rendered its error branch for work about to succeed, and the element the promise resolved to was never inserted and never disposed — live reactive bindings attached to nothing. The check is now by shape, which is whatawaititself accepts, with nodes excluded via a realm-agnosticnodeTypetest so a custom element exposingthenis still inserted. The fallback check had the same realm blindness and now uses the same test.No public type is narrowed. An earlier revision required a branded
LazyIslandLoader, which would catch the mistake at compile time — but it rejects code that compiles today, and this package's contract is that existing public API keeps working, with a codemod for anything that cannot be widened. There is no codemod infrastructure to ship one through, so the narrowing was reverted:IslandRegistrationstill accepts an unbranded loader,LazyIslandLoaderis exported for callers who want it, and the runtime guard carries the fix. A test records that decision so a later tightening can't happen by accident.Behaviour change worth calling out: an
asyncenhancement setup now throws instead of half-working. It was never supported (EnhanceSetupreturnsvoid | (() => void)); it registered whatever ran before the firstawaitand abandoned the rest.Suspenseprops were widened to match what the runtime accepts (PromiseLike, and an element forfallback) — the test suite proves it by no longer needing a cast.Diagnostics. The guard's error is thrown in production too — a check that stops a broken enhancement being reported as successful cannot be development-only. Only its long explanation compiles out, leaving a short message;
tests/dist-artifacts.test.tsasserts both halves. The four new dev-only strings are in the stripping guards.Size: core runtime 25,836 → 25,924 bytes gzipped (+88), production CDN 26,224 → 26,306 (+82). That growth is behaviour, not diagnostics.
Related Issue
Closes #
Type of Change
Checklist