Fork Horse repository, listed by its owner. Publisher snapshot.
packages/forkhorse-gatekeeper/app/main.ts
Snapshot at 34e3175
1import { newMessagePortRpcSession, RpcTarget } from "capnweb";
2import type { SidebarUi } from "../src/ui-types.ts";
3import { connectSidebar } from "../src/client.ts";
4import { hostConnection } from "../src/host-connection.ts";
5import { mountSidebar } from "../src/mount.ts";
6import brand from "../../../brand.json";
7import "../../forkhorse-dashboard/src/styles.css";
8
9import type {
10 GatekeeperAppTheme,
11 GatekeeperAppThemeReceiver,
12} from "@gadgets/workshop-shared/theme";
13import { applyAppTheme, followSystemTheme } from "../../forkhorse-dashboard/src/theme.ts";
14
15class ThemeReceiver extends RpcTarget implements GatekeeperAppThemeReceiver {
16 setTheme(theme: GatekeeperAppTheme) {
17 if (closed) return;
18 stopSystem();
19 applyAppTheme(theme);
20 }
21}
22const stopSystem = followSystemTheme();
23
24interface Host extends RpcTarget {
25 subscribeTheme(receiver: GatekeeperAppThemeReceiver): Promise<GatekeeperAppTheme>;
26 readonly ui: SidebarUi;
27 openPrompt(prompt: string): Promise<void>;
28 setPresenting(active: boolean): Promise<{ rect: unknown; willResize: boolean }>;
29}
30
31const root = document.getElementById("sidebar")!;
32document.title = brand.name;
33const receiver = new ThemeReceiver();
34const connection = hostConnection(() => {
35 const { port1, port2 } = new MessageChannel();
36 const host = newMessagePortRpcSession<Host>(port1, receiver);
37 // Only renew after the previous host getter failed; never re-handshake a healthy host.
38 window.parent.postMessage({ type: "handshake" }, "*", [port2]);
39 // Restore even if the first project snapshot cannot be acquired after a frame reconnect.
40 void host.setPresenting(false).catch(() => {});
41 return {
42 host,
43 dispose() {
44 void host.setPresenting(false).catch(() => {});
45 host[Symbol.dispose]();
46 port1.close();
47 },
48 };
49});
50async function resubscribeTheme(isCurrent: () => boolean) {
51 // Every fresh UI capability starts in its normal pane, even when the host port survives.
52 resetPresentation();
53 if (!isCurrent()) return;
54 const theme = await connection.host.subscribeTheme(receiver);
55 if (isCurrent()) receiver.setTheme(theme);
56}
57let dispose = () => {};
58let resetPresentation = () => {};
59let closed = false;
60let retryTimer: ReturnType<typeof setTimeout> | undefined;
61let failures = 0;
62async function start() {
63 try {
64 const client = await connectSidebar(
65 await connection.acquireUi(),
66 () => {},
67 resubscribeTheme,
68 (isCurrent) => connection.acquireUi(isCurrent),
69 );
70 if (closed) {
71 client.dispose();
72 return;
73 }
74 const view = mountSidebar(root, {
75 brand,
76 project: client.project,
77 transport: client.transport,
78 fixture: root.dataset.fixture === "true",
79 coreOrigin: root.dataset.coreOrigin,
80 setPresenting: async (active) => connection.host.setPresenting(active),
81 openPrompt: async (prompt) => {
82 await connection.host.openPrompt(prompt);
83 },
84 });
85 resetPresentation = () => view.resetPresentation();
86 dispose = () => {
87 view.dispose();
88 client.dispose();
89 };
90 } catch {
91 if (closed) return;
92 // Keep the static content position visible until the first authenticated snapshot arrives.
93 const caption = root.querySelector("[role=status]");
94 if (caption) caption.textContent = "Live updates paused. Reconnecting…";
95 retryTimer = setTimeout(
96 () => void start(),
97 Math.min(30_000, 1000 * 2 ** Math.min(failures++, 5)),
98 );
99 }
100}
101void start();
102window.addEventListener(
103 "pagehide",
104 () => {
105 closed = true;
106 stopSystem();
107 clearTimeout(retryTimer);
108 dispose();
109 connection.dispose();
110 },
111 { once: true },
112);
113