"use client"; import type { DesktopPreviewTabState, PreviewReportStatusInput, ScopedThreadRef, ThreadId, } from "@t3tools/contracts"; import { useEffect, useRef } from "react"; import { useBrowserPointerStore } from "~/browser/browserPointerStore"; import { applyPreviewDesktopState, type DesktopPreviewOverlay } from "~/previewStateStore"; import { previewEnvironment } from "~/state/preview"; import { useAtomCommand } from "~/state/use-atom-command"; import { previewBridge } from "./previewBridge"; /** * Mirrors low-latency desktop state into the store and reflects navigation * events back to the server. Webview lifetime is owned by ElectronBrowserHost. */ export function usePreviewBridge(input: { threadRef: ScopedThreadRef; tabId: string }): void { const { threadRef, tabId } = input; const clearBrowserPointer = useBrowserPointerStore((state) => state.clear); const reportStatus = useAtomCommand(previewEnvironment.reportStatus, "preview status report"); const bridge = previewBridge; // One bridge subscription does both jobs (mirror state + forward to // server) so the desktop bridge keeps a single listener entry per tab. const lastReportedUrl = useRef(null); const lastReportedKind = useRef(null); const lastDesktopNavStatus = useRef(null); useEffect(() => { if (!bridge || typeof window === "undefined") return; lastReportedUrl.current = null; lastReportedKind.current = null; lastDesktopNavStatus.current = null; const unsubscribe = bridge.onStateChange((changedTabId, state) => { if (changedTabId !== tabId) return; if (shouldClearBrowserPointer(lastDesktopNavStatus.current, state.navStatus)) { clearBrowserPointer(tabId); } lastDesktopNavStatus.current = state.navStatus; applyPreviewDesktopState(threadRef, tabId, projectDesktopState(state)); const reported = buildReportInput({ threadId: threadRef.threadId, tabId, state, lastReportedUrl: lastReportedUrl.current, lastReportedKind: lastReportedKind.current, }); if (!reported) return; lastReportedUrl.current = reported.lastReportedUrl; lastReportedKind.current = reported.lastReportedKind; void reportStatus({ environmentId: threadRef.environmentId, input: reported.input, }); }); return unsubscribe; }, [bridge, clearBrowserPointer, reportStatus, tabId, threadRef]); } function shouldClearBrowserPointer( previous: DesktopPreviewTabState["navStatus"] | null, current: DesktopPreviewTabState["navStatus"], ): boolean { if (!previous) return false; if (current.kind === "Loading" && previous.kind !== "Loading") return true; if (current.kind === "Idle" || previous.kind === "Idle") return false; return current.url !== previous.url; } function projectDesktopState(state: DesktopPreviewTabState): DesktopPreviewOverlay { return { canGoBack: state.canGoBack, canGoForward: state.canGoForward, loading: state.navStatus.kind === "Loading", zoomFactor: state.zoomFactor, colorScheme: state.colorScheme, controller: state.controller, }; } /** * Decide whether a state change warrants an RPC to the server, and shape * the report payload. * * - Idle never reports — the tab is post-close or pre-load and the server * already knows the canonical state from `open` / `closed`. * - We dedupe on (kind, url): consecutive Loading→Loading→Loading for the * same URL collapses to a single RPC, ditto Success. * - LoadFailed always reports (the server uses it to emit `failed`). */ function buildReportInput(args: { readonly threadId: ThreadId; readonly tabId: string; readonly state: DesktopPreviewTabState; readonly lastReportedUrl: string | null; readonly lastReportedKind: DesktopPreviewTabState["navStatus"]["kind"] | null; }): { readonly input: PreviewReportStatusInput; readonly lastReportedUrl: string; readonly lastReportedKind: DesktopPreviewTabState["navStatus"]["kind"]; } | null { const { threadId, tabId, state, lastReportedUrl, lastReportedKind } = args; const status = state.navStatus; if (status.kind === "Idle") return null; // Skip if we've already reported the same kind+url. LoadFailed always // reports (rapid duplicate failures are unusual and worth surfacing). const sameAsLast = status.kind !== "LoadFailed" && status.kind === lastReportedKind && status.url === lastReportedUrl; if (sameAsLast) return null; const base = { threadId, tabId, canGoBack: state.canGoBack, canGoForward: state.canGoForward, }; if (status.kind === "LoadFailed") { return { input: { ...base, navStatus: { _tag: "LoadFailed", url: status.url, title: status.title, code: status.code, description: status.description, }, }, lastReportedUrl: status.url, lastReportedKind: "LoadFailed", }; } return { input: { ...base, navStatus: { _tag: status.kind, url: status.url, title: status.title }, }, lastReportedUrl: status.url, lastReportedKind: status.kind, }; }