const listeners = new Map() const pendingMessages = new Map() let rpc = null let initPromise = null let initialized = false export function isElectrobunDesktop() { return Boolean( typeof window !== 'undefined' && window.__electrobunWebviewId && window.__electrobunRpcSocketPort ) } function dispatchMessage(channel, data) { const channelListeners = listeners.get(channel) if (!channelListeners?.size) { if (!pendingMessages.has(channel)) { pendingMessages.set(channel, []) } pendingMessages.get(channel).push(data) return } for (const callback of channelListeners) { callback(data) } } // Bun delivers push messages via executeJavascript for immediate handling. // WebSocket RPC messages can be deferred by WKWebView during native window // transitions (e.g. fullscreen) until the next user interaction. if (typeof window !== 'undefined') { window.__farmcontrolDispatchRpcMessage = dispatchMessage } async function setupRpc() { // electrobun/view captures webview globals at import time — import only after // Electrobun preload has set them (Vite dev can evaluate modules earlier). const { default: Electrobun, Electroview } = await import('electrobun/view') rpc = Electroview.defineRPC({ maxRequestTime: 30000, handlers: { requests: {}, messages: { '*': (channel, data) => { dispatchMessage(channel, data) } } } }) new Electrobun.Electroview({ rpc }) } function shouldWaitForElectrobun() { if (isElectrobunDesktop()) { return true } if (typeof window === 'undefined') { return false } if (window.__electrobunWebviewId) { return true } const { protocol, hostname, port } = window.location if (protocol === 'views:') { return true } // Vite dev server used by electrobun dev:app if (hostname === 'localhost' && port === '5780') { return true } return Boolean( window.__electrobun || window.__electrobunEventBridge || window.__electrobunInternalBridge ) } export async function initElectrobunBridge() { if (initialized) { return } if (!initPromise) { initPromise = (async () => { if (!shouldWaitForElectrobun()) { return } const deadline = Date.now() + 3000 while (Date.now() < deadline) { if (isElectrobunDesktop()) { await setupRpc() window.electronAPI = electronAPI initialized = true return } await new Promise((resolve) => setTimeout(resolve, 50)) } console.warn( 'Electrobun bridge: webview globals not found; desktop RPC unavailable.' ) })() } await initPromise } function onMessage(channel, callback) { void initElectrobunBridge() if (!listeners.has(channel)) { listeners.set(channel, new Set()) } listeners.get(channel).add(callback) const queued = pendingMessages.get(channel) if (queued?.length) { pendingMessages.delete(channel) for (const payload of queued) { callback(payload) } } return () => { listeners.get(channel)?.delete(callback) } } function removeAllListeners(channel) { listeners.delete(channel) pendingMessages.delete(channel) } async function invokeRequest(method, params = {}) { await initElectrobunBridge() if (!rpc?.request) { console.warn(`Electrobun RPC unavailable for request: ${method}`) return null } try { return await rpc.request[method](params) } catch (error) { console.warn(`Electrobun RPC request failed: ${method}`, error) return null } } const electronAPI = { get isDesktop() { return isElectrobunDesktop() }, onMessage, removeAllListeners, getOsInfo: () => invokeRequest('getOsInfo'), getWindowState: () => invokeRequest('getWindowState'), windowControl: (action) => invokeRequest('windowControl', { action }), openExternalUrl: (url) => invokeRequest('openExternalUrl', { url }), openInternalUrl: (url) => invokeRequest('openInternalUrl', { url }), getAuthSession: () => invokeRequest('getAuthSession'), setAuthSession: (session) => invokeRequest('setAuthSession', { session }), clearAuthSession: () => invokeRequest('clearAuthSession'), getAppSettings: () => invokeRequest('getAppSettings'), setAppSettings: (settings) => invokeRequest('setAppSettings', { settings }), startAppUpdate: (update) => invokeRequest('startAppUpdate', { update }), resizeSpotlightWindow: (height) => invokeRequest('resizeSpotlightWindow', { height }), setSidebarViewMenu: (sections) => invokeRequest('setSidebarViewMenu', { sections }), getAppVersion: () => invokeRequest('getAppVersion') } if (typeof window !== 'undefined') { window.electronAPI = electronAPI } export default electronAPI