Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 104 additions & 54 deletions demo/deno.json
Original file line number Diff line number Diff line change
@@ -1,68 +1,118 @@
{
"//": [
"MODULE-IDENTITY CONSTRAINT (from polymorph-iroh/host-polyengine): the",
"sibling polyengine port modules import @polyengine/runtime/embedder by bare",
"specifier; this file maps the specifier ONCE for the whole graph so",
"`instanceof ComponentException` holds across every boundary.",
"MODULE-IDENTITY CONSTRAINT, RESHAPED BY POLYENGINE AMENDMENT A22. The",
"constraint still exists and this file still discharges it, but it now",
"rests on a DIFFERENT package than it used to.",
"",
" WAS TRUE UNTIL 0.5.0: the sibling polyengine port modules imported",
" @polyengine/runtime/embedder by bare specifier, so this file had to",
" map that specifier ONCE for the whole graph or `instanceof",
" ComponentException` stopped holding across the boundary — and every",
" port's own `^` pin had to land on the same runtime version this file",
" pinned.",
"",
" TRUE NOW (A22, polyengine 1e31210): `@polyengine/runtime/embedder` is",
" APPLICATION-ONLY. Published host modules must not import it at all;",
" they couple to @polyengine/protocol instead, and recognize polyengine",
" values by BRAND PREDICATE (`isComponentException`, `isTrap`, …) rather",
" than by `instanceof`. Protocol copies are harmless BY CONSTRUCTION —",
" identity rests on `Symbol.for` registry symbols, never on the package",
" — so the ports no longer constrain, and are no longer constrained by,",
" our runtime version. @polymorph/webcrypto@0.5.0 and",
" @polymorph/websocket@0.5.0 accordingly name NO @polyengine/runtime",
" specifier anywhere; both take @polyengine/protocol@^0.2.2, which lands",
" on the 0.2.3 pinned above.",
"",
" WHAT THIS FILE STILL OWES THE GRAPH: exactly one @polyengine/runtime,",
" because the APPLICATION's own modules (runtime/engine.ts and the",
" device host) hold real embedder state — and one @polyengine/protocol,",
" for tidiness rather than for correctness. Re-verified post-bump with",
" `deno info`: one @polyengine/runtime@0.5.1, one",
" @polyengine/protocol@0.2.3, no 0.4.x residue.",
"",
"JSR PINS (jsr-pins branch; replaces the IROH_CHECKOUT-era sibling",
"checkouts for these ports; see engine/justfile's PINS",
"block for the endpoint-wasm/relay half of this same pin pair):",
" @polymorph/webcrypto@0.4.0 and @polymorph/websocket@0.4.0 both pin",
" @polyengine/runtime@^0.4.0, the SAME minor line as this file's own",
" @polyengine/runtime@0.4.0 pin above, so ^0.4.0 resolves to 0.4.0",
" and a single copy still serves the whole graph; re-verified",
" post-bump with `deno info` (exactly one @polyengine/runtime@0.4.0,",
" no 0.3.x residue). Both are consumed straight from JSR.",
"checkouts for these ports; see engine/justfile's PINS block for the",
"endpoint-wasm/relay half of this same pin pair).",
"",
" 0.4.0 -> 0.5.1 IS A BREAKING MINOR, and the break is A22 above: the",
" embedder's A9 courtesy re-exports and its concrete handle classes are",
" GONE. `ComponentException`, `isComponentException`, `isTrap`,",
" `toCloneable`, `fromCloneable` and `suspending` all moved to",
" @polyengine/protocol; ~12 modules here changed their import line and",
" nothing else. `instantiate` and `artifactsFromEnvelope` stayed put —",
" they are application surface, which is precisely A22's dividing line.",
"",
" 0.3.1 -> 0.4.0 IS A BREAKING MINOR, and it breaks in two places at",
" once, which is why everything above moved together rather than in",
" patch steps. (1) polyengine amendment A19: the embedder gains",
" `toCloneable`/`fromCloneable` -- a sanctioned, round-trip-exact",
" crossing for the branded error taxonomy over a structured-clone",
" boundary, built with this repo's SharedWorker device host as its",
" named consumer driver -- and renames the brand key again",
" (`witError` -> `componentException`), a hard break with no",
" diagnostic. runtime/device-store/rpc.ts consequently DELETED its",
" hand-rolled brand rather than chase the spelling a third time.",
" (2) polymorph webcrypto#392: SigningKey/VerifyingKey/Ikm gain",
" token-gated constructors plus `fromCryptoKey`/`toCryptoKey`, the",
" by-value crossing that supersedes #390's inject factory and",
" unblocks the platform-posture slice (PERSISTENCE.md's T-A/T-B).",
" The prior 0.3.0 -> 0.3.1 note is kept for the record: context",
" intrinsics resolve their thread by declaring instance",
" (polyengine#213, the #24 residue; ended the browser user-create",
" guest trap, #49).",
" WHY THE BUMP HAPPENED AT ALL: polyengine#239 (af97c13), contained in",
" v0.5.1. 0.4.0's `driveAsync` took its speculative pending-resumption",
" entry UNCONDITIONALLY (0.4.0 src/exec/boundary.ts:1064), and that",
" entry is a STORE-WIDE scheduling gate. Two of this device's OWN",
" periodic drivers collide with it — the worker's 500 ms-debounced",
" non-blocking state-checkpoint (runtime/device-store/worker.ts, a",
" recorded design decision) is near-always in flight, and the solo",
" page's 1 s us-events drain (demo/host/solo.ts) arrives into it and can",
" only hop at the top of its own loop. Ten thousand hops later the",
" internal-bug assert fires: \"driveAsync: a resumed-activation claim was",
" never released\". NO network latency and NO egress are involved, and",
" there is no Gecko differential — it was field-reported from mobile",
" Firefox only because slow OPFS widens the window. Devstore row 47b is",
" the regression gate (RED on 0.4.0, GREEN here).",
"",
" @polymorph/webrtc-datachannels@0.4.0 stays on the sibling checkout",
" DELIBERATELY: it type-checks and its @polyengine/runtime pin matches",
" the same way, but its published module graph breaks this file's",
" `deno bundle --external node-datachannel --external",
" node-datachannel/polyfill` browser-bundling trick -- the resulting",
" serve/demo.js contains eager top-level `import ... from",
" The prior notes are kept for the record. 0.3.1 -> 0.4.0: polyengine",
" A19/A20 gave the embedder `toCloneable`/`fromCloneable` and renamed",
" the brand key (`witError` -> `componentException`), so",
" runtime/device-store/rpc.ts deleted its hand-rolled brand rather than",
" chase the spelling a third time; polymorph webcrypto#392 added the",
" token-gated constructors plus `fromCryptoKey`/`toCryptoKey`. 0.3.0 ->",
" 0.3.1: context intrinsics resolve their thread by declaring instance",
" (polyengine#213, the #24 residue).",
"",
" Also in this window, and absorbed with NO code change here:",
" polyengine f07a3b9 tightened wasi/fs so directory-mutating ops require",
" the `mutate-directory` flag specifically and refuse read-only with the",
" WIT-mandated code. Every preopen we build passes `writable: true`,",
" which the provider turns into read+write+mutate-directory on the",
" preopened descriptor, so the sealed-fs layer and the engine's state",
" root are unaffected — devstore row 7 and the resume batteries confirm.",
"",
" @polymorph/webrtc-datachannels STAYS ON THE SIBLING CHECKOUT, and the",
" reason is unchanged and was RE-MEASURED at this bump: the JSR copy",
" defeats this file's `deno bundle --external node-datachannel --external",
" node-datachannel/polyfill` browser-bundling trick. Pointed at",
" jsr:@polymorph/webrtc-datachannels@0.5.0, serve/solo.js comes out",
" carrying eager top-level `import ... from",
" \"node:url\"/\"node:path\"/\"node:module\"/\"node:stream\"/\"node:events\"`",
" (node-datachannel's native-binding loader), which the browser",
" cannot resolve (CORS-blocked fetch of a bare node: specifier at",
" the page origin) and every e2e scenario times out on page boot.",
" The sibling checkout's identical dynamic `import(\"node-datachannel",
" /polyfill\")` stays genuinely lazy under the same --external flags",
" (this is the README 'Findings' entry on webrtc bundling); something",
" about how JSR's own import-map scoping resolves the nested",
" \"node-datachannel\"/\"node-datachannel/polyfill\" specifiers changes",
" what deno bundle's --external string-matches against. Root-causing",
" that is out of this track's scope -- flagged, not worked around."
" (node-datachannel's native-binding loader reached through",
" `createRequire`), the justfile's `node:` tripwire fires, and every e2e",
" scenario would time out on page boot. The sibling checkout's identical",
" DYNAMIC `import(\"node-datachannel/polyfill\")` stays genuinely lazy",
" under the same --external flags; something about how JSR's own",
" import-map scoping resolves the nested specifiers changes what deno",
" bundle string-matches against. Root-causing that remains out of scope",
" — flagged, not worked around.",
"",
" BUT THE SIBLING CHECKOUT MUST BE AT ITS v0.5.0, and this is a real",
" prerequisite rather than a nicety: at 0.4.0 the sibling's own",
" polyengine-impl/deno.json declares `@polyengine/runtime@^0.4.0`, which",
" Deno DOES consult for this relative import — `deno info` showed a",
" second @polyengine/runtime@0.4.0 in the graph beside our 0.5.1, the",
" exact duplication this block exists to prevent. At v0.5.0 the sibling",
" is A22-clean: it names no @polyengine/runtime specifier at all and",
" takes @polyengine/protocol@^0.2.2, so one runtime serves the graph and",
" the lazy-bundling property is retained. Same class of prerequisite the",
" 0.3.1 -> 0.4.0 bump had, when the sibling was already on its 0.4.0."
],
"nodeModulesDir": "auto",
"minimumDependencyAge": "PT1M",
"imports": {
"@polyengine/runtime/embedder": "jsr:@polyengine/runtime@0.4.0/embedder",
"@polyengine/translator": "jsr:@polyengine/translator@0.4.0",
"@polyengine/wasi": "jsr:@polyengine/wasi@0.4.0",
"@polyengine/wasi/http": "jsr:@polyengine/wasi@0.4.0/http",
"@polyengine/wasi/filesystem-node": "jsr:@polyengine/wasi@0.4.0/filesystem-node",
"@polyengine/wasi/filesystem-web": "jsr:@polyengine/wasi@0.4.0/filesystem-web",
"@polymorph/webcrypto-polyengine": "jsr:@polymorph/webcrypto@0.4.0",
"@polymorph/websocket-polyengine": "jsr:@polymorph/websocket@0.4.0",
"@polyengine/protocol": "jsr:@polyengine/protocol@0.2.3",
"@polyengine/runtime/embedder": "jsr:@polyengine/runtime@0.5.1/embedder",
"@polyengine/translator": "jsr:@polyengine/translator@0.5.1",
"@polyengine/wasi": "jsr:@polyengine/wasi@0.5.1",
"@polyengine/wasi/http": "jsr:@polyengine/wasi@0.5.1/http",
"@polyengine/wasi/filesystem-node": "jsr:@polyengine/wasi@0.5.1/filesystem-node",
"@polyengine/wasi/filesystem-web": "jsr:@polyengine/wasi@0.5.1/filesystem-web",
"@polymorph/webcrypto-polyengine": "jsr:@polymorph/webcrypto@0.5.0",
"@polymorph/websocket-polyengine": "jsr:@polymorph/websocket@0.5.0",
"@polymorph/webrtc-polyengine": "../../polymorph-webrtc-datachannels/polyengine-impl/src/webrtc.ts",
"node-datachannel": "npm:node-datachannel@0.32.3",
"node-datachannel/polyfill": "npm:node-datachannel@0.32.3/polyfill",
Expand Down
66 changes: 34 additions & 32 deletions demo/deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 34 additions & 2 deletions demo/e2e/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,16 +505,48 @@ async function main() {
// absent on OPFS files in this build. The device store never asks for
// one — it writes through `createWritable` — so the boot is green
// regardless. Worth knowing before someone reaches for the sync API.)
const FIREFOX_PREFS: Record<string, boolean> = {
// THE SECOND PREF, and it is a HARNESS fact rather than a lag in
// Playwright's build. `navigator.storage.persist()` under headless
// Playwright Firefox NEVER SETTLES — the promise neither resolves nor
// rejects — because the persistent-storage permission prompt has no UI
// to answer it and headless has nobody to click. Every kept-device
// ceremony awaits that call, so without this pref the whole
// keep-this-device family wedges on a promise that will not settle,
// which reads as a deadline failure in whatever act happened to be
// first. Setting the permission to 1 (ALLOW) answers the prompt the way
// a user who chose to keep the device already answered it. Measured
// 2026-08-23: with it, solo-persistence, solo-storage, solo-gdrive and
// solo-account-storage all pass under Gecko; without it, all four hang.
const FIREFOX_PREFS: Record<string, boolean | number> = {
"javascript.options.wasm_js_promise_integration": true,
"permissions.default.persistent-storage": 1,
};
let firefoxBrowser: Browser | null = null;
const launchFirefox = () =>
firefox.launch({ headless: !headed, firefoxUserPrefs: FIREFOX_PREFS });
/** Which engine the scenario now running asked for. `ctx.browser` and
* `fresh` both read it through `current()` rather than closing over a
* browser, for the same reason the chromium handle is a getter: the
* runner replaces a wedged browser underneath a scenario. */
* runner replaces a wedged browser underneath a scenario.
*
* THE JUGGLER HAZARD, and it constrains how a Firefox-lane scenario may
* be WRITTEN, not just which browser it gets. Calling a
* `WebAssembly.promising` export from inside a `page.evaluate` frame
* SIGSEGVs the Firefox content process — measured 2026-08-23, 4 of 4
* minimal cases, with the identical code in a PAGE SCRIPT running clean
* every time. The fault is in the Juggler protocol's evaluate frame
* meeting a JSPI stack switch, not in the engine: nothing in the wasm
* or the embedder differs between the two paths. Playwright reports it
* as a bare "Target crashed", which names nothing and sends the reader
* looking at the engine.
*
* So: a scenario on the firefox lane must reach engine work through
* page scripts, exposed bindings or event hooks — anything the page
* itself drives — and must never sit in an evaluate frame that reaches
* a promising export. `firefox-smoke` is written to that rule: it
* evaluates only FEATURE PROBES (typeof checks, constructor presence),
* which touch no promising export, and leaves engine instantiation to
* the page's own boot. */
let engine: "chromium" | "firefox" = "chromium";
const current = (): Browser => engine === "firefox" ? firefoxBrowser! : browser;

Expand Down
2 changes: 1 addition & 1 deletion demo/host/bringup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { type Engine, hex, newEngine, unhex, until } from "../../runtime/engine.ts";
import { probeNet, probeNoNet, probeReaderNet } from "./probe-net.ts";
import { type FakeDrive, type FakeSpace, startFakeDrive } from "./fake-drive.ts";
import { ComponentException } from "@polyengine/runtime/embedder";
import { ComponentException } from "@polyengine/protocol";
import type { EngineNet, StoreFetch } from "../../runtime/engine.ts";
import { filesystemNode } from "@polyengine/wasi/filesystem-node";

Expand Down
7 changes: 2 additions & 5 deletions demo/host/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@
// `polyvisor:tasks` import is wired DIRECTLY to the engine
// instance's export — the framework-links-apps-to-services topology.

import {
artifactsFromEnvelope,
ComponentException,
instantiate,
} from "@polyengine/runtime/embedder";
import { artifactsFromEnvelope, instantiate } from "@polyengine/runtime/embedder";
import { ComponentException } from "@polyengine/protocol";
import { createRunner, type Runner } from "../../visor/surface/runner.ts";
import { createFrameBackend } from "../../visor/frame/frame-backend.ts";
import { createSurface } from "../../visor/surface/surface.ts";
Expand Down
Loading
Loading