Start the

git clone [email protected]:spcBackToLife/jupiter-electron-ipc-demo.git
cd jupiter-electron-ipc-demo
yarn install
yarn dev
Copy the code

Introduction to the

This project is a complete realization of the ipc communication mechanism in vscode, we can see the above startup mode, start and experience.

Service Usage Examples

[Create a windowSercice]

export class WindowService {

  doSomething(): string {
    console.log('do something and return done')
    return 'done'; }}Copy the code

[Create a channel for a service]

import { IServerChannel } from ".. /core/common/ipc";
import { WindowService } from "./windowService";
import { Event } from '.. /base/event';

export class WindowChannel implements IServerChannel {
  constructor(
    public readonly windowService: WindowService,
  ) {}

  listen(_: unknown, event: string): Event<any> {
    // Currently not supported
    throw new Error(`Not support listen event currently: ${event}`);
  }

  call(_: unknown, command: string, arg? :any) :Promise<any> {
    switch (command) {
      case 'doSomething':
        return Promise.resolve(this.windowService.doSomething());

      default:
        return Promise.reject('No service to call! '); }}}Copy the code

[render process – SRC /render/index.html]

<html>
  <div>jupiter electron</div>
  <script>
    const { Client }  = require('.. /.. /core/electron-render/IPCClient');
    const mainProcessConnection = new Client(`window_1`);
    const channel = mainProcessConnection.getChannel('windowManager');
    channel.call('doSomething').then((result) = > console.log('result:', result));
  </script>
</html>

Copy the code

[main process-src /main.ts]

import { app, BrowserWindow } from 'electron';
import path from 'path';
import { Server as ElectronIPCServer } from '.. /core/electron-main/ipc.electron-main';
import { WindowChannel } from './windowServiceIpc';
import { WindowService } from './windowService';

app.on('ready'.() = > {
  const electronIpcServer = new ElectronIPCServer();
  electronIpcServer.registerChannel('windowManager'.new WindowChannel(new WindowService()))


  const win = new BrowserWindow({
    width: 1000.height: 800.webPreferences: {
      nodeIntegration: true}});console.log('render index html:', path.join(__dirname, 'render'.'index.html'));
  win.loadFile(path.join(__dirname, 'render'.'index.html'));
})

Copy the code

Start and run a wave:

"scripts": {..."dev": "tsc && electron ./src/main.js". },Copy the code

Activation:

  yarn dev
Copy the code

At this point, we have implemented the ipc mechanism of vscode, you can go here to experience:

jupiter-electron-ipc-demo

Model is introduced

You can also use Vscode’s communication mechanism in your Electron: implement Vscode communication mechanism from zero to one

Electron is multi-process, so interprocess communication is essential when writing applications, especially between the main process and the renderer process. But if you don’t design the communication mechanism well, then the communication in the application can be chaotic and unmanageable, as those of you who developed Electron may well know.

Let’s take an example of what communication looks like in traditional Electron and Jupiter Electron:

Example: A window sends a message to the main process to do something and returns a result: done.

In Electron, we might need to do this:

【 Main process 】


/ / do something
const doSomething = (. params) = > {
  console.log('do some sync things');
  return Promise.resolve('complete');
}
app.on('ready'.() = > {
  const win = new BrowserWindow({});
  ipcMain.on('dosomething'.async (e, message) => {
    const result = await doSomething(message.params);
    // Return the result (the renderer needs to pass the request ID to ensure unique channel communication)
    win.webContents.send(`dosomething_${message.requestId}`, result); })})Copy the code

[Rendering process]

const doSomething = () = > {
  return new Promise((resolve, reject) = > {
    const requestId = new Date().getTime();
    // Listen for only one return
    ipcRenderer.once(`dosomething_${requestId}`.(result) = > {
       console.log('result:', result);
       resolve(result);
    })
    // Send a message -dosomething
    ipcRenderer.send('dosomething', {
      requestId,
      params: {... }})})}... doSomething();Copy the code

The obvious problem is that if the main process fails, it has to send the failure message back and do some processing.

In fact, there are many problems in the above traditional Electron communication writing method, which will not be added here.

What do we do in Jupiter Electron?

【 Main process 】

const doSomething = (params) = > {
  console.log('do something');
  return Promise.resolve('complete');
}
Copy the code

[Rendering process]

 import bridge from '@jupiter-electron/runtime';

 export const doAthing = () = > {
   return bridge.call('doSomething', params)
     .catch((err) = > console.log('main exec error:', err));
 }

 doAthing();
Copy the code
  • Don’t worry about uniqueness, internal mechanics take care of it.
  • Don’t worry about how failed exceptions are handled back to the front end, internal mechanisms return errors to the renderer process.
  • Communication compression optimization? Don’t worry, it’s taken care of.

It can be found that in Jupiter Electron, communication is a very simple thing, the main process and the renderer process communicate; Window to window communication.

