Files
wasmeld/crates/wasmeld-console/web/src/primitives/create-frame-bridge.ts
T
Maofeng 9ff3223c44 refactor(console-web): centralize page lifecycle and UI composition
Declare navigation, page metadata, lazy loaders, and keep-alive policy in one registry. Retain primary iframe pages until a continuous inactive TTL expires and expose typed show, hide, and unload events through the Page SDK.

Move business-aware components out of the UI primitive directory, migrate views and Host dialogs to variant primitives and local Tailwind classes, and remove the global component style layer. Keep native dialogs inside the overlay flex context so they remain centered.

Cover iframe expiration, lifecycle dispatch, UI dependency direction, and global style ownership with focused tests.
2026-07-30 15:16:02 +08:00

81 lines
2.9 KiB
TypeScript

import { createSignal, onCleanup, onMount } from "solid-js";
import type { View } from "../pages/registry";
import { createPageLifecycleDispatcher, type PageLifecycleEventType } from "../sdk/lifecycle";
import { isHostMessage, type HostSharedState, type PageCommand } from "../sdk/protocol";
import { notifyReady, sendCommand } from "../sdk/page";
/**
* Connects one iframe document to the persistent Host Shell.
*
* A keep-alive document remains mounted while inactive and exposes that state
* through `active`. Host `dispose`, browser `pagehide` and Solid cleanup all
* converge on one deduplicated SDK unload transition. The document boundary
* itself remains the final guarantee that an evicted Page releases its realm.
*/
export function createFrameBridge(view: View) {
const [state, setState] = createSignal<HostSharedState | null>(null);
const [active, setActive] = createSignal(false);
const pageLifecycle = createPageLifecycleDispatcher();
const receive = (event: MessageEvent<unknown>) => {
if (event.origin !== window.location.origin || event.source !== window.parent) return;
if (!isHostMessage(event.data)) return;
if (event.data.type === "state" && event.data.state.view === view) {
setState(event.data.state);
}
if (event.data.type === "lifecycle") {
const phase = event.data.phase;
const lifecycleEvent = pageLifecycle.dispatch(eventForPhase(phase), "host");
if (!lifecycleEvent) return;
setActive(lifecycleEvent.current === "visible");
// Preserve the original DOM events for existing pages. New code should
// use the typed SDK lifecycle returned by this bridge.
window.dispatchEvent(
new CustomEvent("wasmeld:lifecycle", {
detail: { phase },
}),
);
window.dispatchEvent(new Event(`wasmeld:${phase}`));
}
};
onMount(() => {
window.addEventListener("message", receive);
const unload = (event: PageTransitionEvent) => {
// A persisted document is frozen in the back-forward cache, not unloaded.
// Its Host and iframe will resume together when the browser restores it.
if (event.persisted) return;
if (pageLifecycle.dispatch("unload", "document")) setActive(false);
};
window.addEventListener("pagehide", unload);
notifyReady(view);
onCleanup(() => window.removeEventListener("pagehide", unload));
});
onCleanup(() => {
window.removeEventListener("message", receive);
pageLifecycle.dispatch("unload", "document");
});
return {
state,
active,
lifecycle: pageLifecycle.lifecycle,
command(command: PageCommand) {
sendCommand(view, command);
},
};
}
function eventForPhase(phase: "activate" | "deactivate" | "dispose"): PageLifecycleEventType {
switch (phase) {
case "activate":
return "show";
case "deactivate":
return "hide";
case "dispose":
return "unload";
}
}