1.0.2 • Published 2 years ago

@electron-tools/ipc v1.0.2

Weekly downloads
-
License
MIT
Repository
github
Last release
2 years ago

@electron-tools/ipc

Introduction

Inspired by VSCode IPC implement. A more simple and more powerful way to use Electron IPC. Support service Invoke/Acknowledgement mode and event Subscribe/Publish mode between main process and renderer process.

Installation

# npm
npm install @electron-tools/ipc
# yarn
yarn add @electron-tools/ipc
# pnpm
pnpm add @electron-tools/ipc

Features

Invoke service with acknowledgement between main process and renderer process

Before using @electron-tools/ipc,we have to use event.sender.send to response to renderer and use try catch to notice client current invoke operation has error, it is not clear.

// main process
import { ipcMain } from 'electron';

ipcMain.on('getUserById', async (event, id) => {
  try {
    const userInfo = await userService.getUserById(id);
    event.sender.send('getUserById:response', userInfo);
  } catch (e) {
    event.sender.send('getUserById:error', e.message);
  }
});
// renderer process
import { ipcRenderer } from 'electron';

function getUserById(id) {
  return new Promise((resolve, reject) => {
    ipcRenderer.on('getUserById:response', (event, userInfo) => {
      resolve(userInfo);
    });
    ipcRenderer.on('getUserById:error', (event, errorMessage) => {
      resolve(new Error(errorMessage));
    });
    ipcRenderer.send('getUserById', id);
  });
}

After using @electron-tools/ipc

// main process
import { ElectronIPCMain } from '@electron-tools/ipc';

const electronIPCMain = new ElectronIPCMain();
// register user ServerChannel to handle client request.
electronIPCMain.registerChannel('user', {
  invoke(ctx, command, ...args) {
    switch (command) {
      case 'getUserById':
        // the only thing you need to do is invoke the service method.
        // if userService#getUserById throw a error,
        // error message will auto response to client.
        return userService.getUserById(args[0]);
    }
  },
});
// renderer process
import { ElectronIPCRenderer } from '@electron-tools/ipc';

const ipcRenderer = new ElectronIPCRenderer('main_window');
function getUserById(id) {
  // get user channel and invoke with command getUserById.
  // invoke returns type will be Promise<UserInfo>.
  // if server throw a error in execution,
  // invoke will reject with a error.
  return ipcRenderer.getChannel('user').invoke('getUserById', id);
}

Since Electron\@7, we can use ipcMain.handle with ipcRenderer.invoketo do something like above show. Actually, @electron-tools/ipc use ipcRender.send and ipcMain.on under the hood. in this example, we make a invoke request from main process to renderer process, but we can use the same way to make a invoke request from renderer process to main process, let's do it.

// main process
import { ElectronIPCMain } from '@electron-tools/ipc';

const electronIPCMain = new ElectronIPCMain();

async function getUserById(id) {
  // we should set the target client by the first argument of getChannel which mean the client ctx.
  // because the renderer process with ctx 'main_window' may be more than one,
  // invoke returns with type Promise<Array<UserInfo>>
  const [user] = await electronIPCMain.getChannel('main_window', 'user').invoke('getUserById', id);
  return user;
}
// renderer process
import { ElectronIPCRenderer } from '@electron-tools/ipc';

const ipcRenderer = new ElectronIPCRenderer('main_window');
ipcRenderer.registerChannel('user', {
  invoke(ctx, command, ...args) {
    switch (command) {
      case 'getUserById':
        return userService.getUserById(args[0]);
    }
  },
});

Sometimes a server execution will take a long time, we may want to cancel the execution because it is outdated. Channel#invoke method returns type will have a cancel properties. which can cannel the invoke manually. for example:

// renderer process
import { ElectronIPCRenderer } from '@electron-tools/ipc';

const ipcRenderer = new ElectronIPCRenderer('main_window');
function getUserById(id) {
  return ipcRenderer.getChannel('user').invoke('getUserById', id);
}

const userPromise = getUserById('xxx');
userPromise.cancel();

