electrobun-rpc-patterns
SKILL.md
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
Weekly Installs
23
Repository
rajavijayach/el…n-skillsGitHub Stars
3
First Seen
Feb 21, 2026
Security Audits
Installed on
codex23
opencode22
gemini-cli22
amp22
github-copilot22
kimi-cli22