Skip to content

borislavvp/electron-tipc

Repository files navigation

electron-tipc

Typesafe communication through electron's renderer and main processes.

Demo

The renderer process, which is demonstrated in the App.tsx above is not importing any code from the electron main process, only the type declarations of the rendererAPI.


Usage

Basic setup

  1. IPC events are organized and defined in a type of the form an array type that includes basic FSA compliant object definitions with optional payload:
 type SomeEvents = [
  {
    type: "event-type",
    payload?: SomePayload
  },
  {...},
  {...},
  ...
 ]
  1. Then an eventer is created using the defined type using the library:
import { createEventer } from "electron-tipc";

type SomeEvents = [...];

const someEventer = createEventer<SomeEvents>();

The eventer contains both of electron's ipcMain and ipcRenderer - someEventer.main and someEventer.renderer respectively which have the same signature for each function exposed in the original ipcMain and ipcRenderer. The difference is that each function is strictly typed allowing communication only to channels that are defined in the type for which the eventer is created, where the channel corresponds to the type property, and the data received from any callback function corresponds to the payload property, everything infered.

Example for the defined SomeEvents :

Using the ipcRenderer.send :

const data: SomePayload = {..}
someEventer.renderer.send("event-type", data);

Using the ipcMain.on :

someEventer.main.on("event-type", (_,data: SomePayload) => {...});

Then whenever there is a need in the codebase to use any IPC communication involving the defined SomeEvents, the someEventer is used.

  1. Expose electron functions to the render process in the preload
import { createBridge } from "electron-tipc";
import { someEventer } from "./ipc"

const electronAPI = {
  onSomeRenderCall:(data:SomePayload) => someEventer.send("event-type",data)
}

export const electronBridge = createBridge({
   electronAPI
})
  1. Extend the type of the render process window object to include the exposed electronAPI. When the electronBridge is exposed, it is attached to the window object. Note that the render process only imports the type of the exposed API and nothing else.
//index.d.ts

export {};

type ElectronBridge =
  typeof import("path-to-preload").electronBridge;

declare global {
  interface Window extends ElectronBridge {}
}

With this setup, the following is achieved:

  • the render process will be in sync with the exposed API from the electron's main process
  • the whole IPC communication through the electron processes will be typesafe and a single change to a definition of an event or function's signature will be highlighted

Additional features

Typesafe BrowserWindow

import { createBrowserWindow } from "electron-tipc";

const window = createBrowserWindow<SomeEvents>();

Typesafe BrowserView

import { createBrowserView } from "electron-tipc";

const window = createBrowserView<SomeEvents>();

Callback extension for ipcRender.on, ipcRender.once, ipcRender.off

import { someEventer } from "./ipc";

const callbackOn = someEventer.render.on("event-type");
const callbackOnce = someEventer.render.once("event-type");
const callbackOff = someEventer.render.off("event-type");

callbackOn((data:SomePayload) => {
 // Code here is executed when the "event-type" event is caught by ipcRender.on
})

callbackOnce((data:SomePayload) => {
 // Code here is executed when the "event-type" event is caught by ipcRender.once
})

callbackOff((data:SomePayload) => {
 // Code here is executed when the "event-type" event is caught by ipcRender.off
})

TODO

  • Add tests
  • Add more examples
  • Explore possibilities for more features/integrations
  • Evaluate if the current abstraction and way of defining events is usefull in more complex projects