import { Boxes, FileCode2, History, LayoutDashboard, Package, Search, Server, Settings, ShieldCheck, WifiOff, } from "lucide-solid"; import { batch, createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show, type Component, } from "solid-js"; import type { LucideProps } from "lucide-solid"; import { activateDeployment, changeRuntimeState, changeServiceState, fetchSnapshot, getApiBase, invokeComponent, publishWitPackage, registerComponent, saveApiBase, type BackendSnapshot, type InvokeResult, } from "../lib/api"; import { isView, serviceKey, type ConnectionState, type Service, type View } from "../lib/model"; import { servicesFrom } from "../lib/view-state"; import { pageUrl, sendLifecycle, sendState } from "../sdk/host"; import { isPageMessage, type HostGlobalState, type HostSharedState, type PageCommand, type RuntimeAction, } from "../sdk/protocol"; import { DialogLayer, type DialogState } from "./dialogs"; import { FrameCache, type FrameEntry } from "./frame-cache"; import { createBrowserHostTabSync, type HostTabSync } from "./tab-sync"; const NAVIGATION: Array<{ id: View; label: string; icon: Component; keepAlive?: boolean; }> = [ { id: "overview", label: "运行概览", icon: LayoutDashboard }, { id: "services", label: "服务版本", icon: Package, keepAlive: true }, { id: "instances", label: "运行实例", icon: Server }, { id: "wit-packages", label: "WIT 包", icon: FileCode2 }, { id: "activity", label: "调用记录", icon: History }, { id: "settings", label: "运行设置", icon: Settings, keepAlive: true }, ]; const MAX_KEEP_ALIVE_IFRAMES = 3; export default function HostApp() { // All backend ownership stays in this persistent document. Page iframes are // disposable renderers and can only request typed commands through the SDK. const initial = initialView(); const frameCache = new FrameCache(initial, { maxKeepAliveEntries: MAX_KEEP_ALIVE_IFRAMES, shouldKeepAlive: keepsFrameAlive, }); const [view, setView] = createSignal(initial); const [frames, setFrames] = createSignal(frameCache.snapshot()); const [readyViews, setReadyViews] = createSignal>(new Set()); const [snapshot, setSnapshot] = createSignal(null); const [connection, setConnection] = createSignal("connecting"); const [apiBase, setApiBase] = createSignal(getApiBase()); const [query, setQuery] = createSignal(""); const [runtimeAction, setRuntimeAction] = createSignal(null); const [serviceAction, setServiceAction] = createSignal(null); const [deploymentAction, setDeploymentAction] = createSignal(null); const [dialog, setDialog] = createSignal(null); const [dialogBusy, setDialogBusy] = createSignal(false); const [toast, setToast] = createSignal(null); const frameElements = new Map(); let tabSync: HostTabSync | null = null; let applyingSharedHostState = false; let toastTimer: number | undefined; let refreshGeneration = 0; const globalState = createMemo(() => ({ snapshot: snapshot(), connection: connection(), apiBase: apiBase(), runtimeAction: runtimeAction(), serviceAction: serviceAction(), deploymentAction: deploymentAction(), })); const state = createMemo(() => ({ ...globalState(), view: view(), query: query(), })); const activeServices = createMemo( () => servicesFrom(state()).filter((service) => service.status === "running").length, ); const frameReady = createMemo(() => readyViews().has(view())); const title = createMemo( () => NAVIGATION.find((item) => item.id === view())?.label ?? "运行概览", ); function stateFor(targetView: View): HostSharedState { return { ...state(), view: targetView }; } function applySharedHostState(next: HostGlobalState) { applyingSharedHostState = true; refreshGeneration += 1; batch(() => { setSnapshot(next.snapshot); setConnection(next.connection); setApiBase(next.apiBase); setRuntimeAction(next.runtimeAction); setServiceAction(next.serviceAction); setDeploymentAction(next.deploymentAction); }); applyingSharedHostState = false; } async function refresh() { const generation = ++refreshGeneration; try { const next = await fetchSnapshot(apiBase()); if (generation !== refreshGeneration) return; batch(() => { setSnapshot(next); setConnection("online"); }); } catch { if (generation !== refreshGeneration) return; batch(() => { setSnapshot(null); setConnection("offline"); }); } } function notify(message: string) { if (toastTimer !== undefined) window.clearTimeout(toastTimer); setToast(message); toastTimer = window.setTimeout(() => setToast(null), 2800); } function navigate(next: View, pushHistory = true) { if (next === view()) return; const previous = view(); const transition = frameCache.activate(next); const removedViews = new Set(transition.removed.map((entry) => entry.view)); // Retained frames are paused before being hidden. Transient and LRU-evicted // frames receive dispose before Solid removes their document from the DOM. if (!removedViews.has(previous)) sendFrameLifecycle(previous, "deactivate"); for (const entry of transition.removed) sendFrameLifecycle(entry.view, "dispose"); setReadyViews((current) => { const remaining = new Set(current); for (const removed of removedViews) remaining.delete(removed); return remaining; }); setFrames(transition.entries); setView(next); setQuery(""); if (readyViews().has(next)) { sendFrameState(next); sendFrameLifecycle(next, "activate"); } if (pushHistory) { const url = new URL(window.location.href); url.searchParams.set("view", next); window.history.pushState({ view: next }, "", url); } } function findService(key: string): Service | null { return servicesFrom(state()).find((service) => serviceKey(service) === key) ?? null; } async function performRuntimeAction(action: RuntimeAction) { setRuntimeAction(action); try { await changeRuntimeState(apiBase(), action); await refresh(); notify( action === "start" ? "Runtime 已启动" : action === "stop" ? "Runtime 已停止" : "Runtime 已重启", ); } catch (error) { notify(error instanceof Error ? error.message : "Runtime 操作失败"); } finally { setRuntimeAction(null); } } async function performServiceAction(key: string, action: "start" | "stop" | "restart") { const service = findService(key); if (!service) return; setServiceAction(key); try { await changeServiceState(apiBase(), service, action); await refresh(); notify( `${service.id} ${action === "stop" ? "已停止" : action === "restart" ? "已重启" : "已启动"}`, ); } catch (error) { notify(error instanceof Error ? error.message : "Actor 操作失败"); } finally { setServiceAction(null); } } async function handleCommand(command: PageCommand) { switch (command.type) { case "refresh": await refresh(); return; case "navigate": navigate(command.view); return; case "open-register": setDialog({ type: "register" }); return; case "open-wit-publish": setDialog({ type: "wit-publish" }); return; case "open-invoke": { const service = findService(command.serviceKey); if (service) setDialog({ type: "invoke", service }); return; } case "open-activate": { const service = findService(command.serviceKey); if (!service) return; const currentRevision = snapshot()?.deployments.find((item) => item.service_id === service.id)?.active_revision ?? null; setDialog({ type: "activate", service, currentRevision }); return; } case "runtime-action": await performRuntimeAction(command.action); return; case "service-action": await performServiceAction(command.serviceKey, command.action); return; case "save-api-base": try { const next = saveApiBase(command.value); setApiBase(next); setSnapshot(null); setConnection("connecting"); await refresh(); notify("API 地址已保存"); } catch (error) { notify(error instanceof Error ? error.message : "API 地址无效"); } } } const receive = (event: MessageEvent) => { if (event.origin !== window.location.origin || !isPageMessage(event.data)) return; const sourceView = viewForSource(event.source); if (!sourceView || event.data.view !== sourceView) return; if (event.data.type === "ready") { setReadyViews((current) => new Set(current).add(sourceView)); sendFrameState(sourceView); sendFrameLifecycle(sourceView, sourceView === view() ? "activate" : "deactivate"); return; } // Hidden documents cannot initiate management operations. This matters for // page-owned timers or libraries that may finish work after deactivation. if (sourceView !== view()) return; void handleCommand(event.data.command); }; function viewForSource(source: MessageEventSource | null): View | null { for (const [frameView, element] of frameElements) { if (source === element.contentWindow) return frameView; } return null; } function sendFrameState(targetView: View) { const target = frameElements.get(targetView)?.contentWindow; if (target) sendState(target, stateFor(targetView)); } function sendFrameLifecycle(targetView: View, phase: "activate" | "deactivate" | "dispose") { const target = frameElements.get(targetView)?.contentWindow; if (target) sendLifecycle(target, phase); } const popState = () => { const requested = new URLSearchParams(window.location.search).get("view"); navigate(isView(requested) ? requested : "overview", false); }; onMount(() => { window.addEventListener("message", receive); window.addEventListener("popstate", popState); tabSync = createBrowserHostTabSync({ getState: globalState, applyState: applySharedHostState, }); void refresh(); const interval = window.setInterval(() => void refresh(), 5000); onCleanup(() => window.clearInterval(interval)); }); onCleanup(() => { window.removeEventListener("message", receive); window.removeEventListener("popstate", popState); tabSync?.close(); if (toastTimer !== undefined) window.clearTimeout(toastTimer); for (const frameView of frameElements.keys()) sendFrameLifecycle(frameView, "dispose"); }); createEffect(() => { state(); const ready = readyViews(); for (const frame of frames()) { if (ready.has(frame.view)) sendFrameState(frame.view); } }); createEffect( on( globalState, (next) => { if (!applyingSharedHostState) tabSync?.publish(next); }, { defer: true }, ), ); async function register(file: File) { setDialogBusy(true); try { const registered = await registerComponent(apiBase(), file); await refresh(); setDialog(null); notify(`${registered.id}@${registered.revision} 已注册`); } finally { setDialogBusy(false); } } async function publishWit(file: File) { setDialogBusy(true); try { const published = await publishWitPackage(apiBase(), file); await refresh(); setDialog(null); notify(`${published.name}@${published.version} 已发布`); } finally { setDialogBusy(false); } } async function activate(service: Service) { setDialogBusy(true); setDeploymentAction(serviceKey(service)); try { await activateDeployment(apiBase(), service); await refresh(); setDialog(null); notify(`${service.id}@${service.revision} 已设为对外版本`); } finally { setDialogBusy(false); setDeploymentAction(null); } } async function invoke(service: Service, input: Uint8Array): Promise { const result = await invokeComponent(apiBase(), service, input); await refresh(); return result; } return (
{title()}
{(frame) => ( frameElements.set(frameView, element)} unregister={(frameView, element) => { if (frameElements.get(frameView) === element) frameElements.delete(frameView); }} /> )}
正在加载 {title()}
管理 API 不可用
{snapshot()?.runtime.status === "running" ? "Sandbox 正常" : "Runtime 已停止"} Component Model · WASI P2
{(activeDialog) => ( !dialogBusy() && setDialog(null)} onRegister={register} onPublishWit={publishWit} onActivate={activate} onInvoke={invoke} /> )} {(message) => (
{message()}
)}
); } function initialView(): View { const requested = new URLSearchParams(window.location.search).get("view"); return isView(requested) ? requested : "overview"; } function keepsFrameAlive(view: View): boolean { return NAVIGATION.some((item) => item.id === view && item.keepAlive === true); } function titleFor(view: View): string { return NAVIGATION.find((item) => item.id === view)?.label ?? view; } function PageFrame(props: { frame: FrameEntry; active: boolean; title: string; register: (view: View, element: HTMLIFrameElement) => void; unregister: (view: View, element: HTMLIFrameElement) => void; }) { let element: HTMLIFrameElement | undefined; onCleanup(() => { if (element) props.unregister(props.frame.view, element); }); return (