├── .npmrc ├── test └── fixture │ ├── package.json │ ├── index.html │ ├── util.js │ ├── renderer.js │ ├── test.js │ └── index.js ├── .gitattributes ├── .gitignore ├── index.js ├── .editorconfig ├── .github └── workflows │ └── main.yml ├── source ├── util.js ├── renderer.js └── main.js ├── license ├── package.json ├── index.test-d.ts ├── index.d.ts └── readme.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /test/fixture/package.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /test/fixture/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.type === 'renderer') { 4 | module.exports.ipcRenderer = require('./source/renderer.js'); 5 | } else { 6 | module.exports.ipcMain = require('./source/main.js'); 7 | } 8 | -------------------------------------------------------------------------------- /test/fixture/util.js: -------------------------------------------------------------------------------- 1 | const countDataAndErrorListeners = emitter => 2 | emitter.eventNames().filter(name => /(data|error)-channel/.test(name)).length; 3 | 4 | module.exports.countDataAndErrorListeners = countDataAndErrorListeners; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} 8 | runs-on: macos-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 16 14 | - 14 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: actions/setup-node@v2 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm test 22 | -------------------------------------------------------------------------------- /source/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getUniqueId = () => `${Date.now()}-${Math.random()}`; 4 | 5 | const getSendChannel = channel => `%better-ipc-send-channel-${channel}`; 6 | const getRendererSendChannel = channel => `%better-ipc-send-channel-${channel}`; 7 | 8 | module.exports.currentWindowChannel = '%better-ipc-current-window'; 9 | 10 | module.exports.getSendChannel = getSendChannel; 11 | module.exports.getRendererSendChannel = getRendererSendChannel; 12 | 13 | module.exports.getResponseChannels = channel => { 14 | const id = getUniqueId(); 15 | return { 16 | sendChannel: getSendChannel(channel), 17 | dataChannel: `%better-ipc-response-data-channel-${channel}-${id}`, 18 | errorChannel: `%better-ipc-response-error-channel-${channel}-${id}` 19 | }; 20 | }; 21 | 22 | module.exports.getRendererResponseChannels = channel => { 23 | const id = getUniqueId(); 24 | return { 25 | sendChannel: getRendererSendChannel(channel), 26 | dataChannel: `%better-ipc-response-data-channel-${channel}-${id}`, 27 | errorChannel: `%better-ipc-response-error-channel-${channel}-${id}` 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-better-ipc", 3 | "version": "2.0.1", 4 | "description": "Simplified IPC communication for Electron apps", 5 | "license": "MIT", 6 | "repository": "sindresorhus/electron-better-ipc", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "scripts": { 14 | "test": "xo && tsd", 15 | "test-with-app": "xo && (cd test/fixture && ava) && tsd" 16 | }, 17 | "files": [ 18 | "index.js", 19 | "index.d.ts", 20 | "source" 21 | ], 22 | "keywords": [ 23 | "electron", 24 | "ipc", 25 | "communication", 26 | "communicate", 27 | "app", 28 | "message", 29 | "messages", 30 | "send", 31 | "receive", 32 | "call", 33 | "answer", 34 | "reply", 35 | "async", 36 | "await" 37 | ], 38 | "dependencies": { 39 | "serialize-error": "^8.1.0" 40 | }, 41 | "devDependencies": { 42 | "ava": "^2.2.0", 43 | "electron": "^10", 44 | "execa": "^5.0.0", 45 | "tsd": "^0.14", 46 | "xo": "^0.39.1" 47 | }, 48 | "xo": { 49 | "envs": [ 50 | "node", 51 | "browser" 52 | ], 53 | "ignores": [ 54 | "test/fixture" 55 | ] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /source/renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const electron = require('electron'); 3 | const {serializeError, deserializeError} = require('serialize-error'); 4 | const util = require('./util.js'); 5 | 6 | const {ipcRenderer} = electron; 7 | const ipc = Object.create(ipcRenderer || {}); 8 | 9 | ipc.callMain = (channel, data) => new Promise((resolve, reject) => { 10 | const {sendChannel, dataChannel, errorChannel} = util.getResponseChannels(channel); 11 | 12 | const cleanup = () => { 13 | ipcRenderer.off(dataChannel, onData); 14 | ipcRenderer.off(errorChannel, onError); 15 | }; 16 | 17 | const onData = (_event, result) => { 18 | cleanup(); 19 | resolve(result); 20 | }; 21 | 22 | const onError = (_event, error) => { 23 | cleanup(); 24 | reject(deserializeError(error)); 25 | }; 26 | 27 | ipcRenderer.once(dataChannel, onData); 28 | ipcRenderer.once(errorChannel, onError); 29 | 30 | const completeData = { 31 | dataChannel, 32 | errorChannel, 33 | userData: data 34 | }; 35 | 36 | ipcRenderer.send(sendChannel, completeData); 37 | }); 38 | 39 | ipc.answerMain = (channel, callback) => { 40 | const sendChannel = util.getRendererSendChannel(channel); 41 | 42 | const listener = async (_event, data) => { 43 | const {dataChannel, errorChannel, userData} = data; 44 | 45 | try { 46 | ipcRenderer.send(dataChannel, await callback(userData)); 47 | } catch (error) { 48 | ipcRenderer.send(errorChannel, serializeError(error)); 49 | } 50 | }; 51 | 52 | ipcRenderer.on(sendChannel, listener); 53 | 54 | return () => { 55 | ipcRenderer.off(sendChannel, listener); 56 | }; 57 | }; 58 | 59 | module.exports = ipc; 60 | -------------------------------------------------------------------------------- /test/fixture/renderer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const {ipcRenderer} = require('electron'); 3 | const {ipcRenderer: ipc} = require('../..'); 4 | const {countDataAndErrorListeners} = require('./util'); 5 | 6 | ipcRenderer.once('count', () => ipcRenderer.send('log', 'test-count-renderer-listeners: ' + countDataAndErrorListeners(ipcRenderer))); 7 | 8 | ipc.callMain('test', 'optional-data').then(answer => { 9 | ipcRenderer.send('log', 'test:renderer:answer-from-main: ' + answer); 10 | }); 11 | 12 | ipc.answerMain('test', data => { 13 | ipcRenderer.send('log', 'test:renderer:data-from-main: ' + data); 14 | return 'test:renderer:answer-data'; 15 | }); 16 | 17 | ipc.callMain('test-error').catch(error => { 18 | ipcRenderer.send('log', 'test-error:renderer:from-main:is-error ' + (error instanceof Error)); 19 | ipcRenderer.send('log', 'test-error:renderer:from-main:error-message ' + error.message); 20 | }); 21 | 22 | ipc.callMain('test-focused', 'optional-data').then(answer => { 23 | ipcRenderer.send('log', 'test-focused:renderer:answer-from-main: ' + answer); 24 | }); 25 | 26 | ipc.callMain('test-concurrency', 'data-1').then(answer => { 27 | ipcRenderer.send('log', 'test-concurrency:renderer:answer-from-main-1: ' + answer); 28 | }); 29 | 30 | ipc.callMain('test-concurrency', 'data-2').then(answer => { 31 | ipcRenderer.send('log', 'test-concurrency:renderer:answer-from-main-2: ' + answer); 32 | }); 33 | 34 | ipc.answerMain('test-focused', data => { 35 | ipcRenderer.send('log', 'test-focused:renderer:data-from-main: ' + data); 36 | return 'test-focused:renderer:answer-data'; 37 | }); 38 | 39 | ipc.callMain('test-specific-window', 'data-1').then(answer => { 40 | ipcRenderer.send('log', 'test-specific-window:renderer:answer-from-main: ' + answer); 41 | }); 42 | -------------------------------------------------------------------------------- /test/fixture/test.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import test from 'ava'; 3 | import execa from 'execa'; 4 | 5 | const run = async file => { 6 | const {stdout} = await execa(electron, [file], { 7 | timeout: 10000 8 | }); 9 | 10 | return stdout.trim(); 11 | }; 12 | 13 | test('main', async t => { 14 | const stdout = await run('index.js'); 15 | 16 | const logs = [ 17 | ...stdout.split('\n') 18 | ].filter(x => 19 | x !== '' 20 | ).sort(); 21 | 22 | console.log(logs); 23 | 24 | t.deepEqual(logs, [ 25 | 'test-concurrency:main:data-from-renderer: data-1', 26 | 'test-concurrency:main:data-from-renderer: data-2', 27 | 'test-concurrency:renderer:answer-from-main-1: test-concurrency:main:answer:data-1', 28 | 'test-concurrency:renderer:answer-from-main-2: test-concurrency:main:answer:data-2', 29 | 'test-count-main-listeners: 0', 30 | 'test-count-renderer-listeners: 0', 31 | 'test-error:renderer:from-main:error-message test-error:main:answer', 32 | 'test-error:renderer:from-main:is-error true', 33 | 'test-focused:main:answer-from-renderer: test-focused:renderer:answer-data', 34 | 'test-focused:main:data-from-renderer: optional-data', 35 | 'test-focused:main:error-from-renderer: No browser window in focus', 36 | 'test-focused:renderer:answer-from-main: test-focused:main:answer', 37 | 'test-focused:renderer:data-from-main: optional-data', 38 | 'test-specific-window:main:data-from-renderer: data-1', 39 | 'test-specific-window:renderer:answer-from-main: test-specific-window:main:answer:data-1', 40 | 'test:main:answer-from-renderer: test:renderer:answer-data', 41 | 'test:main:data-from-renderer: optional-data', 42 | 'test:main:error-from-renderer: Browser window required', 43 | 'test:renderer:answer-from-main: test:main:answer', 44 | 'test:renderer:data-from-main: optional-data' 45 | ]); 46 | }); 47 | -------------------------------------------------------------------------------- /test/fixture/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const {app, BrowserWindow, ipcMain} = require('electron'); 4 | const {ipcMain: ipc} = require('../../index.js'); 5 | const {countDataAndErrorListeners} = require('./util.js'); 6 | 7 | let countOfLogs = 0; 8 | 9 | ipcMain.on('log', (_event, log) => { 10 | console.log(log); 11 | countOfLogs++; 12 | }); 13 | 14 | ipc.answerRenderer('test', async data => { 15 | console.log('test:main:data-from-renderer:', data); 16 | return 'test:main:answer'; 17 | }); 18 | 19 | ipc.answerRenderer('test-focused', async data => { 20 | console.log('test-focused:main:data-from-renderer:', data); 21 | return 'test-focused:main:answer'; 22 | }); 23 | 24 | ipc.answerRenderer('test-error', async () => { 25 | throw new Error('test-error:main:answer'); 26 | }); 27 | 28 | ipc.answerRenderer('test-concurrency', async data => { 29 | console.log('test-concurrency:main:data-from-renderer:', data); 30 | return `test-concurrency:main:answer:${data}`; 31 | }); 32 | 33 | (async () => { 34 | await app.whenReady(); 35 | 36 | const mainWindow = new BrowserWindow({ 37 | webPreferences: { 38 | nodeIntegration: true 39 | } 40 | }); 41 | 42 | ipc.answerRenderer(mainWindow, 'test-specific-window', async data => { 43 | console.log('test-specific-window:main:data-from-renderer:', data); 44 | return `test-specific-window:main:answer:${data}`; 45 | }); 46 | 47 | await mainWindow.loadFile(path.join(__dirname, 'index.html')); 48 | 49 | const answer = await ipc.callRenderer(mainWindow, 'test', 'optional-data'); 50 | console.log('test:main:answer-from-renderer:', answer); 51 | 52 | const answerFromFocusedRenderer = await ipc.callFocusedRenderer('test-focused', 'optional-data'); 53 | console.log('test-focused:main:answer-from-renderer:', answerFromFocusedRenderer); 54 | 55 | try { 56 | await ipc.callRenderer(); 57 | } catch (error) { 58 | console.log('test:main:error-from-renderer:', error.message); 59 | } 60 | 61 | try { 62 | mainWindow.blur(); 63 | mainWindow.hide(); 64 | await ipc.callFocusedRenderer(); 65 | } catch (error) { 66 | console.log('test-focused:main:error-from-renderer:', error.message); 67 | } 68 | 69 | // Get the count of listeners from the renderer. 70 | mainWindow.webContents.send('count'); 71 | 72 | console.log('test-count-main-listeners:', countDataAndErrorListeners(ipcMain)); 73 | 74 | // Wait to get all logs from the renderer and then quit the app. 75 | setInterval(() => { 76 | if (countOfLogs === 10) { 77 | app.quit(); 78 | } 79 | }, 100); 80 | })(); 81 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import {expectType, expectError} from 'tsd'; 3 | import {BrowserWindow} from 'electron'; 4 | import {ipcMain, ipcRenderer} from './index.js'; 5 | 6 | const browserWindow = BrowserWindow.getFocusedWindow()!; // eslint-disable-line @typescript-eslint/no-non-null-assertion 7 | 8 | // IpcMain 9 | 10 | expectType>( 11 | ipcMain.callRenderer(browserWindow, 'get-emoji') 12 | ); 13 | expectType>( 14 | ipcMain.callRenderer(browserWindow, 'get-emoji', 'unicorn') 15 | ); 16 | expectType>( 17 | ipcMain.callRenderer(browserWindow, 'get-emoji', 'unicorn') 18 | ); 19 | expectType>( 20 | ipcMain.callRenderer(browserWindow, 'get-emoji', 'unicorn') 21 | ); 22 | expectType>( 23 | ipcMain.callRenderer(browserWindow, 'get-emoji', 'unicorn') 24 | ); 25 | 26 | const detachListener = ipcMain.answerRenderer('get-emoji', emojiName => { 27 | expectType(emojiName); 28 | return '🦄'; 29 | }); 30 | ipcMain.answerRenderer('get-emoji', async emojiName => { 31 | expectType(emojiName); 32 | return '🦄'; 33 | }); 34 | ipcMain.answerRenderer('get-emoji', async emojiName => { 35 | expectType(emojiName); 36 | return '🦄'; 37 | }); 38 | ipcMain.answerRenderer('get-emoji', async emojiName => { 39 | expectType(emojiName); 40 | return '🦄'; 41 | }); 42 | ipcMain.answerRenderer(browserWindow, 'get-emoji', async emojiName => { 43 | expectType(emojiName); 44 | return '🦄'; 45 | }); 46 | 47 | expectType<() => void>(detachListener); 48 | detachListener(); 49 | 50 | ipcMain.sendToRenderers('get-emoji'); 51 | ipcMain.sendToRenderers('get-emoji', '🦄'); 52 | ipcMain.sendToRenderers('get-emoji', '🦄'); 53 | 54 | expectError(ipcMain.callMain); 55 | 56 | // IpcRenderer 57 | 58 | expectType>( 59 | ipcRenderer.callMain('get-emoji', 'unicorn') 60 | ); 61 | expectType>( 62 | ipcRenderer.callMain('get-emoji', 'unicorn') 63 | ); 64 | expectType>( 65 | ipcRenderer.callMain('get-emoji', 'unicorn') 66 | ); 67 | 68 | const detachListener2 = ipcRenderer.answerMain( 69 | 'get-emoji', 70 | async emojiName => { 71 | expectType(emojiName); 72 | return '🦄'; 73 | } 74 | ); 75 | ipcRenderer.answerMain('get-emoji', emojiName => { 76 | expectType(emojiName); 77 | return '🦄'; 78 | }); 79 | ipcRenderer.answerMain('get-emoji', emojiName => { 80 | expectType(emojiName); 81 | return '🦄'; 82 | }); 83 | ipcRenderer.answerMain('get-emoji', emojiName => { 84 | expectType(emojiName); 85 | return '🦄'; 86 | }); 87 | 88 | expectType<() => void>(detachListener2); 89 | detachListener(); 90 | 91 | expectError(ipcRenderer.callRenderer); 92 | -------------------------------------------------------------------------------- /source/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const electron = require('electron'); 3 | const {serializeError, deserializeError} = require('serialize-error'); 4 | const util = require('./util.js'); 5 | 6 | const {ipcMain, BrowserWindow} = electron; 7 | const ipc = Object.create(ipcMain || {}); 8 | 9 | ipc.callRenderer = (browserWindow, channel, data) => new Promise((resolve, reject) => { 10 | if (!browserWindow) { 11 | throw new Error('Browser window required'); 12 | } 13 | 14 | const {sendChannel, dataChannel, errorChannel} = util.getRendererResponseChannels(channel); 15 | 16 | const cleanup = () => { 17 | ipcMain.off(dataChannel, onData); 18 | ipcMain.off(errorChannel, onError); 19 | }; 20 | 21 | const onData = (event, result) => { 22 | const window = BrowserWindow.fromWebContents(event.sender); 23 | if (window.id === browserWindow.id) { 24 | cleanup(); 25 | resolve(result); 26 | } 27 | }; 28 | 29 | const onError = (event, error) => { 30 | const window = BrowserWindow.fromWebContents(event.sender); 31 | if (window.id === browserWindow.id) { 32 | cleanup(); 33 | reject(deserializeError(error)); 34 | } 35 | }; 36 | 37 | ipcMain.on(dataChannel, onData); 38 | ipcMain.on(errorChannel, onError); 39 | 40 | const completeData = { 41 | dataChannel, 42 | errorChannel, 43 | userData: data 44 | }; 45 | 46 | if (browserWindow.webContents) { 47 | browserWindow.webContents.send(sendChannel, completeData); 48 | } 49 | }); 50 | 51 | ipc.callFocusedRenderer = async (...args) => { 52 | const focusedWindow = BrowserWindow.getFocusedWindow(); 53 | if (!focusedWindow) { 54 | throw new Error('No browser window in focus'); 55 | } 56 | 57 | return ipc.callRenderer(focusedWindow, ...args); 58 | }; 59 | 60 | ipc.answerRenderer = (browserWindowOrChannel, channelOrCallback, callbackOrNothing) => { 61 | let window; 62 | let channel; 63 | let callback; 64 | 65 | if (callbackOrNothing === undefined) { 66 | channel = browserWindowOrChannel; 67 | callback = channelOrCallback; 68 | } else { 69 | window = browserWindowOrChannel; 70 | channel = channelOrCallback; 71 | callback = callbackOrNothing; 72 | 73 | if (!window) { 74 | throw new Error('Browser window required'); 75 | } 76 | } 77 | 78 | const sendChannel = util.getSendChannel(channel); 79 | 80 | const listener = async (event, data) => { 81 | const browserWindow = BrowserWindow.fromWebContents(event.sender); 82 | 83 | if (window && window.id !== browserWindow.id) { 84 | return; 85 | } 86 | 87 | const send = (channel, data) => { 88 | if (!(browserWindow && browserWindow.isDestroyed())) { 89 | event.sender.send(channel, data); 90 | } 91 | }; 92 | 93 | const {dataChannel, errorChannel, userData} = data; 94 | 95 | try { 96 | send(dataChannel, await callback(userData, browserWindow)); 97 | } catch (error) { 98 | send(errorChannel, serializeError(error)); 99 | } 100 | }; 101 | 102 | ipcMain.on(sendChannel, listener); 103 | 104 | return () => { 105 | ipcMain.off(sendChannel, listener); 106 | }; 107 | }; 108 | 109 | ipc.sendToRenderers = (channel, data) => { 110 | for (const browserWindow of BrowserWindow.getAllWindows()) { 111 | if (browserWindow.webContents) { 112 | browserWindow.webContents.send(channel, data); 113 | } 114 | } 115 | }; 116 | 117 | module.exports = ipc; 118 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {BrowserWindow, IpcMain, IpcRenderer} from 'electron'; 2 | 3 | export interface MainProcessIpc extends IpcMain { 4 | /** 5 | Send a message to the given window. 6 | 7 | In the renderer process, use `ipcRenderer.answerMain` to reply to this message. 8 | 9 | @param browserWindow - The window to send the message to. 10 | @param channel - The channel to send the message on. 11 | @param data - The data to send to the receiver. 12 | @returns - The reply from the renderer process. 13 | 14 | @example 15 | ``` 16 | import {BrowserWindow} from 'electron'; 17 | import {ipcMain as ipc} from 'electron-better-ipc'; 18 | 19 | const browserWindow = BrowserWindow.getFocusedWindow(); 20 | 21 | const emoji = await ipc.callRenderer(browserWindow!, 'get-emoji', 'unicorn'); 22 | console.log(emoji); 23 | //=> '🦄' 24 | ``` 25 | */ 26 | callRenderer( 27 | browserWindow: BrowserWindow, 28 | channel: string, 29 | data?: DataType 30 | ): Promise; 31 | 32 | /** 33 | Send a message to the focused window, as determined by `electron.BrowserWindow.getFocusedWindow`. 34 | 35 | In the renderer process, use `ipcRenderer.answerMain` to reply to this message. 36 | 37 | @param channel - The channel to send the message on. 38 | @param data - The data to send to the receiver. 39 | @returns - The reply from the renderer process. 40 | 41 | @example 42 | ``` 43 | import {ipcMain as ipc} from 'electron-better-ipc'; 44 | 45 | const emoji = await ipc.callFocusedRenderer('get-emoji', 'unicorn'); 46 | console.log(emoji); 47 | //=> '🦄' 48 | ``` 49 | */ 50 | callFocusedRenderer( 51 | channel: string, 52 | data?: DataType 53 | ): Promise; 54 | 55 | /** 56 | This method listens for a message from `ipcRenderer.callMain` defined in a renderer process and replies back. 57 | 58 | @param channel - The channel to send the message on. 59 | @param callback - The return value is sent back to the `ipcRenderer.callMain` in the renderer process. 60 | @returns A function, that when called, removes the listener. 61 | 62 | @example 63 | ``` 64 | import {ipcMain as ipc} from 'electron-better-ipc'; 65 | 66 | ipc.answerRenderer('get-emoji', async emojiName => { 67 | const emoji = await getEmoji(emojiName); 68 | return emoji; 69 | }); 70 | ``` 71 | */ 72 | answerRenderer( 73 | channel: string, 74 | callback: ( 75 | data: DataType, 76 | browserWindow: BrowserWindow 77 | ) => ReturnType | PromiseLike 78 | ): () => void; 79 | 80 | /** 81 | This method listens for a message from `ipcRenderer.callMain` defined in the given BrowserWindow's renderer process and replies back. 82 | 83 | @param browserWindow - The window for which to expect the message. 84 | @param channel - The channel to send the message on. 85 | @param callback - The return value is sent back to the `ipcRenderer.callMain` in the renderer process. 86 | @returns A function, that when called, removes the listener. 87 | 88 | @example 89 | ``` 90 | import {ipcMain as ipc} from 'electron-better-ipc'; 91 | 92 | ipc.answerRenderer('get-emoji', async emojiName => { 93 | const emoji = await getEmoji(emojiName); 94 | return emoji; 95 | }); 96 | ``` 97 | */ 98 | answerRenderer( 99 | browserWindow: BrowserWindow, 100 | channel: string, 101 | callback: ( 102 | data: DataType, 103 | browserWindow: BrowserWindow 104 | ) => ReturnType | PromiseLike 105 | ): () => void; 106 | 107 | /** 108 | Send a message to all renderer processes (windows). 109 | 110 | @param channel - The channel to send the message on. 111 | @param data - The data to send to the receiver. 112 | */ 113 | sendToRenderers(channel: string, data?: DataType): void; 114 | } 115 | 116 | export interface RendererProcessIpc extends IpcRenderer { 117 | /** 118 | Send a message to the main process. 119 | 120 | In the main process, use `ipcMain.answerRenderer` to reply to this message. 121 | 122 | @param channel - The channel to send the message on. 123 | @param data - The data to send to the receiver. 124 | @returns The reply from the main process. 125 | 126 | @example 127 | ``` 128 | import {ipcRenderer as ipc} from 'electron-better-ipc'; 129 | 130 | const emoji = await ipc.callMain('get-emoji', 'unicorn'); 131 | console.log(emoji); 132 | //=> '🦄' 133 | ``` 134 | */ 135 | callMain(channel: string, data?: DataType): Promise; 136 | 137 | /** 138 | This method listens for a message from `ipcMain.callRenderer` defined in the main process and replies back. 139 | 140 | @param channel - The channel to send the message on. 141 | @param callback - The return value is sent back to the `ipcMain.callRenderer` in the main process. 142 | @returns A function, that when called, removes the listener. 143 | 144 | @example 145 | ``` 146 | import {ipcRenderer as ipc} from 'electron-better-ipc'; 147 | 148 | ipc.answerMain('get-emoji', async emojiName => { 149 | const emoji = await getEmoji(emojiName); 150 | return emoji; 151 | }); 152 | ``` 153 | */ 154 | answerMain( 155 | channel: string, 156 | callback: (data: DataType) => ReturnType | PromiseLike 157 | ): () => void; 158 | } 159 | 160 | export const ipcMain: MainProcessIpc; 161 | export const ipcRenderer: RendererProcessIpc; 162 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # electron-better-ipc 2 | 3 | > Simplified IPC communication for Electron apps 4 | 5 | The biggest benefit of this module over the [built-in IPC](https://electronjs.org/docs/api/ipc-main) is that it enables you to send a message and get the response back in the same call. This would usually require multiple IPC subscriptions. 6 | 7 | You can use this module directly in both the main and renderer process. 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm install electron-better-ipc 13 | ``` 14 | 15 | *Requires Electron 10 or later.* 16 | 17 | ## Usage 18 | 19 | ### Using the built-in IPC 20 | 21 | Here, as an example, we use the built-in IPC to get an emoji by name in the renderer process from the main process. Notice how it requires coordinating multiple IPC subscriptions. 22 | 23 | ###### Main 24 | 25 | ```js 26 | const {ipcMain: ipc} = require('electron'); 27 | 28 | ipc.on('get-emoji', async (event, emojiName) => { 29 | const emoji = await getEmoji(emojiName); 30 | event.sender.send('get-emoji-response', emoji); 31 | }); 32 | ``` 33 | 34 | ###### Renderer 35 | 36 | ```js 37 | const {ipcRenderer: ipc} = require('electron'); 38 | 39 | ipc.on('get-emoji-response', (event, emoji) => { 40 | console.log(emoji); 41 | //=> '🦄' 42 | }); 43 | 44 | ipc.send('get-emoji', 'unicorn'); 45 | ``` 46 | 47 | ### Using this module 48 | 49 | As you can see below, this module makes it much simpler to handle the communication. You no longer need multiple IPC subscriptions and you can just `await` the response in the same call. 50 | 51 | ###### Main 52 | 53 | ```js 54 | const {ipcMain: ipc} = require('electron-better-ipc'); 55 | 56 | ipc.answerRenderer('get-emoji', async emojiName => { 57 | const emoji = await getEmoji(emojiName); 58 | return emoji; 59 | }); 60 | ``` 61 | 62 | ###### Renderer 63 | 64 | ```js 65 | const {ipcRenderer: ipc} = require('electron-better-ipc'); 66 | 67 | (async () => { 68 | const emoji = await ipc.callMain('get-emoji', 'unicorn'); 69 | console.log(emoji); 70 | //=> '🦄' 71 | })(); 72 | ``` 73 | 74 | Here we do the inverse of the above, we get an emoji by name in the main process from the renderer process: 75 | 76 | ###### Renderer 77 | 78 | ```js 79 | const {ipcRenderer: ipc} = require('electron-better-ipc'); 80 | 81 | ipc.answerMain('get-emoji', async emojiName => { 82 | const emoji = await getEmoji(emojiName); 83 | return emoji; 84 | }); 85 | ``` 86 | 87 | ###### Main 88 | 89 | ```js 90 | const {ipcMain: ipc} = require('electron-better-ipc'); 91 | 92 | (async () => { 93 | const emoji = await ipc.callFocusedRenderer('get-emoji', 'unicorn'); 94 | console.log(emoji); 95 | //=> '🦄' 96 | })(); 97 | ``` 98 | 99 | ## API 100 | 101 | The module exports `ipcMain` and `ipcRenderer` objects which enhance the built-in `ipc` module with some added methods, so you can use them as a replacement for `electron.ipcMain`/`electron.ipcRenderer`. 102 | 103 | ## Main process 104 | 105 | ### ipcMain.callRenderer(browserWindow, channel, data?) 106 | 107 | Send a message to the given window. 108 | 109 | In the renderer process, use `ipcRenderer.answerMain` to reply to this message. 110 | 111 | Returns a `Promise` with the reply from the renderer process. 112 | 113 | #### browserWindow 114 | 115 | Type: `BrowserWindow` 116 | 117 | The window to send the message to. 118 | 119 | #### channel 120 | 121 | Type: `string` 122 | 123 | The channel to send the message on. 124 | 125 | #### data 126 | 127 | Type: `unknown` 128 | 129 | The data to send to the receiver. 130 | 131 | ### ipcMain.callFocusedRenderer(channel, data?) 132 | 133 | Send a message to the focused window, as determined by `electron.BrowserWindow.getFocusedWindow`. 134 | 135 | In the renderer process, use `ipcRenderer.answerMain` to reply to this message. 136 | 137 | Returns a `Promise` with the reply from the renderer process. 138 | 139 | #### channel 140 | 141 | Type: `string` 142 | 143 | The channel to send the message on. 144 | 145 | #### data 146 | 147 | Type: `unknown` 148 | 149 | The data to send to the receiver. 150 | 151 | ### ipcMain.answerRenderer(channel, callback) 152 | 153 | This method listens for a message from `ipcRenderer.callMain` defined in a renderer process and replies back. 154 | 155 | Returns a function, that when called, removes the listener. 156 | 157 | #### channel 158 | 159 | Type: `string` 160 | 161 | The channel to send the message on. 162 | 163 | #### callback(data?, browserWindow) 164 | 165 | Type: `Function | AsyncFunction` 166 | 167 | The return value is sent back to the `ipcRenderer.callMain` in the renderer process. 168 | 169 | ### ipcMain.answerRenderer(browserWindow, channel, callback) 170 | 171 | This method listens for a message from `ipcRenderer.callMain` defined in the given BrowserWindow's renderer process and replies back. 172 | 173 | Returns a function, that when called, removes the listener. 174 | 175 | #### browserWindow 176 | 177 | Type: `BrowserWindow` 178 | 179 | The window for which to expect the message. 180 | 181 | #### channel 182 | 183 | Type: `string` 184 | 185 | The channel to send the message on. 186 | 187 | #### callback(data?, browserWindow) 188 | 189 | Type: `Function | AsyncFunction` 190 | 191 | The return value is sent back to the `ipcRenderer.callMain` in the renderer process. 192 | 193 | ### ipcMain.sendToRenderers(channel, data?) 194 | 195 | Send a message to all renderer processes (windows). 196 | 197 | #### channel 198 | 199 | Type: `string` 200 | 201 | The channel to send the message on. 202 | 203 | #### data 204 | 205 | Type: `unknown` 206 | 207 | The data to send to the receiver. 208 | 209 | ## Renderer process 210 | 211 | ### ipcRenderer.callMain(channel, data?) 212 | 213 | Send a message to the main process. 214 | 215 | In the main process, use `ipcMain.answerRenderer` to reply to this message. 216 | 217 | Returns a `Promise` with the reply from the main process. 218 | 219 | #### channel 220 | 221 | Type: `string` 222 | 223 | The channel to send the message on. 224 | 225 | #### data 226 | 227 | Type: `unknown` 228 | 229 | The data to send to the receiver. 230 | 231 | ### ipcRenderer.answerMain(channel, callback) 232 | 233 | This method listens for a message from `ipcMain.callRenderer` defined in the main process and replies back. 234 | 235 | Returns a function, that when called, removes the listener. 236 | 237 | #### channel 238 | 239 | Type: `string` 240 | 241 | The channel to send the message on. 242 | 243 | #### callback(data?) 244 | 245 | Type: `Function | AsyncFunction` 246 | 247 | The return value is sent back to the `ipcMain.callRenderer` in the main process. 248 | 249 | ## Related 250 | 251 | - [electron-store](https://github.com/sindresorhus/electron-store) - Simple data persistence for your Electron app 252 | - [electron-timber](https://github.com/sindresorhus/electron-timber) - Pretty logger for Electron apps 253 | - [electron-serve](https://github.com/sindresorhus/electron-serve) - Static file serving for Electron apps 254 | - [electron-debug](https://github.com/sindresorhus/electron-debug) - Adds useful debug features to your Electron app 255 | - [electron-unhandled](https://github.com/sindresorhus/electron-unhandled) - Catch unhandled errors and promise rejections in your Electron app 256 | - [electron-context-menu](https://github.com/sindresorhus/electron-context-menu) - Context menu for your Electron app 257 | - [electron-dl](https://github.com/sindresorhus/electron-dl) - Simplified file downloads for your Electron app 258 | --------------------------------------------------------------------------------