So how does this work and what’s the design behind it?

In fact, the communication mechanism is based on Vsocde source mechanism abstract out, below, on the design mechanism behind the interpretation, with everyone to achieve a set of IPC communication mechanism.

What is the goal of designing a communication mechanism? Process communication what?

Send, ipcRender. Send and ipcmain. on in Electron. Our goal of designing the communication mechanism is:

  • Simplify our communication flow
  • Ensure the stability of communication
  • Improve the quality of communication
  • Improves communication efficiency and reduces resource usage

To design the communication mechanism, we have to say, what is the purpose of our communication in Electron? What are the characteristics?

Communication mechanism design

Because of the Electron multi-process feature, sometimes to do one thing, we have to need multi-process cooperation, therefore, we need communication.

In general, we have these characteristic scenarios:

  • The renderer expects the main process to do something and returns an execution result.
  • The Renderer notifies the main process of a message without returning a result.
  • The main process expects the renderer process to do something and returns the result of its execution.
  • Renderer expects renderer to do something and returns an execution result.
  • The Renderer listens for messages from the main process.

In general, based on the above characteristics, we can sum up as: servitization, which is another feature of the difference between Electron development and Web that I put forward.

Servitization means providing services

  • When your renderer expects the main process to do something and returns an execution result, the main process needs to do something that can be abstracted into a corresponding service that the main process is responsible for providing.
  • When your main process expects the renderer process to do something and return the result of its execution, what the renderer process needs to do can be abstracted into the corresponding service that the renderer is responsible for providing.

Therefore, we can design the following form:

The server can be either a “renderer” or a “main” process, depending on who is serving whom.

As you can see, the server provides n services for clients to access. But there is such a problem, here, the services provided by the service end, is all the client can access, there will be a problem, like: alipay provides the basis for all user services, such as: electricity, taxes, social security query service, but some services may only certain people can access, such as: optimizing 100% money fund services.

Therefore, our design cannot meet this requirement, so we need to make an adjustment:

We added the channel service concept, where each client accesses the service based on the channel, for example:

  • Client 1 accesses channel service 1, and client 2 accesses channel service 2
  • Channel services 1 and 2 both have common services and their own privileged services.
  • When the customer service accesses the service, it generates a channel for each customer to provide him with the service he has
  • When creating the corresponding channel service for customer service, the general service of the server will be registered in the channel, and the special service will be registered according to the characteristics of users.

Through the above mode, the above problems are solved, but it also brings some problems:

Does a user create a new channel service every time he accesses the service?

According to the above logic, this would be the case, so to solve this problem, we need the following design:

On the server, we add a new concept, called connect (Connection), when the client is initialized, initiate communication links, the channel will be to create a new service, and stored in the server, the client the next time a service access request, go directly to obtain channel services, to perform the corresponding services.

Such a scheme looks perfect, but there is a problem. The above scheme can be applied to the following scenarios:

  • The renderer expects the main process to do something and returns an execution result.

If it is such a design, how to meet:

  • The main process expects the renderer process to do something and returns the result of its execution.

It’s as if the “server” and “client” have swapped identities. How do you keep both of these things together?

We can do the following design:

We add “channel client” on the server connection and “channel service” on the client, so that the client can access the service of the channel service on the server, and also call the service of the channel service on the client, so as to achieve the above problem.

So far, we have basically completed the design and analysis of vscode’s entire communication mechanism, and the next step is the concrete implementation. Of course, in the implementation process, we also need to consider:

  • Buffer processing of communication messages
  • Exception handling is performed during communication
  • The uniqueness of communication is guaranteed

Implementation of communication mechanism

In the above statement, we have mentioned services and channels. Here we unify the concept:

  • A channel provides a service, and a service is a channel

The subsequent unified use of “channel” to refer to a “service”

Let’s go through some of the concepts that we’ve come up with in this process.

  • Server -> IPCServer
    • The outermost server in the figure, which manages all connections
  • Client -> IPCClient
    • The outermost client in the figure is used to establish connections, send and receive messages and process messages in a unified manner
  • Connection – > Connection
  • ChannelServer -> ChannelServer
    • Provide one end of the service
  • ChannelClient -> ChannelClient
    • The channel client is the end that accesses a channel (service) on the server
  • ServerChannel -> ServerChannel
    • Channel The channel (service) registered by the server is the “server channel”

Of course, before the implementation of vscode communication mechanism, in fact, there are many required courses, but these will be explained in the future, now you can understand their role, and use it, does not affect the use and understanding of IPC mechanism.

Before we start vscode communication design, we need to provide you with some basic utility classes:

- "cancelablePromise/" cancelablePromise - "disposable/" listen to resource release base class - "buffer, buffer-utils" buffer processing of messages - "events" is vscode Their implementation of the event class, is also a decoration of the event class, very nice!! - "iterator" for linkedList data structure iteration - double linked data structure implementation of "linkedList" JS - "utils" helper classCopy the code

The following will be implemented in the following order:

  • Design and implementation of message communication protocol
  • ServerChannel -> ServerChannel
  • ChannelServer -> ChannelServer
  • ChannelClient -> ChannelClient
  • Connection – > Connection
  • Client -> IPCClient
  • Server -> IPCServer

Design and implementation of message communication protocol

In the figure above, we have described that a “client” and “server” will establish a “connection” and have a “channel server”. In “client”, how to access “channel server”? Here we need to define the access protocol:

We stipulate that:

  • When the client is initialized, it sends an ipc: Hello message to establish a connection with the server.
  // Something like this, not implementation code, just a hint
  class IPCClient {
    constructor() {
      ipcRenderer.send('ipc:hello'); }}Copy the code
  • Messages are received and sent on the IPC: Message channel on both the client and the server.
xxx.webContents.send('ipc:message', message); . ipcRenderer.send('ipc:message', message);Copy the code
  • When the “client” is uninstalled (e.g. the window is closed), send the disconnect message: IPC :disconnect to destroy all message listening.

Therefore, we design the following protocol:

[core/common/ipc.electron.ts]

import { Event } from '.. /.. /base/event'; / / tools
import { VSBuffer } from '.. /.. /base/buffer'; / / tools

export interface IMessagePassingProtocol {
  onMessage: Event<VSBuffer>;
  send(buffer: VSBuffer): void;
}

export interface Sender {
  send(channel: string.msg: Buffer | null) :void;
}

export class Protocol implements IMessagePassingProtocol {
  constructor(
    private readonly sender: Sender,
    readonly onMessage: Event<VSBuffer>,
  ) {}

  send(message: VSBuffer): void {
    try {
      this.sender.send('ipc:message', <Buffer>message.buffer);
    } catch (e) {
      // systems are going down
    }
  }

  dispose(): void {
    this.sender.send('ipc:disconnect'.null); }}Copy the code

Instructions for use:

.const protocol = newProtocol(webContents, onMessage); .const protocol = new Protocol(ipcRenderer, onMessage);
Copy the code

Define the server channel: IServerChannel

A Server Channel is a channel (service) registered in channel Services on the Server.

[core/common/ipc.ts]

export interface IServerChannel<TContext = string> {
  call<T>(
    ctx: TContext,
    command: string, arg? :any, cancellationToken? : CancellationToken, ):Promise<T>; // Initiate a service request
  listen<T>(ctx: TContext, event: string, arg? :any): Event<T>;// Listen for messages
}
Copy the code

The specific implementation will define the service only when it is actually used. Therefore, the definition and use of “server channel” will be introduced in the use case after the IPC mechanism is completed.

Define the server side of the channel

Before the “client” accesses the service, it establishes a “connection” with the “server”. In the “connection”, a “channel server” manages the “service channels” that the service provides to the “client”.

First, we define the “service channel” interface:

[core/common/ipc.ts]

export interface IChannelServer<TContext = string> {
  registerChannel(channelName: string.channel: IServerChannel<TContext>): void;
}
Copy the code
  • There is basically a way to register channels

Next, we implement a “channel service”

export class ChannelServer<TContext = string>
  implements IChannelServer<TContext>, IDisposable {
  // Save the channel information that the client can access
  private readonly channels = new Map<string, IServerChannel<TContext>>();

  // Message communication protocol monitor
  private protocolListener: IDisposable | null;

  // Save the active request, cancel the execution after receiving the cancellation message, release the resource
  private readonly activeRequests = new Map<number, IDisposable>();

  // Before the channel server is registered, many requests may arrive, at which point they will stay in the queue
  // If timeoutDelay is out of date, it will be removed
  // If the channel is registered, it will be taken out of the queue and executed
  private readonly pendingRequests = new Map<string, PendingRequest[]>();

  constructor(
    private readonly protocol: IMessagePassingProtocol, // Message protocol
    private readonly ctx: TContext, / / service name
    private readonly timeoutDelay: number = 1000.// Communication timeout
  ) {
    // Receive ChannelClient messages
    this.protocolListener = this.protocol.onMessage(msg= >
      this.onRawMessage(msg),
    );
    // When the channel server is instantiated, we need to return a message to the channel server that the instantiation is complete:
    this.sendResponse({ type: ResponseType.Initialize });
  }

  public dispose(): void{... }public registerChannel(
    channelName: string.channel: IServerChannel<TContext>): void{... }private onRawMessage(message: VSBuffer): void{... }private disposeActiveRequest(request: IRawRequest): void{... }private flushPendingRequests(channelName: string) :void{... }private sendResponse(response: IRawResponse): void{... }private send(header: any.body: any = undefined) :void{... }private sendBuffer(message: VSBuffer): void{... }private onPromise(request: IRawPromiseRequest): void{... }private collectPendingRequest(request: IRawPromiseRequest): void{... }Copy the code

Let’s explain one important set: activeRequests

  private readonly activeRequests = new Map<number, IDisposable>();
Copy the code

This Map stores active “service requests”, which are still in progress and are disposed uniformly if the client destroys (such as window closing)

 public dispose(): void {
    if (this.protocolListener) {
      this.protocolListener.dispose();
      this.protocolListener = null;
    }
    this.activeRequests.forEach(d= > d.dispose());
    this.activeRequests.clear();
  }
Copy the code

In addition to activeRequests, a protocol release protocol is set up when a connection is released: protocolListener

Next, we implement the “register channel” method

  registerChannel(
    channelName: string.channel: IServerChannel<TContext>): void {

    // Save the channel
    this.channels.set(channelName, channel);

    // If there are many requests before the channel is registered, the request is executed at this point.
    // https://github.com/microsoft/vscode/issues/72531
    setTimeout(() = > this.flushPendingRequests(channelName), 0);
  }

  private flushPendingRequests(channelName: string) :void {
    const requests = this.pendingRequests.get(channelName);

    if (requests) {
      for (const request of requests) {
        clearTimeout(request.timeoutTimer);

        switch (request.request.type) {
          case RequestType.Promise:
            this.onPromise(request.request);
            break;
          default:
            break; }}this.pendingRequests.delete(channelName); }}Copy the code

Next, we implement onRawMessage:

OnRawMessage Is used to process Buffer messages.

[core/common/ipc.ts]

  private onRawMessage(message: VSBuffer): void {
    // Read the Buffer message
    const reader = new BufferReader(message);
    // Unscramble headers:
    / / /
    // type indicates the message type
    // message id
    // channelName, the channelName
    // name Specifies the name of the service method
    // ]
    // deserialize is a tool method, read Buffer
    const header = deserialize(reader);
    // Unscramble the body of the message, which is the argument to execute the service method
    const body = deserialize(reader);
    const type = header[0] as RequestType;

    // Returns the execution result
    switch (type) {
      case RequestType.Promise:
        //
        return this.onPromise({
          type.id: header[1].channelName: header[2].name: header[3].arg: body,
        });
      case RequestType.PromiseCancel:
        return this.disposeActiveRequest({ type.id: header[1]});default:
        break; }}Copy the code

Where onPromise executes the service method and returns the result to the “client” to start accessing the specific service:

[core/common/ipc.ts]

private onPromise(request: IRawPromiseRequest): void {
    const channel = this.channels.get(request.channelName);
    // If the channel does not exist, put it into PendingRequest and wait for the channel to be executed after registration or cleared after expiration.
    if(! channel) {this.collectPendingRequest(request);
      return;
    }

    // Cancel request token -> Mechanism see cancelable Promise section
    const cancellationTokenSource = new CancellationTokenSource();
    let promise: Promise<any>;
    try {
      // Call the channel call to perform the specific service method
      promise = channel.call(
        this.ctx,
        request.name,
        request.arg,
        cancellationTokenSource.token,
      );
    } catch (err) {
      promise = Promise.reject(err);
    }

    const { id } = request;

    promise.then(
      data= > {
        // The execution result is displayed
        this.sendResponse(<IRawResponse>{
          id,
          data,
          type: ResponseType.PromiseSuccess,
        });
        // Clear the request from the active request
        this.activeRequests.delete(request.id);
      },
      err= > {
        // If there is an exception, handle the exception of the message and return the response result.
        if (err instanceof Error) {
          this.sendResponse(<IRawResponse>{
            id,
            data: {
              message: err.message,
              name: err.name,
              stack: err.stack
                ? err.stack.split
                  ? err.stack.split('\n')
                  : err.stack
                : undefined,},type: ResponseType.PromiseError,
          });
        } else {
          this.sendResponse(<IRawResponse>{
            id,
            data: err,
            type: ResponseType.PromiseErrorObj,
          });
        }

        this.activeRequests.delete(request.id); });// Stores requests to active requests and provides tokens that can be released.
    const disposable = toDisposable(() = > cancellationTokenSource.cancel());
    this.activeRequests.set(request.id, disposable);
  }
Copy the code

SendResponse returns a specific type of message based on the execution result; Send serializes the message to buffer. SendBuffer sends the message to the client.

[core/common/ipc.ts]

export enum ResponseType {
  Initialize = 200.// Initialize the message return
  PromiseSuccess = 201./ / promise success
  PromiseError = 202./ / promise to fail
  PromiseErrorObj = 203,
  EventFire = 204,}type IRawInitializeResponse = { type: ResponseType.Initialize };
type IRawPromiseSuccessResponse = {
  type: ResponseType.PromiseSuccess; / / type
  id: number; / / request id
  data: any; / / data
};
type IRawPromiseErrorResponse = {
  type: ResponseType.PromiseError;
  id: number;
  data: { message: string; name: string; stack: string[] | undefined };
};
type IRawPromiseErrorObjResponse = {
  type: ResponseType.PromiseErrorObj;
  id: number;
  data: any;
};

type IRawResponse =
  | IRawInitializeResponse
  | IRawPromiseSuccessResponse
  | IRawPromiseErrorResponse
  | IRawPromiseErrorObjResponse;

private sendResponse(response: IRawResponse): void {
    switch (response.type) {
      case ResponseType.Initialize:
        return this.send([response.type]);

      case ResponseType.PromiseSuccess:
      case ResponseType.PromiseError:
      case ResponseType.EventFire:
      case ResponseType.PromiseErrorObj:
        return this.send([response.type, response.id], response.data);
      default:
        break; }}private send(header: any.body: any = undefined) :void {
    const writer = new BufferWriter();
    serialize(writer, header);
    serialize(writer, body);
    this.sendBuffer(writer.buffer);
  }

  private sendBuffer(message: VSBuffer): void {
    try {
      this.protocol.send(message);
    } catch (err) {
      // noop}}Copy the code

If the request is cancelled, we do the following:

[core/common/ipc.ts]

  private disposeActiveRequest(request: IRawRequest): void {
    const disposable = this.activeRequests.get(request.id);

    if (disposable) {
      disposable.dispose();
      this.activeRequests.delete(request.id); }}Copy the code

The client that defines the channel

The channel client is used to send a request to the service and receive the result of the request:

First, we can define an interface to handle the returned result:

type IHandler = (response: IRawResponse) = > void;
Copy the code
export interface IChannelClient {
  getChannel<T extends IChannel>(channelName: string): T;
}
Copy the code
  export class ChannelClient implements IChannelClient.IDisposable {
    private protocolListener: IDisposable | null;

    private state: State = State.Uninitialized; // Channel status

    private lastRequestId = 0; // Communication request unique ID management

    // Active request, which is used to close when canceling; If the channel is closed (Dispose), Unified sends a cancel message to all channels to ensure the reliability of communication.
    private readonly activeRequests = new Set<IDisposable>();

    private readonly handlers = new Map<number, IHandler>(); // Process the result after communication

    private readonly _onDidInitialize = new Emitter<void> ();// An event is raised when a channel is initialized
	readonly onDidInitialize = this._onDidInitialize.event;

    constructor(private readonly protocol: IMessagePassingProtocol)    {
      this.protocolListener =
        this.protocol.onMessage(msg= > this.onBuffer(msg)); }}Copy the code
enum State {
  Uninitialized, // Not initialized
  Idle, / / ready
}

private state: State = State.Uninitialized;
Copy the code

Channel client has two states, one is “uninitialized”, or “ready”. Uninitialized means the channel server is not ready. When it is ready, the _onDidInitialize event is triggered to update the channel state.

    constructor(
      private readonly protocol: IMessagePassingProtocol) {
      this.protocolListener =
        this.protocol.onMessage(msg= > this.onBuffer(msg));
    }
Copy the code

When the Channel client is initialized, it listens for messages from the Channel server. When the Channel server is ready, it listens for ready messages sent by the channel server.

OnBuffer is read Buffer message; OnResponse performs message processing based on the interpreted message and returns to the place where it was called.

    private onBuffer(message: VSBuffer): void {
      const reader = new BufferReader(message);
      const header = deserialize(reader);
      const body = deserialize(reader);
      const type: ResponseType = header[0];

      switch (type) {
        case ResponseType.Initialize:
          return this.onResponse({ type: header[0]});case ResponseType.PromiseSuccess:
        case ResponseType.PromiseError:
        case ResponseType.EventFire:
        case ResponseType.PromiseErrorObj:
          return this.onResponse({ type: header[0].id: header[1].data: body }); }}private onResponse(response: IRawResponse): void {

      // Channel server ready message processing
      if (response.type === ResponseType.Initialize) {
        this.state = State.Idle;
        this._onDidInitialize.fire();
        return;
      }

      // Channel server for message processing and return
      const handler = this.handlers.get(response.id);

      if(handler) { handler(response); }}Copy the code

Before making a request, the Channel server constructs a channel structure to send a message.

Why construct a channel to send messages instead of sending them directly? Leave a doubt.

    getChannel<T extends IChannel>(channelName: string): T {
      const that = this;
      return {
        call(
          command: string.// Service method namearg? :any./ / parameterscancellationToken? : CancellationToken) { / / cancel
          return that.requestPromise(
            channelName,
            command,
            arg,
            cancellationToken,
          );
        },
        listen(event: string, arg: any) {
          // TODO
          // return that.requestEvent(channelName, event, arg);}},as T;
    }
Copy the code

RequestPromise is to initiate a service invocation request:

private requestPromise(
    channelName: string.name: string, arg? :any,
    cancellationToken = CancellationToken.None,
  ): Promise<any> {
    const id = this.lastRequestId++;
    const type = RequestType.Promise;
    const request: IRawRequest = { id, type, channelName, name, arg };

    // If the request is cancelled, it will not be executed.
    if (cancellationToken.isCancellationRequested) {
      return Promise.reject(canceled());
    }

    let disposable: IDisposable;

    const result = new Promise((c, e) = > {
      // If the request is cancelled, it will not be executed.
      if (cancellationToken.isCancellationRequested) {
        return e(canceled());
      }

      // The channel will be queued until the channel is registered
      // When the Channel server is ready, a ready message is sent back, which triggers the state to change to Idle
      / / which will trigger uninitializedPromise. Then
      // So that messages can be sent
      let uninitializedPromise: CancelablePromise<
        void
      > | null = createCancelablePromise(_= > this.whenInitialized());
      uninitializedPromise.then(() = > {
        uninitializedPromise = null;

        const handler: IHandler = response= > {
          console.log(
            'main process response:'.JSON.stringify(response, null.2));// Depending on the type of result returned, Initialize is not handled here, which is handled higher
          switch (response.type) {
            case ResponseType.PromiseSuccess:
              this.handlers.delete(id);
              c(response.data);
              break;

            case ResponseType.PromiseError:
              this.handlers.delete(id);
              const error = new Error(response.data.message);
              (<any>error).stack = response.data.stack;
              error.name = response.data.name;
              e(error);
              break;

            case ResponseType.PromiseErrorObj:
              this.handlers.delete(id);
              e(response.data);
              break;
            default:
              break; }};// Save the processing of this request
        this.handlers.set(id, handler);

        // Start sending requests
        this.sendRequest(request);
      });

      const cancel = () = > {
        // If not initialized, cancel
        if (uninitializedPromise) {
          uninitializedPromise.cancel();
          uninitializedPromise = null;
        } else {
        // If already initialized and on request, an interrupt message is sent
          this.sendRequest({ id, type: RequestType.PromiseCancel });
        }

        e(canceled());
      };

      const cancellationTokenListener = cancellationToken.onCancellationRequested(
        cancel,
      );
      disposable = combinedDisposable(
        toDisposable(cancel),
        cancellationTokenListener,
      );
      // Save the request to the active request
      this.activeRequests.add(disposable);
    });
    // Remove the request from the active request after execution
    return result.finally(() = > this.activeRequests.delete(disposable));
  }
Copy the code

The method of sending a message is the same as the method of receiving a message, and is not redundant:

private sendRequest(request: IRawRequest): void {
  switch (request.type) {
    case RequestType.Promise:
    return this.send(
      [request.type, request.id, request.channelName, request.name],
      request.arg,
    );

  case RequestType.PromiseCancel:
    return this.send([request.type, request.id]);
    default:
      break; }}private send(header: any.body: any = undefined) :void {
  const writer = new BufferWriter();
  serialize(writer, header);
  serialize(writer, body);
  this.sendBuffer(writer.buffer);
}

private sendBuffer(message: VSBuffer): void {
  try {
    this.protocol.send(message);
  } catch (err) {
    // noop}}Copy the code

Define a Connection: Connection

According to the above design drawing, as follows:

export interface Client<TContext> {
  readonly ctx: TContext;
}
export interface Connection<TContext> extends Client<TContext> {
  readonly channelServer: ChannelServer<TContext>; // Channel server
  readonly channelClient: ChannelClient; // Channel client
}
Copy the code

Define the server: IPCServer

class IPCServer<TContext = string>
  implements
    IChannelServer<TContext>,
    IDisposable {

    // Channel accessible to the server side
    private readonly channels = new Map<string, IServerChannel<TContext>>();

    // Connection between client and server
    private readonly _connections = new Set<Connection<TContext>>();

    private readonly _onDidChangeConnections = new Emitter<
      Connection<TContext>
    >();

    // An event listener is triggered when a connection changes
    readonly onDidChangeConnections: Event<Connection<TContext>> = this
      ._onDidChangeConnections.event;

    // All connections
    get connections() :Array<Connection<TContext> > {const result: Array<Connection<TContext>> = [];
      this._connections.forEach(ctx= > result.push(ctx));
      return result;
    }
    // Release all listeners
    dispose(): void {
      this.channels.clear();
      this._connections.clear();
      this._onDidChangeConnections.dispose(); }}Copy the code

Earlier we mentioned “message communication protocol”, namely:

  • Send and receive messages on the ‘IPC: Message’ channel
  • Send the ‘IPC: Hello’ channel message to start the link
  • When disconnecting, send a message to the ‘IPC: Disconnect’ channel

You may have a question, why our communication message also need to establish a connection, this means a long connection?

In fact, it is not long connection, or one-time communication, in fact, the process is like this:

  • At first the renderer sends the ‘IPC :hello’ message, with the intention that subsequent communication may occur at’ IPC :message’. Please prepare the main process.
  • The main process receives the ‘IPC: Hello’ message, discovers that it is the renderer that needs the ‘IPC :message’ channel for the first time, and starts listening for it.
  • When the render process unloads, an ‘IPC :disconnect’ message is emitted.
  • The main process received the ‘IPC: Disconnect’ message from the renderer process. Disable listening on ‘IPC :message’.

One thing to note here is that ‘IPC :hello’ is actually a listener set up when the main process IPCServer is instantiated to see what communication each renderer needs.

Thus, the complete flow of this communication mechanism actually looks like this:

TODO

The renderer communicates with the main process via ‘IPC :message’, so the message sent to window A will also be received by window B. Of course not! , please listen to the following explanation. The above figure is a simple demonstration, but of course there is a retry mechanism for connections.

Next, let’s start implementing the above process.

First, define a client connection event interface:

export interface ClientConnectionEvent {
  protocol: IMessagePassingProtocol; // Message communication protocol
  onDidClientDisconnect: Event<void>; // Disconnect event
}

Copy the code

Next, we implement a method that listens for client connections, getOnDidClientConnect

export class IPCServer<TContext = string>
  implements
    IChannelServer<TContext>,
    IDisposable {
    private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
      const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
        ipcMain,
        'ipc:hello'.({ sender }) = >sender, ); . }constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
      onDidClientConnect(({ protocol, onDidClientDisconnect }) = >{... }}}Copy the code

From the flowchart, we know that when IPCServer is instantiated, we register the ‘IPC: Hello’ message listener, which we can receive as a signal to establish a connection.

If I send multiple onHello messages, I’m connected multiple times, right? Of course not. Listen to the following analysis.

First, a brief explanation of this sentence, which will be explained in more detail in the upcoming vscode event mechanism:

const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
    ipcMain,
    'ipc:hello'.({ sender }) = > sender,
);
Copy the code

Ipc :hello = ipc:hello = ipc:hello = ipc:hello

const handler = (e) = > {
  console.log('sender:',  e.sender.id);
};
ipcMain.on('ipc:hello', handler)

// Remove the listener
ipcMain.removeListener('ipc:hello', handler);
Copy the code

And here it is:

const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
    ipcMain,
    'ipc:hello'.({ sender }) = > sender,
);
const listener = onHello((sender) = > {
  console.log('sender');
});

// Remove the listener
listener.dispose();
Copy the code

Written as has many benefits, and said the Event. How fromNodeEventEmitter implementation, will continue to update later on.

So let’s go ahead and implement getOnDidClientConnect()

export class IPCServer<TContext = string>
  implements
    IChannelServer<TContext>,
    IDisposable {
    private static getOnDidClientConnect(): Event<ClientConnectionEvent> {
      const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
        ipcMain,
        'ipc:hello'.({ sender }) = > sender,
      );
      return Event.map(onHello, webContents= > {
        const{ id } = webContents; .const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event<
          VSBuffer
        >;
        const onDidClientDisconnect = Event.any(
          Event.signal(createScopedOnMessageEvent(id, 'ipc:disconnect')),
          onDidClientReconnect.event,
        );
        const protocol = new Protocol(webContents, onMessage);
        return{ protocol, onDidClientDisconnect }; }); }... }Copy the code

As we explained earlier, after we define onHello, we can listen for events like this:

const listener = onHello((sender) = > {
  console.log('sender');
});
Copy the code

As you can see, the message argument for all ipc: Hello messages is filtered to sender instead of e. And:

getOnDidClientConnect(): Event<ClientConnectionEvent> {
  return Event.map(onHello, webContents= >{...return{ protocol, onDidClientDisconnect }; })}Copy the code

Filter onHello to {protocol, onDidClientDisconnect}. This is equivalent to using decorator mode to decorate the parameters again on the onHello event

Maybe it’s a little confusing, but let me compare it horizontally, after two decorations

// For the first time, the event argument e becomes the sender argument
const onHello = Event.fromNodeEventEmitter<Electron.WebContents>(
  ipcMain,
  'ipc:hello'.({ sender }) = > sender,
);

// The second decorator event parameter sender(webContents) becomes {protocol, onDidClientDisconnect}
getOnDidClientConnect(): Event<ClientConnectionEvent> {
  return Event.map(onHello, webContents= >{...return{ protocol, onDidClientDisconnect }; })}Copy the code

From the original:

const handler = (e) = > {
  console.log('sender:', e.sender.id);
};
ipcMain.on('ipc:hello', handler)

// Remove the listener
ipcMain.removeListener('ipc:hello', handler);
Copy the code

It becomes:


// Still ipc: Hello event, only {protocol, onDidClientDisconnect} instead of the message parameter e
const onDidClientConnnect = getOnDidClientConnect();
const listener = onDidClientConnnect(({protocol, onDidClientDisconnect}) = >{... });// Remove the listener
listener.dispose();

Copy the code

Ok, so let’s look at {protocol, onDidClientDisconnect}.

Protocol, the communication protocol we defined above, contains:

  • Sender is the interface that sends the object, as long as there is a method: send.
  • The agreement states:
    • Send the ‘IPC: Hello’ channel message to start the link
    • Send and receive messages on the ‘IPC: Message’ channel
    • When disconnecting, send a message to the ‘IPC: Disconnect’ channel

Therefore:

  const onMessage = createScopedOnMessageEvent(id, 'ipc:message') as Event<
    VSBuffer
  >;
  const protocol = new Protocol(webContents, onMessage);
Copy the code
  • WebContents is sender: the object that sends the message.
  • OnMessage is the method that listens for messages on the IPC: Message channel

The onMessage here can simply be understood as listening via encapsulated messages, from the original:

ipcMain.on('ipc:message'.(e, message) = > {
  console.log('message:', message);
})
Copy the code

Becomes:

onMessage((message) = > {
  console.log('message:', message);
})
Copy the code
  • One notable feature is that the event parameter is changed from (e, message) to (message)
  • Messages are compressed using buffer, which is part of the communication optimization.
  • In addition to this, of course, createScopedOnMessageEvent another ability, is the filter, the front have mentioned, all the rendering process, all through

Ipc: Message channel communication, how to avoid the message sent to render process A, render process B also received, is filtered here, after filtering, render process A will only receive messages to A.

OnDidClientDisconnect again, ‘IPC: Disconnect’ message listener, I won’t explain.

How does IPCServer register ipc: Hello listener

class IPCServer<TContext = string>
  implements
    IChannelServer<TContext>,
    IDisposable {
      constructor(onDidClientConnect: Event<ClientConnectionEvent>) {
        onDidClientConnect(({ protocol, onDidClientDisconnect }) = > {
          const onFirstMessage = Event.once(protocol.onMessage);
          // Receive the message for the first time
          onFirstMessage(msg= > {
            const reader = new BufferReader(msg);
            const ctx = deserialize(reader) as TContext; // Further explanation

            const channelServer = new ChannelServer(protocol, ctx);
            const channelClient = new ChannelClient(protocol);

            this.channels.forEach((channel, name) = >
              channelServer.registerChannel(name, channel),
            );

            const connection: Connection<TContext> = {
              channelServer,
              channelClient,
              ctx,
            };
            this._connections.add(connection);
            // this._onDidChangeConnections.fire(connection);

            onDidClientDisconnect(() = > {
              channelServer.dispose();
              channelClient.dispose();
              this._connections.delete(connection); }); })}}}Copy the code

When we first receive ipc:message, we create a new connection: Connection; When receiving: IPC :disconnect, delete the connection and remove the listener.

The key method here is to register channels, as shown in the design above. For each new channel, change channels are added to the existing connection:

registerChannel(
  channelName: string.channel: IServerChannel<TContext>,
): void {
  this.channels.set(channelName, channel);

  // At the same time in all connections, you need to register channels
  this._connections.forEach(connection= > {
    connection.channelServer.registerChannel(channelName, channel);
  });
}
Copy the code

With the server ready, we need to implement the client details:

export class IPCClient<TContext = string>
  implements IChannelClient, IChannelServer<TContext>, IDisposable {
  private readonly channelClient: ChannelClient;

  private readonly channelServer: ChannelServer<TContext>;

  constructor(protocol: IMessagePassingProtocol, ctx: TContext) {

    const writer = new BufferWriter();
    serialize(writer, ctx);
    // Send the service registration message CTX, the service name.
    protocol.send(writer.buffer);

    this.channelClient = new ChannelClient(protocol);
    this.channelServer = new ChannelServer(protocol, ctx);
  }

  getChannel<T extends IChannel>(channelName: string): T {
    return this.channelClient.getChannel(channelName);
  }

  registerChannel(
    channelName: string.channel: IServerChannel<TContext>,
  ): void {
    // Register channels
    this.channelServer.registerChannel(channelName, channel);
  }

  dispose(): void {
    this.channelClient.dispose();
    this.channelServer.dispose(); }}Copy the code
export class Client extends IPCClient implements IDisposable {
  private readonly protocol: Protocol;

  private static createProtocol(): Protocol {
    const onMessage = Event.fromNodeEventEmitter<VSBuffer>(
      ipcRenderer,
      'ipc:message'.(_, message: Buffer) = > VSBuffer.wrap(message),
    );
    ipcRenderer.send('ipc:hello');
    return new Protocol(ipcRenderer, onMessage);
  }

  constructor(id: string) {
    const protocol = Client.createProtocol();
    super(protocol, id);
    this.protocol = protocol;
  }

  dispose(): void {
    this.protocol.dispose(); }}Copy the code