Skip to content

Re-target the todomvc dioxus guest onto polyengine-dioxus; fix the surface guests - #143

Merged
lannbot merged 2 commits into
mainfrom
todomvc/polyengine-dioxus
Sep 5, 2026
Merged

Re-target the todomvc dioxus guest onto polyengine-dioxus; fix the surface guests#143
lannbot merged 2 commits into
mainfrom
todomvc/polyengine-dioxus

Conversation

@lannbot

@lannbot lannbot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Two commits, reviewable separately.

1. Re-target the todomvc dioxus guest onto polyengine-dioxus

The ruling this embodies. polyvisor keeps two app surfaces, split by what the app is written in: polyvisor:surface (the WebIDL mirror, #15's bet) for hand-written, Preact and componentize-js apps; polymorph:dioxus for dioxus apps and the visor. spikes/todomvc/guest-dioxus was the prototype of the second — its renderer.rs and events.rs are deleted in favour of the renderer that generalised them, along with a second copy of TodoMVC. The page's ?guest=hand|dioxus|preact toggle now demonstrates the two-backend shape in one place.

The frame transport is the real work. The page's default backend is the opaque-origin sandboxed iframe — the trust story — so the dioxus guest had to keep it. This adds the polyengine-dioxus analogue of visor/frame: the instance runs in the shell, mutation batches cross a MessageChannel, and the frame runs the sibling's own DomApplier and EventDispatcher. history is the shell's (fragmentHistory); head is supplied with a refusing interceptor, since a framed app's <head> is the frame's and whether it gets one is a later decision.

Four degradations across the frame, each documented at its site: prevent-default/stop-propagation are no-ops; the sync dom queries answer none/false; eval is not granted and is asserted absent on the artifact; resource-carrying payloads cannot cross a structuredClone. The first produced a real bug the gate found — an un-prevented in-page anchor click navigated the frame's fragment, which still appends to the joint session history, so the shell's back button stepped through frame entries. The frame now refuses in-page anchor defaults on the guest's behalf.

Two of the three documented gaps close; the third is reopened by the frame. Rich events and real bubbling come with the renderer. onmounted/focus closes in the renderer and is reopened by the sync dom queries being unanswerable across the frame — a different cause, same visible symptom, reported rather than buried.

The spike had no automated gates. It now has two: the differential harness wrapped as Playwright, and a real-interaction test of the dioxus guest inside the frame that also asserts the frame cannot read the shell's anchor colour.

Size: 224 KB gzipped, from 130 — wasi-libc/std with the wasip2 target, the full renderer replacing ~530 lines of glue, and dioxus-document/dioxus-history for the unconditional providers.

2. The surface guests could not instantiate for fifteen days

Found by the new gate on its first honest run. Commit 4ec8d2f (#74, 2026-08-21) moved visor/surface/surface.ts onto polyvisor:surface/*@0.1.0 import keys; spikes/todomvc/wit/todomvc.wit kept declaring polymorph:todomvc-spike@0.0.1. The interface bodies were byte-identical, which is why it looked innocuous — but the package name is the import key, and every surface component died at import resolution. Nothing caught it because the spike's correctness lived in a page a human had to open.

The fix is a deletion, not a rename. Two byte-identical definitions in the tree was the arrangement that produced this; the second copy was the defect. todomvc.wit now imports polyvisor:surface@0.1.0 through a repo-relative symlink to wit/surface — one definition. 64 lines of duplicated WIT leave; guest paths follow mechanically; no guest behaviour touched, which is what lets the stepwise DOM comparison certify the change as purely mechanical.

And the question the gate existed to answer has now actually been asked: the old surface components do survive the runtime bump — three backends, fifteen steps, stepwise-identical DOM; eight trap probes identical. Evidenced, not assumed.

Also

docs/spike-todomvc/ is the justfile's build output and is now ignored alongside docs/demo/.


Commits are unsigned — an agent's commits should not carry a signature asserting a human author.

THE RULING THIS EMBODIES: polyvisor keeps two app surfaces, split by
what the app is written in. `polyvisor:surface` — the WebIDL mirror,
issue #15's pass-through bet — stays for hand-written, Preact and
componentize-js apps. `polymorph:dioxus`, the sibling renderer, is the
backend for dioxus apps and for the visor. This spike's `guest-dioxus`
was the PROTOTYPE of the second: its `renderer.rs` a hand-rolled
`WriteMutations` over the surface, its `events.rs` a hand-rolled event
converter. polyengine-dioxus generalised and superseded both, so they
are deleted (285 + 249 lines, plus a second copy of TodoMVC), and the
guest builds on `polyengine_dioxus::launch!` for wasm32-wasip2 — no
wbg-sever, no `component new`. The page's ?guest=hand|dioxus|preact
toggle now demonstrates the two-backend shape in one place.

THE FRAME TRANSPORT IS THE REAL WORK. The page's default backend is the
sandboxed opaque-origin iframe, and that is the trust story, so the
dioxus guest had to keep it. The sibling's `mountApp` applies into the
document it runs in; this adds the polyengine-dioxus analogue of
visor/frame: the instance runs in the shell, mutation batches cross a
MessageChannel, and the frame runs the sibling's own `DomApplier` and
`EventDispatcher`, posting serialised payloads back to `handle-event`.
`history` is the shell's (`fragmentHistory` — it owns the URL); `head`
is supplied with a refusing interceptor, because a framed app's <head>
is the frame's and whether it gets one is a later decision.

FOUR DEGRADATIONS ACROSS THE FRAME, each documented at its site:
prevent-default and stop-propagation are no-ops (the native listener
returned before the shell saw the event); the sync `dom` queries answer
none/false; eval is not granted and is asserted absent on the artifact;
and resource-carrying payloads (files, data-transfer) cannot cross a
structuredClone. The first of these produced a real bug the gate found:
an un-prevented in-page anchor click navigated the FRAME's fragment,
which still appends to the joint session history, so the shell's back
button stepped through frame entries. The frame now refuses in-page
anchor defaults on the guest's behalf.

TWO OF THE THREE DOCUMENTED GAPS CLOSE; THE THIRD IS REOPENED BY THE
FRAME. Rich event families and real bubbling come with the renderer.
`onmounted`/focus closes IN the renderer and is reopened by the sync
`dom` queries being unanswerable across the frame — a different cause
for the same visible symptom, and the honest report is that on the
default backend the auto-focus behaviour is unchanged.

THE SPIKE HAD NO AUTOMATED GATES. It now has two: the existing
differential harness wrapped as a Playwright test, and a real-interaction
test of the dioxus guest inside the frame that also asserts the frame
cannot read the shell's anchor colour. The runtime moved off a pre-async
polyengine onto the sibling's checkout to carry the mutation stream.

SIZE: 224,565 gzipped, from 130 KB. wasi-libc and std enter with the
wasip2 target, the full renderer replaces ~530 lines of glue, and
dioxus-document/dioxus-history arrive for the unconditional providers.

docs/spike-todomvc/ is the justfile's build output and is now ignored
alongside docs/demo/.
…tion now

FOUND BY THE GATE ON ITS FIRST HONEST RUN. Commit 4ec8d2f (2026-08-21,
#74 "Split the WIT contracts") moved visor/surface/surface.ts onto
`polyvisor:surface/*@0.1.0` import keys; spikes/todomvc/wit/todomvc.wit
kept declaring `polymorph:todomvc-spike@0.0.1`, and so did the three
surface guests' generated paths and import specifiers. The interface
bodies were byte-identical, which is why it looked innocuous — but the
package name IS the import key, and every surface component died at
import resolution. Nothing caught it because the spike's correctness
lived in a page a human had to open, and for fifteen days nobody did.
The differential harness, wrapped as a Playwright gate in the previous
commit, was red on this before it could serve the purpose it was written
for.

THE FIX IS A DELETION, NOT A RENAME. Renaming the package in place would
have left two byte-identical definitions of `dom`/`events`/`shell` in the
tree — exactly the arrangement that produced this. The second copy was
the defect. todomvc.wit now declares only the spike's two worlds and
imports `polyvisor:surface@0.1.0` through wit/deps/polyvisor-surface, a
repo-relative symlink to wit/surface: one definition, and the spike
cannot drift from it again. (spikes/visor-dioxus vendors its deps
because its dependency lives OUTSIDE the repo; here a copy is optional
and a link is honest.) 64 lines of duplicated WIT leave; the guests'
module paths and specifiers follow mechanically, with `generate_all`
added where wit-bindgen requires it for a dependency package. No guest
behaviour touched — which is what lets the harness's stepwise DOM
comparison certify the change as purely mechanical.

AND THE QUESTION THE GATE EXISTED TO ANSWER HAS NOW ACTUALLY BEEN ASKED.
The runtime moved off a pre-async polyengine in the previous commit, and
whether the old surface components survived that could not be tested
while they died at import resolution. They do: three backends, fifteen
scripted steps, full-DOM serialisation stepwise identical; eight trap
probes identical; flush-on-trap holding. Evidenced, not assumed.
@lannbot
lannbot enabled auto-merge September 5, 2026 15:27
@lannbot
lannbot merged commit 569ebf1 into main Sep 5, 2026
3 of 5 checks passed
@lannbot
lannbot deleted the todomvc/polyengine-dioxus branch September 5, 2026 16:29
lannbot pushed a commit that referenced this pull request Sep 5, 2026
THE E2E SUITE WENT FROM ~20 CONSECUTIVE FIRST-ATTEMPT PASSES TO ONE PASS
IN SEVEN, starting at #143, and the failures never repeated a scenario:
firefox-smoke's browser process gone, solo-erase's page load timing out,
cross-engine-pairing's sync never arriving, store-outage-recovery's PUTs
failing after three attempts. Different scenario each time is the shape
of a runner under load, not of a broken test.

WHAT #143 ACTUALLY CHANGED FOR THE DEMO: nothing in its build, and one
line in spikes/todomvc/rust-toolchain.toml. But all three workflows key
the shared cargo cache on `**/rust-toolchain.toml`, which matches every
spike's toolchain file though no spike is among the cached workspaces —
so that edit rotated the key, the E2E job restored nothing ("No cache
found" from that run on, "full match: true" on every run before), and
each run built the engine composite cold. And because the E2E job never
saves and CACHE_ON_FAILURE is off, a red main run could not seed the
new key either; only the Pages build on main eventually did.

The key now hashes engine/rust-toolchain.toml alone — the only toolchain
file belonging to a workspace any of these jobs caches — in pages.yml,
e2e.yml and soak.yml alike, since they must match. The spike workspaces
Pages additionally caches carry no toolchain file of their own. A spike
may change its compiler without the demo paying for it.

The mechanism linking a cold build to a browser crash is plausible
(disk and time on a shared runner) and not proven; the correlation is
exact and the fix is right regardless.
lannbot pushed a commit that referenced this pull request Sep 5, 2026
THE SPIKE HAD A LIVE-DEMO LINK TO A DEAD SITE. Its README pointed at a
former polymorph-apps Pages root that 404s, and claimed the built demo
was committed under docs/spike-todomvc/ — which stopped being true when
docs/ moved to CI builds, and is doubly false now that the directory is
gitignored. pages.yml built only docs/demo/. So the re-targeted dioxus
guest (#143) had no URL anyone could open, and a demo nobody can open is
most of how the surface guests stayed broken for fifteen days.

Now pages.yml runs `just build` in spikes/todomvc after the demo build
and checks its key outputs exist before uploading, so a silently empty
build fails the job rather than deploying a hole. It deploys to
  https://polymorph-components.github.io/polyvisor/spike-todomvc/
on every push to main; PRs build and stop at the artifact, as before.

WHAT THE BUILD NEEDS THAT CI LACKED. The dioxus guest depends on the
polyengine-dioxus renderer BY PATH, as a sibling of the repo root — the
same layout setup.sh already uses for polymorph-webrtc-datachannels — so
setup.sh now pins it (lannbot/polyengine-dioxus @ fdc0d52) through the
existing pin_repo helper. That sibling in turn carries its own
.deps/polyengine checkout plus a translator shim, which the spike's
deno.json and translate step reach into; setup.sh clones and pins the
former in the siblings phase (reading POLYENGINE_REV out of the sibling's
justfile rather than duplicating it, so a sibling bump carries it) and
builds the shim in the tools phase, mirroring the sibling's own `just
deps`. wasm32-wasip2 arrives through the spike's rust-toolchain.toml with
no setup change. rust-cache gains the spike's three workspaces; the
shared key already hashes setup.sh, so this diff rotates it.

Verified against a throwaway siblings dir and a disposable worktree,
never the real checkouts: setup.sh pins the right shas, produces the
shim, and is idempotent on re-run; `just build` then succeeds end to end
in ~28s warm. Cold-cache CI cost is estimated at three to five minutes
inside a job the engine composite already dominates.

Also: guest-preact's recipe now runs `npm ci` before `npm run build` — a
fresh checkout without node_modules failed `just build` regardless of
CI. And the README's harness and bench links, also dead, now point at
the new site.
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.

2 participants