farmcontrol-ui/src/electrobun-bridge.js
Tom Butcher 578caaffc2 Refactor HProgress component and CSS for improved animation and layout
- Updated the HProgress component to include a mask and wave animation for a more visually appealing progress display.
- Adjusted CSS styles in App.css to enhance the animation responsiveness and ensure seamless transitions.
- Removed unnecessary postMessage calls in electrobun-bridge.js to streamline message handling.
- Cleaned up rpc.js by removing the rendererResponseAck handler for better code clarity.
2026-08-09 15:10:35 +01:00

260 lines
6.5 KiB
JavaScript

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(
typeof window !== 'undefined' &&
window.__electrobunWebviewId &&
window.__electrobunRpcSocketPort
)
}
export function isElectrobunBridgeReady() {
return initialized && Boolean(rpc?.request)
}
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)
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)) {
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)
}
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()
if (!rpc?.request) {
console.warn(`Electrobun RPC unavailable for request: ${method}`)
return null
}
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)
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 }),
checkAppUpdateResult: () => invokeRequest('checkAppUpdateResult'),
checkDuplicateInstallations: () =>
invokeRequest('checkDuplicateInstallations'),
removeDuplicateInstallations: () =>
invokeRequest('removeDuplicateInstallations'),
resizeSpotlightWindow: (height) =>
invokeRequest('resizeSpotlightWindow', { height }),
setSidebarViewMenu: (sections) =>
invokeRequest('setSidebarViewMenu', { sections }),
getAppVersion: () => invokeRequest('getAppVersion'),
getAppEngine: () => invokeRequest('getAppEngine')
}
if (typeof window !== 'undefined') {
window.electronAPI = electronAPI
}
export default electronAPI