Implement RPC request handling in Electrobun bridge and desktop RPC
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- Introduced a new mechanism for handling RPC requests and responses in the Electrobun bridge, including timeout management for requests.
- Refactored the desktop RPC to utilize a centralized request handler, improving code organization and clarity.
- Added support for various RPC methods, enhancing communication between the renderer and the desktop environment.
This commit is contained in:
Tom Butcher 2026-08-09 13:23:03 +01:00
parent bac09b6543
commit 68aed21cc5
2 changed files with 166 additions and 69 deletions

View File

@ -23,85 +23,117 @@ import {
} from './window.js' } from './window.js'
export function createAppRpc() { 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({ return BrowserView.defineRPC({
maxRequestTime: 30000, maxRequestTime: 30000,
handlers: { handlers: {
requests: { requests: requestHandlers,
getOsInfo: async () => ({ messages: {
platform: process.platform rendererResponseAck: ({ id }) => {
}), console.log('[rpc-diagnostic] renderer received response', id)
getWindowState: async () => getWindowState(),
windowControl: async ({ action }) => {
handleWindowControl(action)
return { ok: true }
}, },
openExternalUrl: async ({ url }) => { rendererRequest: ({ id, method, params }) => {
openExternalUrl(url) const handler = requestHandlers[method]
console.log('openExternalUrl', url) if (!handler) {
return { ok: true } sendToRenderer('rpcResponse', {
}, id,
openInternalUrl: async ({ url }) => ({ success: false,
ok: openInternalUrl(url) error: `Unknown desktop RPC method: ${String(method)}`
}),
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
}) })
return
} }
// Updates can take several minutes; return immediately and report void Promise.resolve()
// progress via appUpdateProgress messages instead of blocking RPC. .then(() => handler(params || {}))
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) => { .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: {}
} }
}) })
} }

View File

@ -1,8 +1,12 @@
const listeners = new Map() const listeners = new Map()
const pendingMessages = 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 rpc = null
let initPromise = null let initPromise = null
let initialized = false let initialized = false
let nextRequestId = 0
export function isElectrobunDesktop() { export function isElectrobunDesktop() {
return Boolean( return Boolean(
@ -17,6 +21,30 @@ export function isElectrobunBridgeReady() {
} }
function dispatchMessage(channel, data) { 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) const channelListeners = listeners.get(channel)
if (!channelListeners?.size) { if (!channelListeners?.size) {
if (!pendingMessages.has(channel)) { if (!pendingMessages.has(channel)) {
@ -148,6 +176,38 @@ function removeAllListeners(channel) {
pendingMessages.delete(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 = {}) { async function invokeRequest(method, params = {}) {
await initElectrobunBridge() await initElectrobunBridge()
@ -157,6 +217,11 @@ async function invokeRequest(method, params = {}) {
} }
try { try {
const nativeRequest = invokeNativeRequest(method, params)
if (nativeRequest) {
return await nativeRequest
}
return await rpc.request[method](params) return await rpc.request[method](params)
} catch (error) { } catch (error) {
console.warn(`Electrobun RPC request failed: ${method}`, error) console.warn(`Electrobun RPC request failed: ${method}`, error)