Subscribe event which will be published later

Publish/Subscribe design pattern is widely used in daily development. @electron-tools/ipc also support it. Renderer process can subscribe main process event which will be published later and vice versa.

// main process
import { ElectronIPCMain } from '@electron-tools/ipc';
import { EventEmitter } from 'events';

const eventBus = new EventEmitter();

setInterval(() => {
  eventBus.emit('userStatusChange', 'user login');
}, 1000);

const electronIPCMain = new ElectronIPCMain();
electronIPCMain.registerChannel('user', {
  event(ctx, event) {
    switch (event) {
      case 'userStatusChange':
        return {
          subscribe(cb) {
            eventBus.on('userStatusChange', cb);
            return () => eventBus.off('userStatusChange', cb);
          },
        };
    }
  },
});
// renderer process
import { ElectronIPCRenderer } from '@electron-tools/ipc';

const ipcClient = new ElectronIPCRenderer('main_window');
// subscription has a unscribe method to stop listen userStatusChange event.
const subscription = ipcClient
  .getChannel('user')
  .event('userStatusChange')
  .subscribe((latestStatus) => {
    console.log(latestStatus);
  });
setTimeout(() => {
  // after subscribe, main process will auto remove userStatusChange listener.
  // and will never send and ipc message except userStatusChange event been subscripted again.
  subscription.unsubscribe();
}, 5000);

// user login
// user login
// user login
// user login

@electron-tools/ipc use rxjs subscribe/publish pattern under the hood. so it really simple when working with rxjs.

// main process
import { ElectronIPCMain } from '@electron-tools/ipc';
import { Subject } from 'rxjs';

const userStatusSubject = new Subject();

setInterval(() => {
  userStatusSubject.next('user login');
}, 1000);

const electronIPCMain = new ElectronIPCMain();
electronIPCMain.registerChannel('user', {
  event(ctx, event) {
    switch (event) {
      case 'userStatusChange':
        return userStatusSubject;
    }
  },
});

Concept

Connection

Represent connection between main process(ElectronIPCMain) and renderer process(ElectronIPCRenderer). ElectronIPCMain maintains a group of connection. ElectronIPCRenderer maintains only one connection with ElectronIPCMain.

ServerChannel

ServerChannel has two method, invoke and event, connect with ClientChannel by connection. Represent a group of service and subscribable, service can be invoked and subscribable can be subscribe through ClientChannel.

ClientChannel

ClientChannel is a proxy channel of ServiceChannel, also contains invoke and event method.

API Reference

interface Subscription {
  unsubscribe(): void;
}

interface Subscribable<TData = any> {
  subscribe(receiver: (data: TData) => void): Subscription | void;
}

interface ClientSubscribable<TData = any> {
  subscribe(receiver: (data: TData) => void): Subscription;
}

interface ServerChannel<TContext = string> {
  invoke?(ctx: TContext, command: string, ...args: any[]): any;
  event?(ctx: TContext, event: string): Subscribable<any>;
}

interface Cancellable {
  cancel(reason?: string): void;
}

type CancellablePromise<T = any> = Promise<T> & Cancellable;

interface ClientChannel {
  invoke<TArgs extends any[] = any[], TReturn = any>(command: string, ...args: TArgs): CancellablePromise<TReturn>;
  event<TData = any>(event: string): ClientSubscribable<TData>;
}

interface ElectronIPCMain<TContext = string> {
  registerChannel(channel: string, serverChannel: ServerChannel): void;
  getChannel(ctx: TContext, channel: string): ClientChannel;
  // destroy will disconect all connections.
  destroy(): void;
}

interface ElectronIPCRenderer {
  registerChannel(channel: string, serverChannel: ServerChannel): void;
  getChannel(ctx: TContext, channel: string): ClientChannel;
  // destroy will disconect current connection.
  destroy(): void;
}

Something you should keep in mind

  1. ElectronIPCMain and ElectronIPCRenderer should be initialized only once, or says it should be a singleton instance. You can initialize in one place and exports the instance, all other source want to use should import the instance.
1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago