Multiple application instances in electron will have indexedDB multiple open exceptions due to session sharing. Roughly, you will encounter an error like the following.

1
DOMException: Internal error opening backing store for indexedDB.open.

When indexedDB is used in an application, this is a problem that will definitely be faced and needs to be solved.

  • If it is not necessary, you can detect and disable multiple application sessions at startup.
  • If there is a need for multiple applications, resource isolation can be achieved by detecting multiple applications and setting up separate sessions.

Single-instance mode: disable electron application multi-segmentation

In most cases, you can create multiple windows to satisfy most of your application’s simulation needs. This is the official recommendation of electron, and an example of a solution to keep the application in singleton mode is given. Example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
improt { app } from 'electron';
let myWindow = null;
 
// 请求获取实例锁,若成功则返回 true,否则表示已存在打开的应用实例
const gotTheLock = app.requestSingleInstanceLock();
 
if (!gotTheLock) {
  app.quit();
} else {
  app.on('second-instance', (event, commandLine, workingDirectory) => {
    // 当运行第二个实例时退出它,并聚焦到 myWindow 窗口
    if (myWindow) {
      if (myWindow.isMinimized()) myWindow.restore();
      myWindow.focus();
    }
  })
 
  app.whenReady().then(() => {
    myWindow = createWindow()
  })
}

Related APIs are:

Use separate sessions to allow multiple openings for electron applications

In our real-world application, which allows users to open multiple clients to log in to different accounts, indexedDB is used to cache large amounts of underlying data to reduce memory usage and reduce GC pressure. Cached data persistence is not necessary in this case. The data that needs to be persisted locally is not too much and can be achieved by creating different local files for different users with encrypted storage.

When creating a BrowserWindow, electron can use a different session by setting the session or partition parameter. session has a higher priority than partition, but partition specifies a string in a more concise way. When the value of partition starts with persist:, the created session is persisted in the app.getPath('userData')/Partitions directory, otherwise it is created in memory only.

One thing to note is that only BrowserWindow with the same partition can share session (various stores, MessageChannel, etc. can only share and communicate with each other), so when creating multiple different windows, if you need to share session with each other, you need to set the same parameter value.

So we can implement the basic scheme of electron application multiplication in the following way.

  • detect if there is an application multiplier
  • If there is a multi-partition, set a different partition value when creating the form to use a separate session

Methods for detecting whether an application is multipartitioned

Use the wmic command on Windows and the ps command on Linux and Mac OS to get information about all running applications on the system. By using the pid and ppid information of the application process, you can identify whether the application has multiple openings. Example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**
  * 获取应用全部的 pid 与 ppid 与 pid 对应列表
  */
function getAppPids(appExecName?: string) {
    const pidToPpid = {} as { [pid: number]: number };
 
    try {
        if (!appExecName) appExecName = path.basename(process.execPath);
        const isWin = process.platform === 'win32';
        const cmd = isWin ? `wmic process where name="${appExecName}" GET processId,parentProcessId` : `ps -ef`;
        const str = execSync(cmd, { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }).trim();
 
        if (isWin) {
            str.split('\n')
                .map(line => line.trim().split(/\s+/))
                .slice(1)
                .forEach(line => (pidToPpid[+line[1]] = +line[0]));
        } else {
            str.split('\n')
                .filter(line => line.includes(appExecName))
                .map(line => line.trim().split(/\s+/))
                .forEach(line => (pidToPpid[+line[1]] = +line[0]));
        }
    } catch (err) {
        logger.info('[process][getAppPids][error]', appExecName, err.message);
    }
 
    return pidToPpid;
},
/**
  * 当前是否为应用多开
  *
  * @param {boolean} [isPrintDetail=true] 是否打印所有进程详情
  */
function isMultiOpen() {
    const appName = path.basename(process.execPath).toLowerCase();
    const pids = this.getAppPids(appName);
    const app = electron.app || electron.remote.app;
    const appMetrics = app.getAppMetrics().map(d => [d.pid, d.type] as const);
    const isMultiOpen = Object.keys(pids).length > appMetrics.length;
    const info = { appName, isMultiOpen, pids, appMetrics };
 
    logger.info('[process]isMultiOpen', info);
 
    return info;
}

Since electron itself provides the app.requestSingleInstanceLock API, you can also simply use it to determine if the current start is a multiple start.

1
2
3
function isMultiOpen() {
    return !app.requestSingleInstanceLock();
}

Setting up separate sessions for multiple applications

If the data stored in indexedDB needs to be persistent in order to optimize data loading performance for secondary starts, you can specify a separate persistent session for each instance, otherwise it can be created as an in-memory temporary session.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const options: Electron.BrowserWindowConstructorOptions = {
    useContentSize: true,
    webPreferences: {
        nodeIntegration: true,
        devTools: IS_DEV,
        nodeIntegrationInWorker: true,
        webviewTag: true,
    },
};
 
/** 获取可复用的 partition ID */
function getPersisitId() {}
 
if (isMultiOpen) {
  // session 持久化,主要注意可复用逻辑、避免随机创建过多持久化数据导致磁盘占用膨胀
  opts.webPreferences.partition = `persist:part${getPersisitId()}`;
  // 创建为内存中的临时 session
  // opts.webPreferences.partition = `persist_${process.pid}`;
}
const mainWindow = new BrowserWindow(options);

Note that the value of the partition parameter should be the same when creating multiple BrowserWindow.