import { HOST_BRIDGE_PROTOCOL, HOST_BRIDGE_VERSION, type HostBridgeError, type HostBridgeMethod, type HostBridgeRequest, type HostBridgeResponse, } from '../../../packages/shared/src/contracts/hostBridge'; const DEFAULT_NATIVE_APP_BRIDGE_TIMEOUT_MS = 8000; const MAX_NATIVE_APP_BRIDGE_TIMEOUT_MS = 30000; type NativeAppBridgeWindow = Window & { ReactNativeWebView?: { postMessage?: (message: string) => void; }; __TAURI__?: { core?: { invoke?: ( command: string, args?: Record, ) => Promise; }; }; }; type PendingNativeRequest = { resolve: (value: unknown) => void; reject: (error: Error) => void; timeoutId: ReturnType; }; const pendingNativeRequests = new Map(); let nativeBridgeListenerInstalled = false; let nextNativeRequestSequence = 0; function resolveNativeWindow() { return typeof window === 'undefined' ? null : (window as NativeAppBridgeWindow); } function buildNativeRequestId(method: HostBridgeMethod) { nextNativeRequestSequence += 1; return `host_${method.replace(/[^a-z0-9]/gi, '_')}_${Date.now()}_${nextNativeRequestSequence}`; } function resolveTimeoutMs(timeoutMs: number | undefined) { if (!Number.isFinite(timeoutMs ?? Number.NaN) || !timeoutMs) { return DEFAULT_NATIVE_APP_BRIDGE_TIMEOUT_MS; } return Math.min( Math.max(1, Math.trunc(timeoutMs)), MAX_NATIVE_APP_BRIDGE_TIMEOUT_MS, ); } function createHostBridgeError(error: HostBridgeError) { const result = new Error(error.message); result.name = error.code; return result; } function isHostBridgeResponse(value: unknown): value is HostBridgeResponse { if (!value || typeof value !== 'object') { return false; } const candidate = value as Partial; return ( candidate.bridge === HOST_BRIDGE_PROTOCOL && candidate.version === HOST_BRIDGE_VERSION && typeof candidate.id === 'string' && typeof candidate.ok === 'boolean' ); } function parseNativeMessage(data: unknown) { if (typeof data === 'string') { try { return JSON.parse(data) as unknown; } catch { return null; } } return data; } function settleNativeResponse(response: HostBridgeResponse) { const pending = pendingNativeRequests.get(response.id); if (!pending) { return; } pendingNativeRequests.delete(response.id); clearTimeout(pending.timeoutId); if (response.ok) { pending.resolve(response.result); return; } pending.reject(createHostBridgeError(response.error)); } function ensureNativeBridgeListener() { const nativeWindow = resolveNativeWindow(); if (!nativeWindow || nativeBridgeListenerInstalled) { return; } nativeWindow.addEventListener('message', (event) => { const value = parseNativeMessage(event.data); if (isHostBridgeResponse(value)) { settleNativeResponse(value); } }); nativeBridgeListenerInstalled = true; } export function canUseReactNativeHostBridge() { return ( typeof resolveNativeWindow()?.ReactNativeWebView?.postMessage === 'function' ); } export function canUseTauriHostBridge() { return ( typeof resolveNativeWindow()?.__TAURI__?.core?.invoke === 'function' ); } export function canUseNativeAppHostBridge() { return canUseReactNativeHostBridge() || canUseTauriHostBridge(); } export async function requestNativeAppHostBridge( method: HostBridgeMethod, payload?: unknown, options: { timeoutMs?: number; } = {}, ) { const nativeWindow = resolveNativeWindow(); if (!nativeWindow) { return null; } const timeoutMs = resolveTimeoutMs(options.timeoutMs); const request: HostBridgeRequest = { bridge: HOST_BRIDGE_PROTOCOL, version: HOST_BRIDGE_VERSION, id: buildNativeRequestId(method), method, timeoutMs, ...(payload === undefined ? {} : { payload }), }; const tauriInvoke = nativeWindow.__TAURI__?.core?.invoke; if (typeof tauriInvoke === 'function') { const response = await tauriInvoke>( 'host_bridge_request', { request }, ); if (!isHostBridgeResponse(response)) { throw new Error('host_bridge_invalid_response'); } if (response.ok) { return response.result as Result; } throw createHostBridgeError(response.error); } const postMessage = nativeWindow.ReactNativeWebView?.postMessage; if (typeof postMessage !== 'function') { return null; } ensureNativeBridgeListener(); return await new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { pendingNativeRequests.delete(request.id); reject(createHostBridgeError({ code: 'timeout', message: 'host_bridge_timeout', })); }, timeoutMs); pendingNativeRequests.set(request.id, { resolve: (value) => resolve(value as Result), reject, timeoutId, }); try { postMessage(JSON.stringify(request)); } catch (error) { pendingNativeRequests.delete(request.id); clearTimeout(timeoutId); reject(error instanceof Error ? error : new Error(String(error))); } }); } export function resetNativeAppHostBridgeForTest() { for (const pending of pendingNativeRequests.values()) { clearTimeout(pending.timeoutId); } pendingNativeRequests.clear(); nextNativeRequestSequence = 0; }