@canlooks/roost-electron-renderer
v0.0.1
Published
A backend micro service framework
Readme
@canlooks/roost-electron-renderer
Electron renderer process plugin for the Roost micro-service framework. Provides seamless IPC-based remote procedure call (RPC) proxying — call main-process controller actions from the renderer as if they were local methods.
Overview
In an Electron application, business logic typically runs in the main process via Roost controllers decorated with @Controller and @Action. This package creates transparent proxy instances of those controllers in the renderer process. Every @Action-decorated method on a proxy is replaced with an ipcRenderer.invoke() call, routing arguments through Electron's IPC channel to the main process and returning the result as a promise.
The renderer code never touches IPC directly — it simply calls methods on controller instances.
┌─ Renderer Process ─────────────────────┐
│ │
│ const { myCtrl } = │
│ await createRoostRenderer( │
│ { MyController }, │
│ { ipcRenderer } │
│ ) │
│ │
│ // Looks like a local call... │
│ const result = await myCtrl.doWork(x) │
│ │ │
└─────────────────────┼───────────────────┘
│ ipcRenderer.invoke(channel, path, ...args)
▼
┌─ Main Process ─────────────────────────┐
│ │
│ @Controller('api') │
│ class MyController { │
│ @Action('doWork') │
│ doWork(x) { ... } │
│ } │
│ │
└─────────────────────────────────────────┘Installation
npm install @canlooks/roost-electron-rendererPeer dependencies:
@canlooks/roost— the core Roost frameworkelectron— providesipcRenderer
API Reference
createRoostRenderer(controllers, options)
Creates proxy controller instances whose @Action methods are wired to ipcRenderer.invoke.
function createRoostRenderer<T extends Record<string, ComponentType>>(
controllers: T,
options: CreateRoostRendererOptions
): Promise<{ [K in keyof T]: InstanceType<T[K]> }>Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| controllers | Record<string, ComponentType> | Map of controller classes keyed by name. Each class must be decorated with @Controller from @canlooks/roost. |
| options | CreateRoostRendererOptions | Configuration for the renderer proxy. |
Returns
An object with the same keys as the input controllers map, where each value is an instance of the corresponding controller class with its @Action methods rewritten to invoke IPC.
CreateRoostRendererOptions
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
| ipcRenderer | IpcRenderer | Yes | — | The Electron ipcRenderer instance from the renderer process. |
| channel | string | No | '@canlooks/roost-electron' | Custom IPC channel name. |
rewriteActions(instances, options)
Internal. Exported for advanced use cases. Rewrites
@Actionmethods on controller instances to proxy throughipcRenderer.invoke.
function rewriteActions(
instances: any[],
options: CreateRoostRendererOptions
): voidHow It Works
Action Path Resolution
Each @Action-decorated method is mapped to an IPC key derived from the controller and action paths:
| Controller Decorator | Action Decorator | Resolved IPC Key |
|----------------------|------------------|------------------|
| @Controller('api') | @Action('hello') | '/api/hello' |
| @Controller('users') | @Action('list') | '/users/list' |
| @Controller() | @Action('status') | '/status' |
The resolved key is passed as the second argument to ipcRenderer.invoke(channel, key, ...args).
Method Rewriting
- Only methods decorated with
@Actionare rewritten. Regular methods and properties are left untouched. - Rewriting happens per-instance, not on the prototype. The original class prototype remains intact.
- Each call to a rewritten method results in a fresh
ipcRenderer.invoke()call — no caching or batching. - All arguments passed to the method are forwarded as variadic arguments to
ipcRenderer.invoke. - The return value of
ipcRenderer.invoke(aPromise) is returned directly, preserving the original reference.
Error Propagation
Errors thrown by the main process handler (or IPC errors) are propagated as rejected promises:
const { ctrl } = await createRoostRenderer({ TestController }, { ipcRenderer })
try {
await ctrl.hello('World')
} catch (err) {
// err is the exact rejection from ipcRenderer.invoke
}Usage
Basic Example
// ═══════════════════════════════════════════════════════════════════
// Shared controller definition (e.g., in a shared package)
// ═══════════════════════════════════════════════════════════════════
import { Controller, Action } from '@canlooks/roost'
@Controller('api')
export class ApiController {
@Action('greet')
greet(name: string): string {
return `Hello, ${name}!`
}
@Action('add')
add(a: number, b: number): number {
return a + b
}
// Regular methods are NOT proxied
getVersion(): string {
return '1.0.0'
}
}// ═══════════════════════════════════════════════════════════════════
// Main process — sets up IPC handler with Roost
// ═══════════════════════════════════════════════════════════════════
import { app, BrowserWindow, ipcMain } from 'electron'
import { Roost } from '@canlooks/roost'
app.whenReady().then(async () => {
const roost = await Roost.create({
named: { ApiController }
})
// Handle incoming IPC calls
ipcMain.handle('@canlooks/roost-electron', async (_event, path, ...args) => {
const results = await roost.invoke(path, ...args)
return results[0] // Return the first result for single-action calls
})
// ... create BrowserWindow, load renderer
})// ═══════════════════════════════════════════════════════════════════
// Renderer process — creates proxy and calls methods transparently
// ═══════════════════════════════════════════════════════════════════
import { createRoostRenderer } from '@canlooks/roost-electron-renderer'
import { ipcRenderer } from 'electron'
async function main() {
const { ApiController: api } = await createRoostRenderer(
{ ApiController },
{ ipcRenderer }
)
// These look like local calls but go through IPC to the main process:
const greeting = await api.greet('World') // → "Hello, World!"
const sum = await api.add(3, 4) // → 7
// Non-action methods are NOT proxied — they run locally:
const version = api.getVersion() // → "1.0.0" (local call)
}Multiple Controllers
const { UserController, OrderController } = await createRoostRenderer(
{ UserController, OrderController },
{ ipcRenderer }
)
// Each controller's @Action methods are independently proxied
const users = await UserController.list()
const order = await OrderController.findById(42)Custom IPC Channel
const { ApiController: api } = await createRoostRenderer(
{ ApiController },
{
ipcRenderer,
channel: 'my-custom-channel' // Must match main process ipcMain.handle()
}
)Controllers Without Actions
Controllers with no @Action methods are handled gracefully — the instance is returned as-is with no method rewriting:
@Controller('config')
class ConfigController {
theme = 'dark'
setTheme(t: string) { this.theme = t }
}
const { ConfigController: config } = await createRoostRenderer(
{ ConfigController },
{ ipcRenderer }
)
console.log(config.theme) // 'dark' — normal property accessTypeScript Support
The package is written in TypeScript and ships with full type declarations. The return type of createRoostRenderer is fully inferred from the input controller map — each property is correctly typed as an instance of the corresponding class.
const renderers = await createRoostRenderer(
{ ApiController, UserController },
{ ipcRenderer }
)
// TypeScript knows these types:
renderers.ApiController.greet(name: string): Promise<string>
renderers.UserController.list(): Promise<string[]>The ComponentType constraint ensures only class constructors (not plain objects or primitives) can be passed as controllers.
Main Process Integration
This package handles the renderer side of the IPC bridge. The main process must:
- Create a Roost app with the same controllers.
- Register an
ipcMain.handle()listener on the same channel. - Call
roost.invoke(path, ...args)and return the result.
See the Roost framework documentation for details on main-process setup, including the @canlooks/roost-electron package which provides main-process IPC handling out of the box.
License
MIT © C.CanLiang
