Skip to main content

Architecture

The application is built with Electron.
Before continuing, reading the process model documentation is highly recommended to understand the different Electron processes.

Because it's easy to make an Electron application unresponsive by blocking either the main process or the renderer process, the application forks a Node.js process, which hosts a WebSocket server. This process does almost all the heavy work, such as database/filesystem access, demo analysis, etc.
It communicates with the main and renderer process through their WebSocket clients.

The following diagram shows the creation of essential components of the application when it starts:

As you can see, the WebSocket server is created from a BrowserWindow instead of a Node.js process during development only.
It gives us a dedicated window to debug the WebSocket server using the Chrome DevTools.

JavaScript bundles

The code of the application is split into several JavaScript bundles:

  • The WebSocket server bundle
  • The Electron main process bundle
  • The Electron renderer process (UI) bundle
  • The preload script bundle
  • The CLI bundle

The renderer process doesn't have access to any Node.js API or Electron main modules.
To expose the minimum amount of APIs to the renderer process, a preload script is loaded when the BrowserWindow is created.

The following table shows which APIs are available in each bundle:

BundleAPIs
WebSocket serverNode.js
Main processNode.js, Electron main modules
PreloadNode.js, Electron renderer modules
Renderer processWEB APIs, Preload script APIs
CLINode.js

Communication

Main/Renderer process to WebSocket server

The main and renderer process use their WebSocket client to communicate with the WebSocket server.

Messages are JSON objects with the following shape:

type Message = {
name: string;
payload: unknown;
};

Main process to renderer process

Sending messages from the main process to the renderer process is done using the WebContents instance of the BrowserWindow as described here.
It's helpful to update the UI from the main process.

Example:

preload.js
import { ipcRenderer, contextBridge } from 'electron';

contextBridge.exposeInMainWorld('csdm', {
onShowSettings: (callback: () => void) => {
ipcRenderer.addListener('show-settings', callback);

return () => {
ipcRenderer.removeListener('show-settings', callback);
};
},
});
renderer.js
const unListen = window.csdm.onShowSettings(() => {
console.log('Show settings message received!');
});
unListen();
main.js
// mainWindow is the BrowserWindow instance
mainWindow.webContents.send('show-settings');

Renderer process to main process

Sending messages from the renderer process to the main process is done using ipcRenderer.invoke/ipcMain.handle functions described here.
It's helpful to call Electron main modules (app, dialog, etc.) from the renderer process.

Example:

preload.js
import { ipcRenderer, contextBridge } from 'electron';

contextBridge.exposeInMainWorld('csdm', {
isAppHidden: () => {
ipcRenderer.invoke('is-app-hidden');
},
});
main.js
import { ipcMain, app } from 'electron';

ipcMain.handle('is-app-hidden', () => {
return app.isHidden();
});
renderer.js
<button
onClick={() => {
const isHidden = window.csdm.isAppHidden();
console.log(isHidden);
}}
>
Check app hidden
</button>