Fork Horse repository, listed by its owner. Publisher snapshot.

packages/forkhorse-gatekeeper/app/main.ts

Snapshot at 34e3175

Containing directory

3,663 bytes · typescriptView raw
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