IPC Communication Guide
This guide covers how IPC (Inter-Process Communication) works in Tronbun and best practices for building your frontend-to-backend communication layer.
How IPC Works
Tronbun uses a JSON-line protocol over stdin/stdout between the Bun process and the native webview:
Frontend (webview) → JSON message → webview_main → stdin → Bun process
Bun process → stdout → webview_main → JSON → FrontendEach IPC call:
- The frontend calls
window.<WindowName>.<handler>(data) - The webview sends
{"type": "ipc_call", "channel": "<handler>", "data": ...}to Bun - Bun's registered handler processes the request and returns a result
- The result is sent back to the frontend as a resolved Promise
Two Approaches
1. Decorator-Based (Recommended)
Use @windowName and @mainHandler for type-safe, auto-generated APIs:
// Backend
import { WindowIPC, windowName, mainHandler } from "tronbun";
@windowName("App")
class MainWindow extends WindowIPC {
@mainHandler("getUser")
async handleGetUser(userId: string) {
return await db.findUser(userId);
}
}
// Frontend — auto-typed
const user = await window.App.getUser("user-123");Advantages:
- Auto-generated TypeScript types (
src/ipc-types.ts) - Full type safety on both backend and frontend
- Self-documenting: decorators make the API surface clear
- Types regenerated on every build
2. Manual Registration
Use handle() for simple cases or dynamic handlers:
// Backend
const win = new Window({ title: "My App" });
win.handle("getUser", async (userId: string) => {
return await db.findUser(userId);
});
// Frontend — no auto-generated types
const user = await window.__ipc.invoke("getUser", "user-123");Use when:
- You need to register handlers dynamically at runtime
- You're prototyping and don't need type safety yet
- You're mixing with the decorator approach
Handler Patterns
Simple Value Parameters
@mainHandler("greet")
async handleGreet(name: string) {
return `Hello, ${name}!`;
}Object Parameters
interface CreateUserInput {
name: string;
email: string;
role: "admin" | "user";
}
@mainHandler("createUser")
async handleCreateUser(data: CreateUserInput) {
return await db.createUser(data);
}No Parameters
@mainHandler("getSystemInfo")
async handleGetSystemInfo() {
return {
platform: process.platform,
memory: process.memoryUsage(),
uptime: process.uptime(),
};
}Void Return
@mainHandler("logEvent")
async handleLogEvent(event: { type: string; data: any }) {
await logger.write(event);
// No return value — frontend gets undefined
}Error Handling
Thrown errors in handlers are caught and returned to the frontend:
@mainHandler("deleteFile")
async handleDeleteFile(path: string) {
const exists = await Bun.file(path).exists();
if (!exists) {
throw new Error(`File not found: ${path}`);
}
await fs.unlink(path);
return { deleted: true };
}On the frontend:
try {
await window.App.deleteFile("/nonexistent");
} catch (err) {
console.error(err.message); // "File not found: /nonexistent"
}Frontend API
When using WindowIPC with @windowName("App"), the frontend gets:
// Each @mainHandler("name") becomes:
window.App.name(data) → Promise<ReturnType>The generated API handles:
- Serializing parameters to JSON
- Sending the IPC message to the backend
- Deserializing the response
- Resolving/rejecting the Promise
Multiple Windows
Each WindowIPC subclass gets its own namespace:
@windowName("Editor")
class EditorWindow extends WindowIPC {
@mainHandler("save")
async handleSave(content: string) { ... }
}
@windowName("Preview")
class PreviewWindow extends WindowIPC {
@mainHandler("render")
async handleRender(markdown: string) { ... }
}Frontend for EditorWindow: window.Editor.save(content) Frontend for PreviewWindow: window.Preview.render(markdown)
Child View IPC
Child views have independent IPC registries:
const sidebar = await win.createChildView({
bounds: { x: 0, y: 0, width: 250, height: 600 },
url: "public/sidebar.html",
});
// This handler is only available in the sidebar's webview
sidebar.registerIPCHandler("getNavItems", async () => {
return navItems;
});
// This handler is only available in the parent window
win.handle("getMainContent", async () => {
return mainContent;
});Type Generation
Running tronbun build or tronbun generate-types scans your source for decorators and generates src/ipc-types.ts:
// Auto-generated — do not edit
interface CreateUserInput {
name: string;
email: string;
role: "admin" | "user";
}
export interface AppHandlers {
getUser: (data: string) => Promise<User>;
createUser: (data: CreateUserInput) => Promise<User>;
getSystemInfo: (data: void) => Promise<SystemInfo>;
}
declare global {
interface Window {
App: AppHandlers;
}
}This gives you:
- IntelliSense/autocomplete on the frontend
- Compile-time type checking for parameter and return types
- Documentation of available IPC methods
Best Practices
Use descriptive handler names.
getUseris better thanfetchordata.Use typed interfaces for complex parameters. Avoids
anyand ensures type safety across the IPC boundary.Keep handlers focused. Each handler should do one thing. Prefer many small handlers over few large ones.
Handle errors in handlers. Thrown errors propagate to the frontend as rejected Promises.
Avoid sending large payloads. IPC uses JSON serialization. Large binary data should be written to disk and referenced by path.
Use the decorator approach for production. Manual
handle()is fine for prototyping, but decorators provide the best developer experience.
See Also
- WindowIPC API — Decorator system reference
- Window API — Manual IPC registration
- Child Views — Child view IPC
