diff --git a/src/desktop/rpc.js b/src/desktop/rpc.js index 0b6bbdf..cb3deef 100644 --- a/src/desktop/rpc.js +++ b/src/desktop/rpc.js @@ -23,85 +23,117 @@ import { } from './window.js' export function createAppRpc() { + const requestHandlers = { + getOsInfo: async () => ({ + platform: process.platform + }), + getWindowState: async () => getWindowState(), + windowControl: async ({ action }) => { + handleWindowControl(action) + return { ok: true } + }, + openExternalUrl: async ({ url }) => { + openExternalUrl(url) + return { ok: true } + }, + openInternalUrl: async ({ url }) => ({ + ok: openInternalUrl(url) + }), + getAuthSession: async () => getAuthSession(), + setAuthSession: async ({ session }) => ({ + ok: await setAuthSession(session) + }), + clearAuthSession: async () => ({ + ok: await clearAuthSession() + }), + getAppSettings: async () => getAppSettings(), + setAppSettings: async ({ settings }) => ({ + ok: await setAppSettings(settings) + }), + startAppUpdate: async ({ update }) => { + const mainWindow = getMainWindow() + const sendProgress = (payload) => { + sendToRenderer('appUpdateProgress', { + timestamp: new Date().toISOString(), + ...payload + }) + } + + // Updates can take several minutes; return immediately and report + // progress via appUpdateProgress messages instead of blocking RPC. + void startAppUpdate(mainWindow, update, sendProgress).catch((error) => { + console.error('App update failed:', error) + }) + return { ok: true } + }, + resizeSpotlightWindow: async ({ height }) => ({ + ok: resizeSpotlightWindow(height) + }), + setSidebarViewMenu: async ({ sections }) => ({ + ok: setSidebarViewMenu(sections) + }), + checkAppUpdateResult: async () => + checkForCompletedUpdate(getMainWindow()), + checkDuplicateInstallations: async () => checkForDuplicateInstallations(), + removeDuplicateInstallations: async () => { + // Removal waits on an admin/UAC prompt which can outlive the RPC + // timeout; report the outcome via a push message instead. + void removeDuplicateInstallations() + .catch((error) => ({ + ok: false, + error: + error?.message || 'Failed to remove the duplicate installation.' + })) + .then((result) => { + sendToRenderer('duplicateInstallationsRemoved', result) + }) + return { ok: true } + }, + getAppVersion: async () => process.env.ELECTROBUN_VERSION || 'desktop', + getAppEngine: async () => { + const mainWindow = getMainWindow() + const renderer = mainWindow?.renderer || 'native' + return renderer === 'cef' ? 'chromium' : 'native' + } + } + return BrowserView.defineRPC({ maxRequestTime: 30000, handlers: { - requests: { - getOsInfo: async () => ({ - platform: process.platform - }), - getWindowState: async () => getWindowState(), - windowControl: async ({ action }) => { - handleWindowControl(action) - return { ok: true } + requests: requestHandlers, + messages: { + rendererResponseAck: ({ id }) => { + console.log('[rpc-diagnostic] renderer received response', id) }, - openExternalUrl: async ({ url }) => { - openExternalUrl(url) - console.log('openExternalUrl', url) - return { ok: true } - }, - openInternalUrl: async ({ url }) => ({ - ok: openInternalUrl(url) - }), - getAuthSession: async () => getAuthSession(), - setAuthSession: async ({ session }) => ({ - ok: await setAuthSession(session) - }), - clearAuthSession: async () => ({ - ok: await clearAuthSession() - }), - getAppSettings: async () => getAppSettings(), - setAppSettings: async ({ settings }) => ({ - ok: await setAppSettings(settings) - }), - startAppUpdate: async ({ update }) => { - const mainWindow = getMainWindow() - const sendProgress = (payload) => { - sendToRenderer('appUpdateProgress', { - timestamp: new Date().toISOString(), - ...payload + rendererRequest: ({ id, method, params }) => { + const handler = requestHandlers[method] + if (!handler) { + sendToRenderer('rpcResponse', { + id, + success: false, + error: `Unknown desktop RPC method: ${String(method)}` }) + return } - // Updates can take several minutes; return immediately and report - // progress via appUpdateProgress messages instead of blocking RPC. - void startAppUpdate(mainWindow, update, sendProgress).catch((error) => { - console.error('App update failed:', error) - }) - return { ok: true } - }, - resizeSpotlightWindow: async ({ height }) => ({ - ok: resizeSpotlightWindow(height) - }), - setSidebarViewMenu: async ({ sections }) => ({ - ok: setSidebarViewMenu(sections) - }), - checkAppUpdateResult: async () => - checkForCompletedUpdate(getMainWindow()), - checkDuplicateInstallations: async () => - checkForDuplicateInstallations(), - removeDuplicateInstallations: async () => { - // Removal waits on an admin/UAC prompt which can outlive the RPC - // timeout; report the outcome via a push message instead. - void removeDuplicateInstallations() - .catch((error) => ({ - ok: false, - error: - error?.message || 'Failed to remove the duplicate installation.' - })) + void Promise.resolve() + .then(() => handler(params || {})) .then((result) => { - sendToRenderer('duplicateInstallationsRemoved', result) + sendToRenderer('rpcResponse', { + id, + success: true, + result + }) + }) + .catch((error) => { + sendToRenderer('rpcResponse', { + id, + success: false, + error: error?.message || String(error) + }) }) - return { ok: true } - }, - getAppVersion: async () => process.env.ELECTROBUN_VERSION || 'desktop', - getAppEngine: async () => { - const mainWindow = getMainWindow() - const renderer = mainWindow?.renderer || 'native' - return renderer === 'cef' ? 'chromium' : 'native' } - }, - messages: {} + } } }) } diff --git a/src/electrobun-bridge.js b/src/electrobun-bridge.js index ee5037c..4221a9e 100644 --- a/src/electrobun-bridge.js +++ b/src/electrobun-bridge.js @@ -1,8 +1,12 @@ const listeners = new Map() const pendingMessages = new Map() +const pendingRequests = new Map() +const RPC_REQUEST_TIMEOUT_MS = 30000 +const RPC_RESPONSE_CHANNEL = 'rpcResponse' let rpc = null let initPromise = null let initialized = false +let nextRequestId = 0 export function isElectrobunDesktop() { return Boolean( @@ -17,6 +21,30 @@ export function isElectrobunBridgeReady() { } function dispatchMessage(channel, data) { + if (channel === RPC_RESPONSE_CHANNEL) { + const pendingRequest = pendingRequests.get(data?.id) + if (!pendingRequest) return + + pendingRequests.delete(data.id) + clearTimeout(pendingRequest.timeout) + window.__electrobunBunBridge?.postMessage( + JSON.stringify({ + type: 'message', + id: 'rendererResponseAck', + payload: { id: data.id } + }) + ) + + if (data.success) { + pendingRequest.resolve(data.result) + } else { + pendingRequest.reject( + new Error(data.error || 'Desktop RPC request failed.') + ) + } + return + } + const channelListeners = listeners.get(channel) if (!channelListeners?.size) { if (!pendingMessages.has(channel)) { @@ -148,6 +176,38 @@ function removeAllListeners(channel) { pendingMessages.delete(channel) } +function invokeNativeRequest(method, params) { + const nativeBridge = window.__electrobunBunBridge + if (!nativeBridge?.postMessage) { + return null + } + + const id = `${Date.now()}-${++nextRequestId}` + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + pendingRequests.delete(id) + reject(new Error(`Desktop RPC request timed out: ${method}`)) + }, RPC_REQUEST_TIMEOUT_MS) + + pendingRequests.set(id, { resolve, reject, timeout }) + + try { + nativeBridge.postMessage( + JSON.stringify({ + type: 'message', + id: 'rendererRequest', + payload: { id, method, params } + }) + ) + } catch (error) { + clearTimeout(timeout) + pendingRequests.delete(id) + reject(error) + } + }) +} + async function invokeRequest(method, params = {}) { await initElectrobunBridge() @@ -157,6 +217,11 @@ async function invokeRequest(method, params = {}) { } try { + const nativeRequest = invokeNativeRequest(method, params) + if (nativeRequest) { + return await nativeRequest + } + return await rpc.request[method](params) } catch (error) { console.warn(`Electrobun RPC request failed: ${method}`, error)