feat(islands): add external() invalidation and ctx.each repeated enha… - #64
Merged
Conversation
…ncement, and route enhance bindings through the DOM binding error pipeline
…ws so it cannot resurrect as a zombie
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
Improves the Islands / progressive-enhancement developer experience, driven by building a complete chess application as a single enhanced island. Two additive public APIs, one error-routing fix, and the documentation the pattern was missing.
external()— reactive integration with state SibuJS does not own. A domain engine, a canvas scene graph, an editor document, a socket-owned cache: the runtime cannot see writes into objects it does not own, and now says so with a primitive instead of leaving every application to invent a revision counter.source.track()inside a getter declares "this reads the outside world";source.invalidate()at the mutation site publishes the change. It never proxies, clones or diffs your object, participates inbatch(), works insidederived()andeffect(), never invalidates a disposed consumer, and routes a throwing consumer through the ordinary error pipeline with that consumer's own phase and node. ~60 bytes gzipped; tree-shakes out when unused.ctx.each(target, describe)— repeated enhancement bindings. A board, a table, a keyboard, a legend: many elements the server already rendered, each needing several bindings. The callback receives the element and its index and returns a descriptor (text,attr,class,show,on,cleanup); every field is committed through the matchingctx.*helper, so ownership, disposal, sanitization, write elision and error metadata are the same objects as the hand-written loop. It is sugar and deliberately nothing more — no expression parsing, no interpolation, noeval, no new DOM, no node moved or replaced. Measured at +9% setup cost for 64 elements × 4 bindings in a production build, and identical at update time.Fix: an
enhance()binding that throws on a later update now reaches the enclosingErrorBoundary. All five reactive helpers onEnhanceContextbuilt their bindings witheffect(), whose subscriber is stampedphase: "effect"and deliberately carries no owner node. Since a boundary is located from the failing node, that branch was unreachable for every progressive-enhancement binding on the page, and scheduled failures were mislabelled. They now bind throughreactiveBinding(commit, el)and reportphase: "binding"with their element, matching every other DOM binding in the runtime. SSR behaviour is unchanged — bindings created during SSR stay inert, as they were undereffect().Reference application and docs.
examples/chess/is a complete chess game as an enhanced island: 64 server-rendered squares,ctx.each,external()invalidation, per-square signals for the interaction hot path, a mounted move-history region, keyboard grid navigation, an accessible promotion dialog, two independent boards, and a deliberately broken island beside them.chess.jssupplies the rules as an example/development dependency only — it is not reachable from any package entry point and not in the published tarball (dependencies: {}).Also:
docs/islands.mdrewritten as the complete guide (decision table, common-mistakes table); newdocs/architecture/external-state.md(four state architectures compared, with measurements and profiling technique) anddocs/interop.md(nine rules for running islands inside a page another framework owns, with two browser-verified implementations).Notes for reviewers
external()lives insrc/core/signals/signal.ts, not a module of its own, and deliberately so. The build splitsdist/into shared chunks, and a module reachable only from the root entry lands in the index-only chunk besideenhance,mountIslands,mountandeach. Importing it from"sibujs"would then pull the whole island runtime into a page that only wanted to make a canvas reactive — 77 KB instead of 9 KB, measured across esbuild, Rollup, Vite and webpack. A control bundle confirmedstoreandwritablebehave the same way, so this is chunk placement, not anything specific to the new primitive. The reason is recorded at the definition and pinned by a test that bundlesdist/, not justsrc/.sibujs/pluginsships i18n with the router (4 of 20 tree-shaking rows). The barrel groups them;router.tsdoes not import i18n. Fixing it touches published entry points and belongs in its own PR.npm run bench:checkreports 28 regressions — and so does pristinemain. Verified by A/B: reverting the source changes, rebuilding and re-running against the same baseline reproduces all 28 on untouched code.bench-baseline.meta.jsonmarks itself"INFORMATIONAL ONLY"and warns of ±20% cross-run drift on this host. The baseline was deliberately not re-recorded.Validation
tsc --noEmit/tsc -p tsconfig.test.jsonbiome check src/ tests/vitest runplaywright testnpm run test:soaknpm run certify:rcnpm packdist+ README + LICENSE only,dependencies: {}Related Issue
Closes #
Type of Change
Checklist