2026-07-30 09:21:55 +08:00
|
|
|
import { createSignal, onCleanup, onMount } from "solid-js";
|
2026-07-30 15:16:02 +08:00
|
|
|
import type { View } from "../pages/registry";
|
|
|
|
|
import { createPageLifecycleDispatcher, type PageLifecycleEventType } from "../sdk/lifecycle";
|
2026-07-30 09:21:55 +08:00
|
|
|
import { isHostMessage, type HostSharedState, type PageCommand } from "../sdk/protocol";
|
|
|
|
|
import { notifyReady, sendCommand } from "../sdk/page";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Connects one iframe document to the persistent Host Shell.
|
|
|
|
|
*
|
2026-07-30 09:43:08 +08:00
|
|
|
* A keep-alive document remains mounted while inactive and exposes that state
|
2026-07-30 15:16:02 +08:00
|
|
|
* 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.
|
2026-07-30 09:21:55 +08:00
|
|
|
*/
|
|
|
|
|
export function createFrameBridge(view: View) {
|
|
|
|
|
const [state, setState] = createSignal<HostSharedState | null>(null);
|
2026-07-30 09:43:08 +08:00
|
|
|
const [active, setActive] = createSignal(false);
|
2026-07-30 15:16:02 +08:00
|
|
|
const pageLifecycle = createPageLifecycleDispatcher();
|
2026-07-30 09:21:55 +08:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
2026-07-30 09:43:08 +08:00
|
|
|
if (event.data.type === "lifecycle") {
|
|
|
|
|
const phase = event.data.phase;
|
2026-07-30 15:16:02 +08:00
|
|
|
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.
|
2026-07-30 09:43:08 +08:00
|
|
|
window.dispatchEvent(
|
|
|
|
|
new CustomEvent("wasmeld:lifecycle", {
|
|
|
|
|
detail: { phase },
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
window.dispatchEvent(new Event(`wasmeld:${phase}`));
|
2026-07-30 09:21:55 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
window.addEventListener("message", receive);
|
2026-07-30 15:16:02 +08:00
|
|
|
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);
|
2026-07-30 09:21:55 +08:00
|
|
|
notifyReady(view);
|
2026-07-30 15:16:02 +08:00
|
|
|
onCleanup(() => window.removeEventListener("pagehide", unload));
|
2026-07-30 09:21:55 +08:00
|
|
|
});
|
|
|
|
|
|
2026-07-30 15:16:02 +08:00
|
|
|
onCleanup(() => {
|
|
|
|
|
window.removeEventListener("message", receive);
|
|
|
|
|
pageLifecycle.dispatch("unload", "document");
|
|
|
|
|
});
|
2026-07-30 09:21:55 +08:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
state,
|
2026-07-30 09:43:08 +08:00
|
|
|
active,
|
2026-07-30 15:16:02 +08:00
|
|
|
lifecycle: pageLifecycle.lifecycle,
|
2026-07-30 09:21:55 +08:00
|
|
|
command(command: PageCommand) {
|
|
|
|
|
sendCommand(view, command);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2026-07-30 15:16:02 +08:00
|
|
|
|
|
|
|
|
function eventForPhase(phase: "activate" | "deactivate" | "dispose"): PageLifecycleEventType {
|
|
|
|
|
switch (phase) {
|
|
|
|
|
case "activate":
|
|
|
|
|
return "show";
|
|
|
|
|
case "deactivate":
|
|
|
|
|
return "hide";
|
|
|
|
|
case "dispose":
|
|
|
|
|
return "unload";
|
|
|
|
|
}
|
|
|
|
|
}
|