Long-lived experiment: the upstream iroh crate on components - #25
Conversation
…ymorph-websocket The spike issue #14 and PR #18 point at: upstream iroh v1.0.3 as a wasm32-wasip2 component under jco/JSPI, relay connectivity only, with the relay websocket owned by the host through the polymorph-websocket host module — the same browser-first JS that serves the WIT package — and delivered to the guest as datagrams on a synthetic UDP socket, so the unmodified tokio reactor parks and wakes exactly as in the udp-wake probes. iroh-relay's wasi branch replaces WsBytesFramed with a DatagramPipe (one datagram = one ws message = one relay frame) behind a tiny control protocol to the bridge at 127.0.0.1:1; the in-band challenge-response handshake and everything above it — relay frames, QUIC (noq + rustls/ring, RFC 7250 raw public keys), streams — runs unmodified in-guest against a stock iroh-relay server. Vendored wasi-enablement patches (cargo-vendored sources, iroh untouched except one probe): noq-udp routes wasi to posix_minimal with a non-vectored recv_from (functional single-datagram UDP over wasi:sockets, not a stub); netwatch routes wasi to its netdev-free posix_minimal branch; iroh-dns tolerates the missing system resolver config; iroh-relay gains the wasm_wasi connect(); iroh short-circuits the net_report HTTPS relay probe on wasi (no HTTP client path exists; the websocket dial itself proves reachability), because home-relay selection gates on it. Measured (Node 24.18, two endpoints in one component, stock relay, --dev, loopback): dial-through-relay 23-30ms; echo RTT over the relay p50 ~0.5ms, p90 1.0-1.9ms (50 rounds, 512B); guest component 7.3MB (opt-level=s + lto). Shutdown logs an expected burst of relay_recv_channel-closed errors after done (teardown ordering), which is cosmetic. Manual recipe (research probe, not a ci gate): just iroh-relay-ws.
The wasip2 enablement this spike wrote into vendor/noq-udp turns out to have landed upstream ten days ago as n0-computer/noq#773 (merged 2026-07-27), structurally identical — posix_minimal alias, non-vectored recv_from, AsFd-based UdpSockRef construction — plus one fix this spike's compile-only usage could not catch: set_nonblocking must go through std (ioctl FIONBIO) rather than socket2 (fcntl F_SETFL), which WASI rejects. The vendored copy is now a verbatim backport of #773, and this patch disappears entirely once iroh consumes a noq-udp release containing it.
The noq-udp wasi fix is merged upstream but unreleased (the published 1.1.1 was cut from a pre-merge base), so the vendored backport gives way to a [patch.crates-io] git pin. Released noq 1.1.1 requires noq-udp ^1.1.1 while the git tree identifies as 1.1.0, so a lone noq-udp patch would split the graph (noq keeping the unpatched registry copy); noq and noq-proto ride the same rev to keep one coherent noq-udp. Verified: the spike runs unchanged on the pinned rev (connect 25ms, echo p50 472us). The other four vendored patches have no upstream counterpart to pin (checked at HEAD: netwatch is still espidf-only, iroh-dns has no wasi arm, iroh-relay and iroh have no wasi handling).
The released crates lag upstream too far to keep chasing (noq#773 merged but unreleased, netwatch restructured on main, iroh main carrying relay-path fixes like #4444's reconnect-backoff handling). The guest now declares iroh as a git dependency pinned to main (735958f8), overridden via [patch] with vendor/iroh — the same rev re-vendored plus the net_report probe short-circuit. Its manifest's own path deps pull the sibling vendor/iroh-relay and vendor/iroh-dns (same rev, same spike patches); iroh-base resolves from crates.io, byte-identical to the release at this rev. Every file this spike patches is unchanged between v1.0.3 and main, so the patches ported by copy. Verified end to end against the stock v1.0.3 relay server (a cross-version interop datum in itself): connect 27ms, echo p50 508us, guest 7.2MB.
The next validation target from PR #25's list: WebRTC data channels as an upstream-iroh CustomTransport (unstable-custom-transports), with the channels owned by the host through the polymorph-webrtc-datachannels host module (node-datachannel's W3C polyfill under Node, the browser global in a browser). The guest transport is a thin poll adapter over a second synthetic UDP socket: CustomAddr data is the peer's EndpointId (the TestTransport convention), and the bridge pairs endpoints eagerly at registration over unreliable, unordered channels (label "quic", maxRetransmits=0), with in-process loopback signaling standing in for the real design's relay-borne signaling. The shim's single datagram listener becomes a per-port bridge registry (1 = relay ws, 2 = webrtc). The demo is a two-phase echo: B dials A relay-only and measures RTT, then closes, waits out iroh's per-remote path memory, and redials with only A's webrtc custom addr — phase 2 runs QUIC end-to-end over the data channel. The reconnect-after-expiry shape is forced by upstream (verified against the vendored rev, empirically and in source): - Custom transport addrs are excluded from the n0 NAT-traversal candidate exchange: local_candidates() and the QNT frames are SocketAddr-typed end to end, so holepunching can never probe a custom path. Address lookup is bootstrap-only (trigger_address_lookup early-outs once a path is selected), and ResolveRemote's insert_multiple records addrs without scheduling path opens. No mechanism exists to open a custom path on a live connection. - While any connection to a remote lives, handle_msg_send_datagram sends later dials' Initials only to the selected path, so a second connect cannot probe new addrs either. - A fresh dial fans the Initial out to every path iroh remembers, and the race is winner-take-all: the server drops the same-DCID Initial arriving on a second path during the handshake and the loser is never revisited (upstream's own custom-transport tests pass because the in-memory TestTransport always wins that race). A local relay outruns the data channel, so the webrtc addr only carries the handshake once the per-remote actor's 60s idle expiry has dropped the relay path and the dial is custom-only. Endpoint::remove_relay is no lever: it edits the RelayMap and re-runs net_report but leaves the established home relay link carrying datagrams. Measured (Node 24.18, both endpoints in one component, stock relay, node-datachannel over loopback): phase 1 relay echo p50 ~0.5ms; phase 2 webrtc echo p50 0.73-0.81ms, p90 1.2-1.5ms (50 rounds, 512B); webrtc selected at 0ms after the reconnect; ~178 datagrams each way over one channel; channel pairing 90ms-1.1s from registration. Transport id 0x57525443 ("WRTC") is spike-local; publishing it means a PR against upstream's TRANSPORTS.md registry.
Findings: WebRTC
|
…overlay Replaces the CustomTransport route (c2b4da4) with the overlay approach from issue #26: the guest runs stock iroh IP transports — no custom transport, no unstable-custom-transports feature — and the host bridge assigns each endpoint a synthetic IP address (100.64.0.N:4433, spike-local fiction) whose datagrams it carries over a real node-datachannel unreliable/unordered data channel. The guest feeds the address to iroh through Endpoint::add_external_addr once the bridge reports the channel ready; iroh advertises it to the peer through the NAT-traversal candidate exchange on the live relay connection, the peer's holepunch probes arrive over the channel (there is no NAT — the shim terminates them), the path validates like any direct path, and the biased selector migrates the connection off the relay, keeping the relay as backup. One connection, one stream, echo RTT measured before and after — the live-migration validation target the custom-transport route could not reach, because custom addrs are excluded from the candidate exchange (PR #25 findings). Mechanics: the shim gains a bound-socket registry and address-keyed routes consulted before the per-port bridges; the webrtc bridge speaks a small control protocol on port 2 (register endpoint id + iroh UDP port, receive assigned address, receive channel-ready), pairs channels eagerly at registration, and pushes inbound channel datagrams straight into the peer's iroh UDP socket sourced from the sender's synthetic address. Address lookup stays cleared so the fiction is never published to discovery. Unroutable destinations (net_report QAD probes toward the relay's UDP port, unreachable through the pipe by design) log once instead of per-datagram. The vendored netwatch's UdpSocket::drop deferred the close to spawn_blocking, which panics on wasi (no worker threads) and hung the component's exit; posix_minimal targets now close inline. Measured (Node 24.18, both endpoints in one component, stock relay, node-datachannel over loopback): phase 1 relay echo p50 ~0.5ms; migration 51-53ms after add_external_addr, repeatable; phase 2 echo on the same connection and stream over the data channel p50 0.53-0.54ms with a tighter tail than the relay (max 0.8-4ms vs 6.7-7.1ms); relay retained as a backup path; ~171 datagrams each way; total run ~4s.
Live migration achieved via the synthetic-address overlay (f4a5200)The mid-connection upgrade the custom-transport findings above declared upstream-impossible is now working — by sidestepping the custom-transport machinery entirely (#26). The guest runs stock iroh IP transports (no Measured, repeatable across runs (Node 24.18, both endpoints in one component, stock relay, node-datachannel loopback):
Why this works where the custom transport couldn't: every closed door in the findings inventory is typed against non-IP addresses. Expressed as an IP address behind a network surface we control on every target (the shim is a wasip2 component's only network surface, browsers included), the address enters Two incidental findings:
Standing caveats, tracked in #26 (the generalization issue — gated on exactly this validation, now done): the address fiction must stay scoped (this spike clears address lookup so it is never published to discovery; a real deployment needs that guarantee explicitly), 100.64/10 is a spike-local placeholder for a random-prefix ULA, and loss/MTU behavior over lossy channels (SCTP chunk-loss amplification for >MTU packets) is still an unmeasured claim. The custom-transport findings comment above remains the upstream-facing record: native iroh users without a controllable network surface still need the seam it proposes. |
The same transpiled component, shim, and bridge JS that run under Node now run in Chromium: browser WebSocket to the stock relay, browser RTCPeerConnection for the data channels (the polymorph modules' browser-first paths, previously exercised only through polyfills), JSPI stable, no flags. The shim drops its Node imports for standard globals (performance, crypto.getRandomValues), keeping process.hrtime's nanosecond clock under Node and taking RUST_LOG from globalThis.RUST_LOG in a browser. browser.html + browser-run.mjs mirror the run.mjs driver, reporting into globalThis.__spike; browser-test.mjs is the Playwright harness — serves the repository root (bridge imports reach ../../../.deps, and COOP/COEP buys 5us timers for the guest's RTT measurements), reuses or starts the relay, mirrors the console, and asserts the migration and phase-2-over-ip lines. `npm run test:browser`. Measured (Chromium via Playwright 1.62, same machine as the Node numbers): channel pairing 36ms (browser ICE vs node-datachannel's ~1s), migration 52-53ms after add_external_addr — identical to Node — phase 2 echo p50 390-445us, faster than the browser's own relay path (p50 ~735us) and than the Node data-channel path (p50 ~520-540us). Total run ~4s, repeatable.
Browser validation (dfb2139)The overlay live-migration now runs in a real Chromium under Playwright ( Measured (Chromium via Playwright 1.62, same machine, repeatable):
Two observations worth keeping:
Harness shape: browser-test.mjs serves the repository root (COOP/COEP for 5us timers), reuses or starts the relay, mirrors the page console, waits on |
…ages Two browsers, one iroh connection: visiting the page hosts a session and shows a QR of `#j=<endpoint id>&r=<relay url>`; scanning it joins. Clicks and taps ripple on a square window-fit canvas, mirrored to the peer. The connection bootstraps through the relay (n0 public map by default, `?relay=` override for local runs) and live-migrates onto a WebRTC data channel via the synthetic-address overlay (issue #26) — the demo's status line shows the path switching from relay to direct. Pieces: - guest/: one iroh endpoint per page, stock IP transports. The page owns all demo semantics; the guest ferries opaque JSON frames over one bi stream (u16-framed), reports status events (ready, connected, path changes from polling the selected path, closed), runs the overlay control client from the spike, and hands the synthetic addr to iroh when the page's bridge reports the channel ready. The joiner writes an empty frame at open — a fresh QUIC stream is invisible to the acceptor until bytes flow, and the host's accept_bi would otherwise deadlock the signaling ferry against the page's offer. - web/overlay.mjs: the spike's overlay bridge reshaped for two real browsers: synthetic addresses derive from endpoint ids (both pages agree without an exchange), signaling (SDP + trickled candidates) rides the ferry over the authenticated relay connection, host initiates (no glare), STUN for cross-network ICE. - web/demo.mjs + index.html: canvas, ripples, QR (vendored qrcode-generator), status line, GUEST_ENV plumbing (role, peer, relay) through the shim's new environment passthrough. - coi-serviceworker (vendored): injects COOP/COEP on Pages for 5us timers; everything is same-origin so isolation blocks nothing. - build.sh assembles a fully self-contained static site (the polymorph webrtc/websocket modules vendored in; the relay bridge's .deps import rewritten) — Pages-ready, no runtime dependencies. - web/test.mjs: Playwright harness — host + joiner + a third page against a local relay; asserts the join URL bootstrap, live migration to the direct path on both sides (relay ~13ms → direct ~1.4-2.2ms through two JSPI pages), pings mirrored both ways after migration, and the third participant refused (single-pair sessions).
Playwright against the Pages deployment: session bootstrap over the n0 public relays, live migration to the direct path on both pages, pings mirrored both ways. First run measured relay rtt 530-760ms (distant public relay) migrating to 1.2-30ms direct.
Public demo on GitHub Pages (2eb6d8b, 6fbe60d)https://polymorph-components.github.io/polymorph-iroh/ — open it in Chrome/Edge (desktop or Android), scan the QR with a second device (or open the link in another window), and tap: ripples mirror between the two, with the status line showing the connection migrate from This is the whole stack in production shape, end to end:
Verified against the live deployment ( Notes and boundaries: JSPI limits the demo to Chromium desktop + Android (iOS Safari cannot run it — accepted for this demo); the deployment lives on the |
Peer departure is now signalled as fast as the platform allows and bounded when it does not: pagehide sends a best-effort bye (an empty datagram; the guest closes the connection with it), chosen over visibilitychange deliberately — task switches only fire the latter and must keep the session alive — and the QUIC idle timeout drops from quinn's default 30s to 8s (keepalives are on by default, so live sessions never idle out; only dead peers do). bfcache restores reload the page: a frozen guest's session is long dead, and the reboot rejoins by itself. Sessions are rejoinable from both sides. The guest now loops: the host returns to accepting when a session ends (still refusing extras while one is active) and the joiner redials every 2s with each attempt bounded at 5s so a mid-reload host cannot stall the loop. Identity persists per tab — the page keeps the ed25519 secret in sessionStorage and passes it through GUEST_ENV — and the host writes the join payload into its own URL fragment, so role detection is "does the fragment name me?": the host's tab re-hosts on refresh, a fresh tab on the same URL joins. The page shows "peer left · scan to rejoin" (host, QR returns) or "peer left · reconnecting…" (joiner). A small "i" button toggles a live debug panel: role, state, self and peer endpoint ids, relay, selected path with RTT, channel and datagram counters, ping counts. The test grows a rejoin scenario: host reloads mid-session; asserts the joiner sees the departure within the idle-timeout bound, the host re-hosts under the same identity, the joiner reconnects and re-migrates to the direct path, and pings mirror again.
Adds timely peer-left signalling, rejoinable sessions with per-tab identity, and the debug panel. See PR #25 on the source repository.
Session lifecycle update (8bdd434, deployed)The demo now handles departures and rejoins:
The Playwright harness grew a rejoin scenario (host reloads mid-session → joiner sees the departure within the idle-timeout bound → host re-hosts under the same identity → joiner reconnects, re-migrates to direct, pings mirror again) — all green locally, and the deployed site re-verified over the n0 public relays with Known residual: Chrome's intensive timer throttling (tab hidden >5min) can starve the guest's keepalives and idle the session out even though the tab is alive; the session loops self-heal on foregrounding — same path as rejoin. Noted rather than engineered around. |
…load Joining burned ~3s before the dial even started. The bulk was net_report: on wasi the QAD probes ride UDP straight to the relay's QUIC socket, which no wasi host can deliver (the relay leg is a websocket pipe), so every probe waited out the full 3s PROBES_TIMEOUT — stalling the report, home-relay selection, and Endpoint::online behind it. The vendored short-circuit that already covered the HTTPS probe now also skips QAD on wasm_wasi; the report completes from the nominal HTTPS latencies in ~200ms. External addresses come from the host via add_external_addr, so nothing consumed the probe results anyway. The demo joiner also no longer waits for online() at all: it dials the host's relay directly (its own home relay only matters for inbound dials, which never happen), taking boot-to-dial from ~3s to ~50ms locally. The host still waits — its home relay is the rendezvous in the QR. The coi-serviceworker goes: its first-visit reload cost more than the 5us timers were worth (the RTT readout is fine at 100us), and the test server drops its isolation headers to match production. The joiner's dial bound widens 5s -> 10s for slow real-network relay handshakes, still bounding hung attempts. Measured locally (Playwright harness): host boot->QR 3098ms -> 295ms; joiner boot->connected 3121ms -> 85ms, migrated by ~590ms; the status console logs now carry t+ms for exactly this kind of reading. The iroh-relay-ws spike browser test still passes with the vendor change (and boots faster for the same reason).
Join latency (9be9393, deployed)Joining burned ~3s before the dial even started. Breakdown and fixes, measured with the harness's new
Local timeline, before → after: host boot→QR 3098ms → 295ms; joiner boot→connected 3121ms → 85ms, migrated to direct by ~590ms. The remaining real-world join cost is honest work: wasm download (~8MB site, streamed compile) plus the relay wss + QUIC handshakes. Deployed and re-verified live over the n0 public relays (this runner homes to an aps1 relay at 500–760ms RTT; migration takes the session to 0.6–29ms — the status line's relay→direct switch is very visible at that distance). The iroh-relay-ws spike inherits the vendor change and its browser test still passes, booting faster for the same reason. |
…ation The ecosystem-compatibility validation target from PR #25 (and the driving question of #14): unmodified iroh-blobs 0.103 (default-features off: MemStore, no fs-store, no rpc) compiled to wasm32-wasip2 against the vendored iroh and ran end to end on the first attempt — the mem path has no spawn_blocking/thread use, and tokio's "sync" feature is all it asks for. The crates-io iroh dependency is substituted with the vendored copy via [patch.crates-io]; the sibling spike's shim, bridges, and webrtc overlay are shared through re-export stubs. The guest runs a provider (stock MemStore + BlobsProtocol behind the stock Router) and a fetcher in one component: bao-verified fetch of blob 1 over the relay, live migration onto the data channel via the synthetic-address overlay, then a verified fetch of blob 2 over the migrated connection. BLOB_MB sizes the blobs (default 4MiB). Measured (local relay, loopback): - in-guest blake3/bao hashing: 392-579 MiB/s (wasm, single thread) - Node 24: relay 7.8-8.8 MiB/s; direct (node-datachannel) 6.1-6.3 MiB/s - Chromium: relay 6.0 MiB/s; direct (native RTCDataChannel) 9.3 MiB/s - migration mid-session: 51-53ms, same as every prior measurement The plateau is the per-datagram JS bridge hop, not the protocol stack; the browser's native SCTP outruns its relay path, node-datachannel's does not — the same host-quality split the ping demo showed. `./run.sh` (Node), `node browser-test.mjs` (Chromium).
Ecosystem validation: stock iroh-blobs runs on the stack (e43295a)The driving question of #14 — can unmodified ecosystem crates run on this endpoint — now has an empirical yes for iroh-blobs. iroh-blobs 0.103, default features off (MemStore, no fs-store, no rpc), compiled to wasm32-wasip2 against the vendored iroh and ran end to end on the first attempt. No patches to blobs; the mem path has no spawn_blocking/thread usage and asks tokio only for "sync". The spike ( Measured (local relay, loopback, 4–16MiB blobs):
Two observations:
This retires the "Ecosystem crates" validation target. The natural follow-ups it opens: a file-drop demo on the Pages site (blobs + the existing session bootstrap), and the remaining targets (identity split, upstreaming, overlay hardening). |
The "+" button (or a drag-and-drop) sends files to the session peer:
the page streams the bytes to its guest in tagged datagram chunks, the
guest adds them to an in-memory blob store and the page offers the hash
over the ferry; the receiving page asks its guest to fetch, which opens
a blobs-ALPN connection to the peer (no addresses — the live session
already gave iroh the path, so the transfer rides the migrated data
channel) and runs a stock bao-verified iroh-blobs fetch, streaming
progress and then the bytes back up as a download link. The receiver
acks over the ferry so the sender's card shows delivery.
Guest: iroh-blobs 0.103 (MemStore) + serde_json for the two page
commands it now intercepts ("send", "fetch"); a single acceptor
dispatches by ALPN — blobs connections go to the stock BlobsProtocol
handler on both roles (the joiner serves too: when it sends, the host
fetches), session connections to the host's session loop with the
busy-refusal folded in. Uploads are capped at 64 MiB and single-flight
per direction.
The harness sends 2MiB each way and asserts verified receipt (state
done implies bao verification passed, byte count matches) plus the
sender ack; the live check gains a 1MiB transfer against the deployed
site. Measured locally over the migrated channel: 10.8 MiB/s cold
(slow start), ~100 MiB/s warm loopback.
File drop shipped on the demo (250ea11, deployed)The Pages demo now transfers files: "+" button or drag-and-drop, offered over the ferry, fetched by the receiving guest through stock iroh-blobs (MemStore, bao-verified) on a blobs-ALPN connection that rides the session's migrated data channel — the no-addresses dial works exactly as the machinery reading predicted (the live session's selected path carries new connections to the same peer, the 'Initial trap' as a feature). Receiver acks over the ferry; the sender card shows delivery; downloads land as object-URL links. Measured: locally 2MiB each way at 10.8 MiB/s cold / ~100 MiB/s warm loopback; against the live deployment over the n0 relays, 1MiB verified at 8.7 MiB/s on the migrated channel. The harness asserts verified receipt (done implies bao verification, byte counts match) plus sender acks in both directions, alongside the whole existing suite (migration, pings, third-refusal, host-reload rejoin) — all green. Guest changes: iroh-blobs 0.103 + serde_json; one acceptor dispatching by ALPN (blobs served on both roles — the joiner provides when it sends), session busy-refusal folded in; page↔guest datagrams grew two binary chunk tags for upload/download streaming, 64 MiB cap, single-flight. With this, the demo exercises the full claim end to end in public: stock iroh + stock iroh-blobs, in browsers, bootstrapped by production relays, live-migrating onto WebRTC, moving real files with verified integrity. |
The iroh/iroh-relay/iroh-dns deltas now live as reviewable PR branches on the fork (lann/iroh#1 iroh-dns resolver-config fallback, #2 relay connect over the datagram pipe, #3 net_report probe gate), merged into its `polymorph-iroh` branch: upstream main at the same pinned rev (735958f8) plus exactly the patches the vendor tree carried, verified byte-identical before the swap. The three guests consume the branch as a git dependency; iroh-base is patched to the same branch in the blobs and ping-demo guests so iroh and iroh-blobs share one copy of its types (crates-io iroh-base 1.0.3 otherwise coexists as a second, type-incompatible instance). netwatch stays vendored: its upstream is n0-computer/net-tools, not the iroh repo, so it is out of scope for the fork. Verified: all three guests build; iroh-relay-ws Node and browser runs (migration 51ms, phase-2 p50 410-664us), iroh-blobs verified fetches across the migration, and the full ping-demo Playwright suite pass.
De-vendored: the iroh deltas now live on a fork branch (61f4f5f)The vendored iroh/iroh-relay/iroh-dns copies are gone. The deltas are now reviewable PR branches on lann/iroh — each one a logical unit of the future upstreaming worklist:
The PRs stay open (not merged); their branches are merged into Still vendored: netwatch (upstream is n0-computer/net-tools, not the iroh repo — same treatment there is a follow-up if wanted). The delta table in the PR description now under-describes reality in the other direction: the iroh-side rows are the fork PRs. Re-verified after the swap: all three guests build; iroh-relay-ws Node + browser runs (migration 51ms, phase-2 p50 410–664µs); iroh-blobs verified fetches across the migration; full ping-demo Playwright suite green (migration, pings, file transfer both ways, third-refusal, rejoin). |
…oh branch Same treatment as the iroh crates: the two netwatch deltas now live as PR branches on the fork (lann/net-tools#1 wasi posix_minimal/netdev routing, #2 inline socket close on drop), merged into its `polymorph-iroh` branch — the netwatch-v0.19.1 tag (iroh main still requires ^0.19.1) plus exactly the patches the vendor tree carried, source files verified byte-identical before the swap. The guests consume the branch through [patch.crates-io]; the vendor tree is gone. Verified: all three guests build; iroh-relay-ws run (migration 53ms), iroh-blobs verified fetches across the migration, and the full ping-demo Playwright suite pass.
netwatch de-vendored too (4f2d711)Same treatment as the iroh crates, on lann/net-tools:
Branches base on the One correction vs. the netwatch delta discussion earlier in this thread: the old drop path did not leak the fd when no runtime was current (the returned socket drops and closes either way) — the fix only moves where the close runs. The fork PR describes it accurately. With this, every local delta is a reviewable fork PR: iroh in lann/iroh#1–3, netwatch in lann/net-tools#1–2, noq already upstream (noq#773, awaiting release). Re-verified after the swap: all three guests build; relay-ws run (migration 53ms), blobs verified fetches across the migration, full ping-demo Playwright suite green. |
Evaluated at dbad4d7d (previous pin 30186b2b). The branch's new work is CM-async machinery: sync-lowered calls into async-lifted callees (sync-start-call, caller task-register restore, spilled-result return pointers), future.transfer and stream.transfer between guests, concurrent task lifetimes within one instance, and composed guest-to-guest regression tests for all of it — the jco half of what a 0.3-shaped world needs. Test evaluation on this machine (arm64): - packages/jco suite: 77 passed, 1 skipped, 0 failed. - packages/jco-transpile suite (includes the P3/CM-async tests: futures, streams, transfers, backpressure, cancellation, deadlock regressions, inter-task wakeup): 366 passed, 51 skipped; the only failures are browser-launch environmentals — puppeteer's bundled x86-64 Chromium cannot run here. With an arm64 Chromium substituted, the JSPI async browser test passes; the three test/browser/general failures reproduce identically at the previous pin (pre-existing, environmental, not regressions). - test/components extended suite: not runnable here (needs wasi-sdk, componentize-py, wac); polyglot issue reproductions, unrelated to the async work. Regression against this repository's stack, all retranspiled at the new head: iroh-relay-ws spike (Node) migrates in 52ms with phase 2 over the data channel; the ping-demo Playwright suite (migration, pings, file transfer both ways, third-participant refusal, host-reload rejoin) passes; the iroh-blobs spike passes both fetch phases.
The standing branch for validating the upstream-iroh-crate-on-components route from #14 — long-lived by design, not for merging in this shape. Vendored/pinned upstream deps live here while validation continues and must never reach main: the endgame is upstreaming the wasi patches (as n0 already did for noq-udp in noq#773, convergent with ours) and porting only the minimal glue main needs.
Currently proven on this branch
just iroh-relay-ws.Endpoint::add_external_addrfeeds the address into the NAT-traversal candidate exchange on the live relay connection; the holepunch probes are terminated by the shim (no NAT exists); the path validates and the selector migrates. One connection, one stream: relay p50 ~0.5ms → migration 51–53ms after the address is handed over → webrtc p50 0.53–0.54ms with a tighter tail than the relay (max 0.8–4ms vs 6.7–7.1ms), relay retained as backup. Total run ~4s.WebSocketto the stock relay, browserRTCPeerConnectionfor the channels, JSPI stable with no flags. Channel pairing 36ms (browser ICE vs node-datachannel's ~1s), migration 52–53ms (identical to Node), phase 2 p50 0.39–0.45ms — faster than the browser's own relay path (~0.74ms).npm run test:browser.CustomTransport(c2b4da4, superseded by the overlay route but the findings stand): QUIC end-to-end over the same channels viaunstable-custom-transports. The upgrade model's migrate-mid-connection leg is upstream-impossible today for custom addrs — they are excluded from the NAT-traversal candidate exchange and nothing else opens paths on live connections; findings comment below has the full mechanism inventory. That demo reconnected after iroh's 60s per-remote path memory expiry (relay p50 ~0.5ms → webrtc p50 0.73–0.81ms).web/live-check.mjs; local CI-able harness inweb/test.mjs). Chromium/Android only (JSPI); sessions are single-pair. The "+" button / drag-and-drop sends files through stock iroh-blobs (MemStore, bao-verified) over a blobs-ALPN connection riding the migrated path — verified live at 8.7 MiB/s through the deployment.BlobsProtocol/Router, fetched over the relay, then over the live-migrated data channel on the same connection. In-guest blake3 hashing 392–579 MiB/s; transfer 6–9 MiB/s with the per-datagram JS bridge hop as the plateau (browser native SCTP: direct 9.3 > relay 6.0; node-datachannel inverts it). The ecosystem-compatibility answer to Upstream iroh crate as endpoint core: feasibility and the ecosystem-compatibility ruling #14's driving question, now empirical.experiments/iroh-blobs/.Delta inventory vs upstream (the future upstreaming worklist)
wasm_wasiconnect() + DatagramPipe (the relay-dialer seam, concretely)netdevalias); UdpSocket::drop closes inline on posix_minimal (spawn_blocking panics on wasi, no worker threads)Next validation targets