Implement RPC request handling in Electrobun bridge and desktop RPC
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good
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:
parent
bac09b6543
commit
68aed21cc5
@ -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: {}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user