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
203 changes: 104 additions & 99 deletions demo/host/demo.ts

Large diffs are not rendered by default.

54 changes: 20 additions & 34 deletions demo/host/solo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,7 @@
// in-page embedder, deliberately, so the two pages remain two different
// arguments.

import {
artifactsFromEnvelope,
instantiate,
} from "@polyengine/runtime/embedder";
import { createRunner, type Runner } from "../../visor/surface/runner.ts";
import { createFrameBackend } from "../../visor/frame/frame-backend.ts";
import { createSurface } from "../../visor/surface/surface.ts";
import { mountApp as mountVisorApp, type Mounted } from "../../visor/frame/mount.ts";
import {
initVisor,
type SurfaceIdentity,
Expand Down Expand Up @@ -1613,40 +1607,32 @@ async function startApp(

// --- the app -------------------------------------------------------------

let appRunner: Runner | null = null;
let appSurfaceMount: Mounted<AppExports> | null = null;
let appMounted = false;

/** Instantiate the app guest over THIS device's engine, in a real
* sandboxed frame (#16). Structurally the same block as demo.ts's
* `mountApp`, and deliberately so: the frame backend, the surface, the
* runner and the `polyvisor:tasks` import being the engine's own
* export object ARE the framework's app-mount shape. What differs is
* that the export object is a REMOTE oneevery call is a port round
* trip, which the app cannot tell apart because both are async. */
* sandboxed frame (#16) — through the same app-mount seam demo.ts
* uses (visor/frame/mount.ts), which is where the frame, the surface
* and the serialized guest-call chain now live. What differs between
* the two pages is only the `polyvisor:tasks` import: here the export
* object is a REMOTE one, every call a port round trip, which the app
* cannot tell apart because both are async. */
const mountApp = async () => {
if (appMounted) return;
appMounted = true;
const container = document.getElementById("solo-app")!;
let dispatch: (ev: UiEvent) => void = () => {};
const frameBackend = createFrameBackend(container, (ev) => dispatch(ev));
const backend = await frameBackend.backend;
const surface = createSurface(backend, () => "");
const instance = await instantiate(
artifactsFromEnvelope(appArt.envelope, appArt.bytes),
{
...surface.imports,
const mounted = await mountVisorApp<AppExports>({
container,
artifact: appArt,
imports: {
// The framework seam: the app's data-service import IS this
// device's `tasks` export, proxied over the port.
"polyvisor:tasks/tasks@0.1.0": tasks,
"polyvisor:tasks/tasks@0.1.0": tasks as unknown as Record<string, unknown>,
},
);
const app = instance.exports as unknown as AppExports;
const runner = createRunner(surface);
dispatch = (ev) => {
runner.call(() => app.onEvent(ev)).catch((e) => status(`event: ${err(e)}`));
};
await runner.call(() => app.run());
appRunner = runner;
onEventError: (e) => status(`event: ${err(e)}`),
});
await mounted.exports.run();
appSurfaceMount = mounted;
// The app's row in the trust table: ONE artifact, ONE record, keyed
// by the name the visor fetched it by.
const { mark, isNew } = sheets.marks.mark(APP_ARTIFACT);
Expand All @@ -1668,12 +1654,12 @@ async function startApp(
visor.renderContext();
// Remote changes surface as revision bumps; poll on a UI cadence,
// skipping a tick whose predecessor is still in flight (an unbounded
// `runner.call` chain is how demo.ts once wedged a page).
// chain of guest calls is how demo.ts once wedged a page).
let polling = false;
setInterval(() => {
if (polling) return;
polling = true;
runner.call(() => app.poll()).catch(() => {}).finally(() => {
mounted.exports.poll().catch(() => {}).finally(() => {
polling = false;
});
}, 400);
Expand Down Expand Up @@ -5476,7 +5462,7 @@ async function startApp(
if (await entry?.joinHandle.tick()) void joinerWire();
await drainAndAdopt();
},
appRunner: () => appRunner !== null,
appRunner: () => appSurfaceMount !== null,
/** The storage sheet, entered the way a user enters it. */
openStorageSheet: () => openStorage(),
/** THE CONSUMED-KIT SENTENCE this boot announced, or "" — the one
Expand Down
22 changes: 17 additions & 5 deletions demo/scripts/check-invariants.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,22 @@ bad() {
# the visor's own voice. So it must not appear anywhere on the seam.
echo "[1/9] petname never crosses the frame seam"
echo " (the visor's word for a component is never readable or influenceable by it)"
hits=$(grep -n "petname" ../visor/frame/frame-backend.ts ../visor/frame/frame.ts ../visor/frame/frame.html 2>/dev/null)
# WHAT "THE SEAM" IS, as a GLOB — for the reason check (b) gives at its
# own file list. The property is about every file the visor ships into
# or across the frame boundary, so the scan should follow the next one
# that layer grows rather than need this list edited. It grew one
# already: frame/mount.ts, the app-mount seam (#142), which holds the
# mount options and is therefore exactly where a caller would hand a
# component something it must not have.
FRAME_SEAM="../visor/frame/*.ts ../visor/frame/*.html"
# shellcheck disable=SC2086
hits=$(grep -n "petname" $FRAME_SEAM 2>/dev/null)
if [ -n "$hits" ]; then
bad "petname appears on the frame seam:"
printf '%s\n' "$hits" | sed 's/^/ /'
else
ok "no petname reference in ../visor/frame/frame-backend.ts, ../visor/frame/frame.ts, ../visor/frame/frame.html"
# shellcheck disable=SC2086
ok "no petname reference in $(echo $FRAME_SEAM)"
fi

# --- (b) the visor never writes the word "password" ---------------------------
Expand Down Expand Up @@ -140,13 +150,15 @@ fi
# cluster's id may appear anywhere on the seam.
echo "[5/9] the user's identity never crosses the frame seam"
echo " (name, device and icon are visor pixels; no component may read or steer them)"
idhits=$(grep -n "pm-demo-identity\|visor-identity" \
../visor/frame/frame.ts ../visor/frame/frame-backend.ts ../visor/frame/frame.html 2>/dev/null)
# Same seam, same glob as check (a) — one definition, both properties.
# shellcheck disable=SC2086
idhits=$(grep -n "pm-demo-identity\|visor-identity" $FRAME_SEAM 2>/dev/null)
if [ -n "$idhits" ]; then
bad "the visor identity record appears on the frame seam:"
printf '%s\n' "$idhits" | sed 's/^/ /'
else
ok "no identity reference in ../visor/frame/frame.ts, ../visor/frame/frame-backend.ts, ../visor/frame/frame.html"
# shellcheck disable=SC2086
ok "no identity reference in $(echo $FRAME_SEAM)"
fi

# --- (f) pairing code and SAS render only in visor-owned surfaces --------
Expand Down
10 changes: 10 additions & 0 deletions spikes/todomvc/deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
{
"//": [
"@polyengine/runtime/embedder and @deltic/runtime/embedder below name",
"the SAME file on purpose. The visor's app-mount seam",
"(visor/frame/mount.ts) is shared code, imported by this spike AND by",
"demo/, and demo/deno.json calls the embedder @polyengine/runtime/embedder.",
"Both keys resolve to one URL, so the graph still holds exactly one",
"embedder and one exception brand — which is the property that",
"mattered, not the spelling."
],
"imports": {
"@deltic/runtime/embedder": "../../../polyengine-dioxus/.deps/polyengine/runtime/src/embedder/mod.ts",
"@polyengine/runtime/embedder": "../../../polyengine-dioxus/.deps/polyengine/runtime/src/embedder/mod.ts",
"@deltic/runtime/shim": "../../../polyengine-dioxus/.deps/polyengine/runtime/src/shim/mod.ts",
"@deltic/protocol": "../../../polyengine-dioxus/.deps/polyengine/protocol/src/mod.ts",
"@deltic/translator": "../../../polyengine-dioxus/.deps/polyengine/translator/mod.ts",
Expand Down
89 changes: 55 additions & 34 deletions spikes/todomvc/host/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { Backend, BackendKind } from "../../../visor/surface/backend.ts";
import { createDirectBackend } from "../../../visor/surface/backend-direct.ts";
import { createQueuedBackend } from "../../../visor/surface/backend-queued.ts";
import { createChannelBackend } from "../../../visor/surface/backend-channel.ts";
import { createFrameBackend } from "../../../visor/frame/frame-backend.ts";
import { mountApp } from "../../../visor/frame/mount.ts";
import { createApplier } from "../../../visor/surface/applier.ts";
import type { UiEvent } from "../../../visor/surface/events.ts";
import { createRunner, type Runner } from "../../../visor/surface/runner.ts";
Expand Down Expand Up @@ -58,10 +58,10 @@ export async function instantiateWorld(
// --- backends ------------------------------------------------------------------

/** The three backends `createBackend` builds synchronously, in-realm.
* "frame" is deliberately excluded from this type: its construction is
* async (a handshake with the sandboxed frame's own document — see
* createFrameBackend), so every caller branches on it separately rather
* than folding it into this switch (see `resolveBackend` below). */
* "frame" is deliberately excluded from this type: it is not a backend
* this file builds at all any more — it is the app-mount seam
* (visor/frame/mount.ts), which stands up the frame, the surface and the
* runner together. Both callers branch on it before reaching here. */
export type SameRealmBackendKind = Exclude<BackendKind, "frame">;

export function createBackend(
Expand All @@ -83,30 +83,6 @@ export function createBackend(
}
}

/** The frame surface's teardown, handed back to the caller so `kind ===
* "frame"` can be torn down on demand (see TodoApp.teardown). undefined
* for the three same-realm kinds, which need none: retiring one of those
* is pausing the runner forever and dropping the DOM node, both of which
* the caller can do without help from here. */
type Teardown = (() => Promise<void>) | undefined;

/** Resolve one backend for `kind`, awaiting the frame handshake when
* `kind === "frame"` and constructing synchronously otherwise (a small
* internal async step either way — `createBackend`'s own signature and
* the three same-realm cases inside it are unchanged). */
async function resolveBackend(
kind: BackendKind,
container: HTMLElement,
dispatch: (ev: UiEvent) => void,
): Promise<{ backend: Backend; teardown: Teardown }> {
if (kind === "frame") {
const frameBackend = createFrameBackend(container, dispatch);
const backend = await frameBackend.backend;
return { backend, teardown: () => frameBackend.destroy() };
}
return { backend: createBackend(kind, container, dispatch), teardown: undefined };
}

// --- the TodoMVC app ------------------------------------------------------------

export interface TodoExports {
Expand All @@ -116,7 +92,13 @@ export interface TodoExports {
}

export interface TodoApp {
runner: Runner;
/** The serialized guest-call chain — present for the three same-realm
* kinds only. `kind === "frame"` goes through the app-mount seam
* (visor/frame/mount.ts), which owns its runner and exposes what a
* caller may do with it (`exports`, suspension, teardown) rather than
* the chain itself. The two harness consumers of `settle`/`generation`
* (harness.ts, bench.ts) sweep the same-realm kinds only. */
runner?: Runner;
exports: TodoExports;
/** Inject a synthetic event record (harness use). */
sendEvent(ev: UiEvent): Promise<void>;
Expand All @@ -141,10 +123,40 @@ export async function startTodoApp(
onEventError: (e: unknown) => void,
artifact = "todomvc",
): Promise<TodoApp> {
// THE FRAME KIND IS NOT A BACKEND CHOICE ANY MORE, it is the app-mount
// seam: the frame, the surface and the runner all live behind
// `mountApp` (visor/frame/mount.ts), which is what the visor's own
// pages use. The three same-realm kinds below stay exactly as they
// were — they are the differential harness's instrument, not a
// product placement.
if (kind === "frame") {
const { envelope, bytes } = await loadArtifacts(artifact);
const mounted = await mountApp<TodoExports>({
container,
artifact: { envelope, bytes },
imports: {},
// The seam holds a route VALUE, not the caller's getter, so the
// route travels with the call that announces it. Every route
// change in this spike already comes through `sendRoute`.
route: route(),
onEventError,
});
await mounted.exports.run();
return {
exports: mounted.exports,
sendEvent: (ev) => mounted.exports.onEvent(ev),
sendRoute: (r) => {
mounted.setRoute(r);
return mounted.exports.onRoute(r);
},
teardown: () => mounted.destroy(),
};
}

// DOM-originated events land on the same serialized chain as everything
// else; the exports binding below closes the loop.
let dispatch: (ev: UiEvent) => void = () => {};
const { backend, teardown } = await resolveBackend(kind, container, (ev) => dispatch(ev));
const backend = createBackend(kind, container, (ev) => dispatch(ev));
const surface = createSurface(backend, route);
const exports = (await instantiateWorld(
artifact,
Expand All @@ -160,7 +172,6 @@ export async function startTodoApp(
exports,
sendEvent: (ev) => runner.call(() => exports.onEvent(ev)),
sendRoute: (r) => runner.call(() => exports.onRoute(r)),
teardown,
};
}

Expand All @@ -172,15 +183,25 @@ export interface LabExports {
}

export interface LabApp {
runner: Runner;
/** Same-realm kinds only — see TodoApp.runner. */
runner?: Runner;
exports: LabExports;
}

export async function startLab(
kind: BackendKind,
container: HTMLElement,
): Promise<LabApp> {
const { backend } = await resolveBackend(kind, container, () => {});
if (kind === "frame") {
const { envelope, bytes } = await loadArtifacts("lab");
const mounted = await mountApp<LabExports>({
container,
artifact: { envelope, bytes },
imports: {},
});
return { exports: mounted.exports };
}
const backend = createBackend(kind, container, () => {});
const surface = createSurface(backend, () => "");
const exports = (await instantiateWorld(
"lab",
Expand Down
5 changes: 3 additions & 2 deletions spikes/todomvc/host/bench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ async function benchBackend(kind: BackendKind, n: number): Promise<Sample> {

const timed = async (f: () => Promise<void>): Promise<number> => {
const t0 = performance.now();
await runner.call(f);
await runner.settle();
// `runner!`: BACKENDS above is same-realm only; see app.ts's LabApp.
await runner!.call(f);
await runner!.settle();
return performance.now() - t0;
};

Expand Down
12 changes: 7 additions & 5 deletions spikes/todomvc/host/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,12 @@ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));

async function quiesce(app: TodoApp): Promise<void> {
for (;;) {
const g = app.runner.generation;
await app.runner.settle();
// `runner!`: this sweep is same-realm kinds only (BACKENDS above),
// and only the frame kind lacks one (app.ts's TodoApp.runner).
const g = app.runner!.generation;
await app.runner!.settle();
await sleep(0);
if (app.runner.generation === g) return;
if (app.runner!.generation === g) return;
}
}

Expand Down Expand Up @@ -145,12 +147,12 @@ async function runTraps(kind: BackendKind, host: HTMLElement) {
const lab = await startLab(kind, container);
let outcome: string;
try {
await lab.runner.call(() => lab.exports.probe(id));
await lab.runner!.call(() => lab.exports.probe(id));
outcome = "ok";
} catch (e) {
outcome = normalizeError(e);
}
await lab.runner.settle();
await lab.runner!.settle();
traps.push(outcome);
if (id === 0 || id === 7) probeDom.push(snapshot(container));
container.remove();
Expand Down
9 changes: 8 additions & 1 deletion visor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ Three layers, one trust story:
allows. `frame.ts` is the code that runs there: the applier wired to
a MessagePort, height reporting, coarse theme (never the anchor
colour). The queued-op protocol is identical to `channel`; only the
realm changes.
realm changes. `mount.ts` is the layer's **app-mount seam**:
`mountApp()` stands a component up on one of these frames — frame,
surface, imports, the serialized guest-call chain — and hands back
only what a visor needs (the exports, the route, input suspension,
teardown). Every embedder mounts through it, so the placement change
#142 rules for (the wasm instance moving INSIDE the frame, surface
imports bound to its real DOM, everything else proxied over a port)
is a change of this file's internals and of nothing above it.

- **`ui/`** — the system UI core. `initVisor()` renders the strip
(two-line context, identity cluster), announcements
Expand Down
Loading
Loading