electrobun-rpc-patterns
Electrobun RPC Patterns
Advanced patterns for type-safe, performant RPC communication in Electrobun.
Type-Safe RPC
Shared Types Definition
Create a shared types file that both main and webview can import:
src/types/rpc.ts:
// Main process handlers (exposed to webview)
export interface MainRpcHandlers {
getUser(id: string): Promise<User>;
saveFile(path: string, content: string): Promise<SaveResult>;
listFiles(directory: string): Promise<FileInfo[]>;
executeCommand(cmd: string, args: string[]): Promise<CommandResult>;
}
// Webview handlers (exposed to main)
export interface WebviewRpcHandlers {
updateUI(data: UIUpdate): Promise<void>;
showNotification(message: string, type: NotificationType): Promise<void>;
refreshData(): Promise<void>;
}
export interface User {
id: string;
name: string;
email: string;
}
export interface SaveResult {
success: boolean;
path?: string;
error?: string;
}
export interface FileInfo {
name: string;
path: string;
size: number;
modified: Date;
}
export interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
}
export interface UIUpdate {
type: "data" | "status" | "error";
payload: any;
}
export type NotificationType = "info" | "success" | "warning" | "error";
Type-Safe Main Process
src/bun/main.ts:
import { BrowserWindow } from "electrobun/bun";
import type { MainRpcHandlers, WebviewRpcHandlers } from "../types/rpc";
const win = new BrowserWindow({ /* ... */ });
// Define handlers with type checking
win.defineRpc<MainRpcHandlers>({
handlers: {
async getUser(id: string) {
// Full type inference and checking
const user = await database.users.findById(id);
return {
id: user.id,
name: user.name,
email: user.email,
};
},
async saveFile(path: string, content: string) {
try {
await Bun.write(path, content);
return { success: true, path };
} catch (error) {
return { success: false, error: error.message };
}
},
async listFiles(directory: string) {
const entries = await fs.readdir(directory, { withFileTypes: true });
return entries.map(entry => ({
name: entry.name,
path: path.join(directory, entry.name),
size: entry.size,
modified: entry.mtime,
}));
},
async executeCommand(cmd: string, args: string[]) {
const proc = Bun.spawn([cmd, ...args]);
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
const exitCode = await proc.exited;
return { stdout, stderr, exitCode };
}
}
});
// Call webview methods with types
const webviewRpc = win.rpc as WebviewRpcHandlers;
await webviewRpc.updateUI({ type: "data", payload: data });
await webviewRpc.showNotification("File saved", "success");
Type-Safe Webview
src/views/mainview/index.ts:
import { Electroview } from "electrobun/browser";
import type { MainRpcHandlers, WebviewRpcHandlers } from "../../types/rpc";
const electroview = new Electroview();
// Type-safe RPC calls to main
const mainRpc = electroview.rpc as MainRpcHandlers;
async function loadUser(id: string) {
// Full IntelliSense and type checking
const user = await mainRpc.getUser(id);
console.log(user.name, user.email);
}
async function saveDocument(path: string, content: string) {
const result = await mainRpc.saveFile(path, content);
if (result.success) {
await mainRpc.showNotification("File saved successfully", "success");
} else {
console.error("Save failed:", result.error);
}
}
// Define webview handlers with types
electroview.defineRpc<WebviewRpcHandlers>({
handlers: {
async updateUI(data) {
if (data.type === "data") {
renderData(data.payload);
} else if (data.type === "error") {
showError(data.payload);
}
},
async showNotification(message, type) {
const notification = document.createElement("div");
notification.className = `notification ${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => notification.remove(), 3000);
},
async refreshData() {
const data = await mainRpc.getData();
renderData(data);
}
}
});
Error Handling
Robust Error Handling Pattern
// Custom error types
class RpcError extends Error {
constructor(
message: string,
public code: string,
public details?: any
) {
super(message);
this.name = "RpcError";
}
}
class ValidationError extends RpcError {
constructor(message: string, details?: any) {
super(message, "VALIDATION_ERROR", details);
}
}
class NotFoundError extends RpcError {
constructor(resource: string) {
super(`${resource} not found`, "NOT_FOUND");
}
}
// Main process with error handling
win.defineRpc({
handlers: {
async getUser(id: string) {
// Validate input
if (!id || typeof id !== "string") {
throw new ValidationError("Invalid user ID", { id });
}
const user = await database.users.findById(id);
if (!user) {
throw new NotFoundError("User");
}
return user;
},
async saveFile(path: string, content: string) {
try {
// Validate path
if (path.includes("..")) {
throw new ValidationError("Invalid file path");
}
await Bun.write(path, content);
return { success: true };
} catch (error) {
if (error instanceof RpcError) {
throw error;
}
// Wrap unknown errors
throw new RpcError(
"Failed to save file",
"SAVE_ERROR",
{ originalError: error.message }
);
}
}
}
});
// Webview with error handling
async function loadUser(id: string) {
try {
const user = await mainRpc.getUser(id);
displayUser(user);
} catch (error) {
if (error.code === "NOT_FOUND") {
showError("User not found");
} else if (error.code === "VALIDATION_ERROR") {
showError("Invalid input");
} else {
showError("Failed to load user");
console.error(error);
}
}
}
Retry Strategy
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries = 3,
delay = 1000
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) {
throw error;
}
console.warn(`Attempt ${attempt + 1} failed, retrying...`);
await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
}
}
throw new Error("Max retries exceeded");
}
// Usage in webview
const user = await withRetry(() => mainRpc.getUser(id));
Performance Optimization
Batching Requests
class BatchRpc {
private batch: Array<{
method: string;
args: any[];
resolve: (value: any) => void;
reject: (error: any) => void;
}> = [];
private batchTimeout: Timer | null = null;
private readonly batchDelay = 10; // ms
constructor(private rpc: any) {}
async call(method: string, ...args: any[]) {
return new Promise((resolve, reject) => {
this.batch.push({ method, args, resolve, reject });
if (!this.batchTimeout) {
this.batchTimeout = setTimeout(() => this.flush(), this.batchDelay);
}
});
}
private async flush() {
if (this.batch.length === 0) return;
const currentBatch = this.batch.splice(0);
this.batchTimeout = null;
try {
const results = await this.rpc.executeBatch(
currentBatch.map(({ method, args }) => ({ method, args }))
);
currentBatch.forEach((item, index) => {
if (results[index].error) {
item.reject(results[index].error);
} else {
item.resolve(results[index].result);
}
});
} catch (error) {
currentBatch.forEach(item => item.reject(error));
}
}
}
// Main process batch handler
win.defineRpc({
handlers: {
async executeBatch(requests: Array<{ method: string; args: any[] }>) {
return await Promise.all(
requests.map(async ({ method, args }) => {
try {
const result = await this.handlers[method](...args);
return { result };
} catch (error) {
return { error: error.message };
}
})
);
}
}
});
// Usage
const batchRpc = new BatchRpc(mainRpc);
const [user1, user2, user3] = await Promise.all([
batchRpc.call("getUser", "1"),
batchRpc.call("getUser", "2"),
batchRpc.call("getUser", "3"),
]);
Caching Layer
class CachedRpc {
private cache = new Map<string, { data: any; timestamp: number }>();
private readonly ttl = 60000; // 1 minute
constructor(private rpc: any) {}
async call(method: string, ...args: any[]) {
const cacheKey = `${method}:${JSON.stringify(args)}`;
const cached = this.cache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < this.ttl) {
return cached.data;
}
const result = await this.rpc[method](...args);
this.cache.set(cacheKey, { data: result, timestamp: Date.now() });
return result;
}
invalidate(method?: string) {
if (method) {
// Invalidate specific method
for (const [key] of this.cache) {
if (key.startsWith(method + ":")) {
this.cache.delete(key);
}
}
} else {
// Invalidate all
this.cache.clear();
}
}
}
// Usage
const cachedRpc = new CachedRpc(mainRpc);
const user = await cachedRpc.call("getUser", id); // Fetches
const user2 = await cachedRpc.call("getUser", id); // Cached
Streaming Data
Chunk-based Streaming
// Main process - stream large data in chunks
win.defineRpc({
handlers: {
async* streamFile(path: string, chunkSize = 1024 * 64) {
const file = Bun.file(path);
const reader = file.stream().getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield {
chunk: Array.from(value),
progress: (file.size - reader.closed) / file.size
};
}
},
async streamLogs(processId: string) {
const process = processes.get(processId);
const reader = process.stdout.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
yield { line: new TextDecoder().decode(value) };
}
}
}
});
// Webview - consume stream
for await (const chunk of mainRpc.streamFile("/large/file.dat")) {
processChunk(chunk.chunk);
updateProgress(chunk.progress);
}
Event-Based Updates
// Main process - emit events
class DataService {
private listeners = new Set<(data: any) => void>();
subscribe(callback: (data: any) => void) {
this.listeners.add(callback);
return () => this.listeners.delete(callback);
}
emit(data: any) {
this.listeners.forEach(cb => cb(data));
}
async watch(path: string) {
const watcher = fs.watch(path);
for await (const event of watcher) {
this.emit({ type: "file-change", path, event });
}
}
}
const dataService = new DataService();
win.defineRpc({
handlers: {
async subscribeToUpdates() {
const unsubscribe = dataService.subscribe(async (data) => {
await win.rpc.handleUpdate(data);
});
return { success: true };
}
}
});
// Webview - handle updates
electroview.defineRpc({
handlers: {
async handleUpdate(data: any) {
console.log("Received update:", data);
updateUI(data);
}
}
});
// Subscribe
await mainRpc.subscribeToUpdates();
Advanced Patterns
Request/Response Correlation
class CorrelatedRpc {
private pending = new Map<string, {
resolve: (value: any) => void;
reject: (error: any) => void;
timeout: Timer;
}>();
private requestId = 0;
constructor(private electroview: any) {
electroview.defineRpc({
handlers: {
handleResponse: async (id: string, result: any, error: any) => {
const pending = this.pending.get(id);
if (!pending) return;
clearTimeout(pending.timeout);
this.pending.delete(id);
if (error) {
pending.reject(error);
} else {
pending.resolve(result);
}
}
}
});
}
async call(method: string, ...args: any[]) {
const id = `req-${++this.requestId}`;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.pending.delete(id);
reject(new Error("Request timeout"));
}, 30000);
this.pending.set(id, { resolve, reject, timeout });
this.electroview.rpc.sendRequest(id, method, args);
});
}
}
Bi-directional Sync
class DataSync {
private localData: any = {};
private syncing = false;
constructor(
private mainRpc: any,
private electroview: any
) {
this.setupHandlers();
}
private setupHandlers() {
this.electroview.defineRpc({
handlers: {
syncFromMain: async (data: any) => {
if (!this.syncing) {
this.localData = data;
this.render();
}
}
}
});
}
async syncToMain() {
this.syncing = true;
try {
const serverData = await this.mainRpc.updateData(this.localData);
this.localData = serverData;
this.render();
} finally {
this.syncing = false;
}
}
async pull() {
const serverData = await this.mainRpc.getData();
this.localData = serverData;
this.render();
}
private render() {
// Update UI with localData
}
}
Resources
For more on Electrobun:
- Core skill:
electrobun- Basic RPC setup - Window management:
electrobun-window-management- Window-to-window RPC - Debugging:
electrobun-debugging- Debug RPC issues
More from rajavijayach/electrobun-skills
electrobun-window-management
Advanced window and view management patterns for Electrobun desktop applications. This skill covers multi-window architectures, BrowserView for embedded webviews, window lifecycle management, window orchestration, tab systems, and complex window hierarchies. Use this skill when building applications with multiple windows, implementing browser-like tab interfaces, managing parent-child window relationships, creating floating panels or toolbars, implementing picture-in-picture modes, managing window state persistence across sessions, or building applications that require sophisticated window coordination. Triggers include "multiple windows", "tab system", "BrowserView", "window orchestration", "floating window", "child window", "window state", "window manager", "multi-window app", or discussions about complex window management in Electrobun desktop applications.
63electrobun-native-ui
Native UI integration for Electrobun desktop applications including ApplicationMenu, ContextMenu, system Tray, native dialogs, keyboard shortcuts, and platform-specific UI patterns. This skill covers creating application menus with submenus and accelerators, context menus triggered by right-click, system tray icons with menus, file/folder dialogs, message boxes, notification systems, global keyboard shortcuts, menu item roles, dynamic menu updates, platform-specific menu conventions (macOS menu bar, Windows system menu), drag-and-drop integration, and native theming. Use when implementing application menus, adding system tray functionality, creating context menus, showing file pickers, implementing keyboard shortcuts, displaying notifications or dialogs, or building platform-native UI experiences. Triggers include "menu", "tray icon", "context menu", "file dialog", "shortcuts", "accelerator", "native dialog", "system tray", "notification", "menu bar", or "right-click menu".
62electrobun-distribution
Packaging, code signing, notarization, and distribution for Electrobun desktop applications. This skill covers building production bundles, creating installers and distributable packages, code signing for Windows and macOS, Apple notarization for Gatekeeper, auto-updater implementation, delta updates, update servers, cross-platform build processes, CI/CD integration, app icons and resources, version management, release workflows, Windows SmartScreen requirements, macOS DMG creation, Linux package formats (deb, rpm, AppImage), and distribution best practices. Use when preparing app for production, implementing auto-updates, setting up code signing certificates, troubleshooting distribution issues, creating installers, configuring update servers, building for multiple platforms, or releasing new versions. Triggers include "build", "package", "distribute", "code sign", "notarize", "installer", "auto-update", "release", "production build", "DMG", "updater", "delta update", or "certificate".
52electrobun-debugging
Development workflow, debugging, and troubleshooting for Electrobun desktop applications. This skill covers debugging the main process (Bun) and webview processes, Chrome DevTools integration, console logging strategies, error handling, performance profiling, memory leak detection, build error troubleshooting, common runtime errors, development environment setup, hot reload configuration, source maps, breakpoint debugging, network inspection, WebView debugging on different platforms, native module debugging, and systematic debugging approaches. Use when encountering build failures, runtime errors, crashes, performance issues, debugging RPC communication, inspecting webview DOM, profiling CPU/memory usage, troubleshooting platform-specific issues, or setting up development workflow. Triggers include "debug", "error", "crash", "troubleshoot", "DevTools", "inspect", "breakpoint", "profiling", "performance issue", "build error", "not working", or "logging".
40