npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-renderer

Peer dependencies:

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 @Action methods on controller instances to proxy through ipcRenderer.invoke.

function rewriteActions(
  instances: any[],
  options: CreateRoostRendererOptions
): void

How 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 @Action are 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 (a Promise) 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 access

TypeScript 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:

  1. Create a Roost app with the same controllers.
  2. Register an ipcMain.handle() listener on the same channel.
  3. 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