Skip to content

feat(islands): add external() invalidation and ctx.each repeated enha… - #64

Merged
hexplus merged 3 commits into
mainfrom
chore/islands-hardening
Sep 3, 2026
Merged

feat(islands): add external() invalidation and ctx.each repeated enha…#64
hexplus merged 3 commits into
mainfrom
chore/islands-hardening

Conversation

@hexplus

@hexplus hexplus commented Sep 3, 2026

Copy link
Copy Markdown
Owner

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 in batch(), works inside derived() and effect(), 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 matching ctx.* 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, no eval, 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 enclosing ErrorBoundary. All five reactive helpers on EnhanceContext built their bindings with effect(), whose subscriber is stamped phase: "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 through reactiveBinding(commit, el) and report phase: "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 under effect().

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.js supplies 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.md rewritten as the complete guide (decision table, common-mistakes table); new docs/architecture/external-state.md (four state architectures compared, with measurements and profiling technique) and docs/interop.md (nine rules for running islands inside a page another framework owns, with two browser-verified implementations).

Notes for reviewers

  • external() lives in src/core/signals/signal.ts, not a module of its own, and deliberately so. The build splits dist/ into shared chunks, and a module reachable only from the root entry lands in the index-only chunk beside enhance, mountIslands, mount and each. 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 confirmed store and writable behave 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 bundles dist/, not just src/.
  • Pre-existing and not fixed here: sibujs/plugins ships i18n with the router (4 of 20 tree-shaking rows). The barrel groups them; router.ts does not import i18n. Fixing it touches published entry points and belongs in its own PR.
  • npm run bench:check reports 28 regressions — and so does pristine main. 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.json marks itself "INFORMATIONAL ONLY" and warns of ±20% cross-run drift on this host. The baseline was deliberately not re-recorded.

Validation

Gate Result
tsc --noEmit / tsc -p tsconfig.test.json clean / 0 errors
biome check src/ tests/ clean (669 files)
vitest run 457 files, 6399 passed, 1 skipped (was 449 / 6320)
playwright test 366 passed — Chromium, Firefox, WebKit (was 309)
npm run test:soak 25 passed, 1 skipped
npm run certify:rc 15/15 PASS, 0 FAIL, 0 NOT TESTED
Bundler matrix builds 20/20, runtime 20/20, tree-shaking 16/20 (was 12/20)
npm pack 114 files, dist + README + LICENSE only, dependencies: {}

Related Issue

Closes #

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update

Checklist

  • I have read CONTRIBUTING.md
  • My code builds without errors
  • I have tested my changes
  • I have updated documentation if needed

…ncement, and route enhance bindings through the DOM binding error pipeline
@hexplus
hexplus merged commit 774973d into main Sep 3, 2026
5 checks passed
@hexplus
hexplus deleted the chore/islands-hardening branch September 7, 2026 04:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant