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(null); const [active, setActive] = createSignal(false); const pageLifecycle = createPageLifecycleDispatcher(); const receive = (event: MessageEvent) => { 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"; } }