Skip to content

Commit

Permalink
feat: MessagePorts in the main process (#24323)
Browse files Browse the repository at this point in the history
Co-authored-by: Milan Burda <miburda@microsoft.com>
  • Loading branch information
miniak and Milan Burda committed Jul 2, 2020
1 parent 71e3296 commit 4ace499
Show file tree
Hide file tree
Showing 34 changed files with 1,319 additions and 114 deletions.
46 changes: 42 additions & 4 deletions docs/api/ipc-renderer.md
Expand Up @@ -57,7 +57,7 @@ Removes all listeners, or those of the specified `channel`.

Send an asynchronous message to the main process via `channel`, along with
arguments. Arguments will be serialized with the [Structured Clone
Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be
Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be
included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will
throw an exception.

Expand All @@ -68,6 +68,10 @@ throw an exception.
The main process handles it by listening for `channel` with the
[`ipcMain`](ipc-main.md) module.

If you need to transfer a [`MessagePort`][] to the main process, use [`ipcRenderer.postMessage`](#ipcrendererpostmessagechannel-message-transfer).

If you want to receive a single response from the main process, like the result of a method call, consider using [`ipcRenderer.invoke`](#ipcrendererinvokechannel-args).

### `ipcRenderer.invoke(channel, ...args)`

* `channel` String
Expand All @@ -77,7 +81,7 @@ Returns `Promise<any>` - Resolves with the response from the main process.

Send a message to the main process via `channel` and expect a result
asynchronously. Arguments will be serialized with the [Structured Clone
Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be
Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be
included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will
throw an exception.

Expand All @@ -102,6 +106,10 @@ ipcMain.handle('some-name', async (event, someArgument) => {
})
```

If you need to transfer a [`MessagePort`][] to the main process, use [`ipcRenderer.postMessage`](#ipcrendererpostmessagechannel-message-transfer).

If you do not need a respons to the message, consider using [`ipcRenderer.send`](#ipcrenderersendchannel-args).

### `ipcRenderer.sendSync(channel, ...args)`

* `channel` String
Expand All @@ -111,7 +119,7 @@ Returns `any` - The value sent back by the [`ipcMain`](ipc-main.md) handler.

Send a message to the main process via `channel` and expect a result
synchronously. Arguments will be serialized with the [Structured Clone
Algorithm][SCA], just like [`postMessage`][], so prototype chains will not be
Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be
included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will
throw an exception.

Expand All @@ -127,6 +135,35 @@ and replies by setting `event.returnValue`.
> last resort. It's much better to use the asynchronous version,
> [`invoke()`](ipc-renderer.md#ipcrendererinvokechannel-args).
### `ipcRenderer.postMessage(channel, message, [transfer])`

* `channel` String
* `message` any
* `transfer` MessagePort[] (optional)

Send a message to the main process, optionally transferring ownership of zero
or more [`MessagePort`][] objects.

The transferred `MessagePort` objects will be available in the main process as
[`MessagePortMain`](message-port-main.md) objects by accessing the `ports`
property of the emitted event.

For example:
```js
// Renderer process
const { port1, port2 } = new MessageChannel()
ipcRenderer.postMessage('port', { message: 'hello' }, [port1])

// Main process
ipcMain.on('port', (e, msg) => {
const [port] = e.ports
// ...
})
```

For more information on using `MessagePort` and `MessageChannel`, see the [MDN
documentation](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel).

### `ipcRenderer.sendTo(webContentsId, channel, ...args)`

* `webContentsId` Number
Expand All @@ -150,4 +187,5 @@ in the [`ipc-renderer-event`](structures/ipc-renderer-event.md) structure docs.

[event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
[SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
[`postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
[`window.postMessage`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
30 changes: 30 additions & 0 deletions docs/api/message-channel-main.md
@@ -0,0 +1,30 @@
# MessageChannelMain

`MessageChannelMain` is the main-process-side equivalent of the DOM
[`MessageChannel`][] object. Its singular function is to create a pair of
connected [`MessagePortMain`](message-port-main.md) objects.

See the [Channel Messaging API][] documentation for more information on using
channel messaging.

## Class: MessageChannelMain

Example:
```js
const { port1, port2 } = new MessageChannelMain()
w.webContents.postMessage('port', null, [port2])
port1.postMessage({ some: 'message' })
```

### Instance Properties

#### `channel.port1`

A [`MessagePortMain`](message-port-main.md) property.

#### `channel.port2`

A [`MessagePortMain`](message-port-main.md) property.

[`MessageChannel`]: https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel
[Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API
49 changes: 49 additions & 0 deletions docs/api/message-port-main.md
@@ -0,0 +1,49 @@
# MessagePortMain

`MessagePortMain` is the main-process-side equivalent of the DOM
[`MessagePort`][] object. It behaves similarly to the DOM version, with the
exception that it uses the Node.js `EventEmitter` event system, instead of the
DOM `EventTarget` system. This means you should use `port.on('message', ...)`
to listen for events, instead of `port.onmessage = ...` or
`port.addEventListener('message', ...)`

See the [Channel Messaging API][] documentation for more information on using
channel messaging.

`MessagePortMain` is an [EventEmitter][event-emitter].

## Class: MessagePortMain

### Instance Methods

#### `port.postMessage(message, [transfer])`

* `message` any
* `transfer` MessagePortMain[] (optional)

Sends a message from the port, and optionally, transfers ownership of objects
to other browsing contexts.

#### `port.start()`

Starts the sending of messages queued on the port. Messages will be queued
until this method is called.

#### `port.close()`

Disconnects the port, so it is no longer active.

### Instance Events

#### Event: 'message'

Returns:

* `messageEvent` Object
* `data` any
* `ports` MessagePortMain[]

Emitted when a MessagePortMain object receives a message.

[`MessagePort`]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
[Channel Messaging API]: https://developer.mozilla.org/en-US/docs/Web/API/Channel_Messaging_API
1 change: 1 addition & 0 deletions docs/api/structures/ipc-main-event.md
Expand Up @@ -3,6 +3,7 @@
* `frameId` Integer - The ID of the renderer frame that sent this message
* `returnValue` any - Set this to the value to be returned in a synchronous message
* `sender` WebContents - Returns the `webContents` that sent the message
* `ports` MessagePortMain[] - A list of MessagePorts that were transferred with this message
* `reply` Function - A function that will send an IPC message to the renderer frame that sent the original message that you are currently handling. You should use this method to "reply" to the sent message in order to guarantee the reply will go to the correct process and frame.
* `channel` String
* `...args` any[]
1 change: 1 addition & 0 deletions docs/api/structures/ipc-renderer-event.md
Expand Up @@ -2,5 +2,6 @@

* `sender` IpcRenderer - The `IpcRenderer` instance that emitted the event originally
* `senderId` Integer - The `webContents.id` that sent the message, you can call `event.sender.sendTo(event.senderId, ...)` to reply to the message, see [ipcRenderer.sendTo][ipc-renderer-sendto] for more information. This only applies to messages sent from a different renderer. Messages sent directly from the main process set `event.senderId` to `0`.
* `ports` MessagePort[] - A list of MessagePorts that were transferred with this message

[ipc-renderer-sendto]: #ipcrenderersendtowindowid-channel--arg1-arg2-
26 changes: 26 additions & 0 deletions docs/api/web-contents.md
Expand Up @@ -1603,6 +1603,32 @@ ipcMain.on('ping', (event) => {
})
```

#### `contents.postMessage(channel, message, [transfer])`

* `channel` String
* `message` any
* `transfer` MessagePortMain[] (optional)

Send a message to the renderer process, optionally transferring ownership of
zero or more [`MessagePortMain`][] objects.

The transferred `MessagePortMain` objects will be available in the renderer
process by accessing the `ports` property of the emitted event. When they
arrive in the renderer, they will be native DOM `MessagePort` objects.

For example:
```js
// Main process
const { port1, port2 } = new MessageChannelMain()
webContents.postMessage('port', { message: 'hello' }, [port1])

// Renderer process
ipcRenderer.on('port', (e, msg) => {
const [port] = e.ports
// ...
})
```

#### `contents.enableDeviceEmulation(parameters)`

* `parameters` Object
Expand Down
4 changes: 4 additions & 0 deletions filenames.auto.gni
Expand Up @@ -32,6 +32,8 @@ auto_filenames = {
"docs/api/locales.md",
"docs/api/menu-item.md",
"docs/api/menu.md",
"docs/api/message-channel-main.md",
"docs/api/message-port-main.md",
"docs/api/modernization",
"docs/api/native-image.md",
"docs/api/native-theme.md",
Expand Down Expand Up @@ -223,6 +225,7 @@ auto_filenames = {
"lib/browser/api/menu-item.js",
"lib/browser/api/menu-utils.js",
"lib/browser/api/menu.js",
"lib/browser/api/message-channel.ts",
"lib/browser/api/module-list.ts",
"lib/browser/api/native-theme.ts",
"lib/browser/api/net-log.js",
Expand Down Expand Up @@ -258,6 +261,7 @@ auto_filenames = {
"lib/browser/ipc-main-impl.ts",
"lib/browser/ipc-main-internal-utils.ts",
"lib/browser/ipc-main-internal.ts",
"lib/browser/message-port-main.ts",
"lib/browser/navigation-controller.js",
"lib/browser/remote/objects-registry.ts",
"lib/browser/remote/server.ts",
Expand Down
5 changes: 5 additions & 0 deletions filenames.gni
Expand Up @@ -125,6 +125,8 @@ filenames = {
"shell/browser/api/gpu_info_enumerator.h",
"shell/browser/api/gpuinfo_manager.cc",
"shell/browser/api/gpuinfo_manager.h",
"shell/browser/api/message_port.cc",
"shell/browser/api/message_port.h",
"shell/browser/api/process_metric.cc",
"shell/browser/api/process_metric.h",
"shell/browser/api/save_page_handler.cc",
Expand Down Expand Up @@ -510,6 +512,7 @@ filenames = {
"shell/common/gin_helper/event_emitter_caller.h",
"shell/common/gin_helper/function_template.cc",
"shell/common/gin_helper/function_template.h",
"shell/common/gin_helper/function_template_extensions.h",
"shell/common/gin_helper/locker.cc",
"shell/common/gin_helper/locker.h",
"shell/common/gin_helper/microtasks_scope.cc",
Expand Down Expand Up @@ -561,6 +564,8 @@ filenames = {
"shell/common/skia_util.h",
"shell/common/v8_value_converter.cc",
"shell/common/v8_value_converter.h",
"shell/common/v8_value_serializer.cc",
"shell/common/v8_value_serializer.h",
"shell/common/world_ids.h",
"shell/renderer/api/context_bridge/object_cache.cc",
"shell/renderer/api/context_bridge/object_cache.h",
Expand Down
12 changes: 12 additions & 0 deletions lib/browser/api/message-channel.ts
@@ -0,0 +1,12 @@
import { MessagePortMain } from '@electron/internal/browser/message-port-main';
const { createPair } = process.electronBinding('message_port');

export default class MessageChannelMain {
port1: MessagePortMain;
port2: MessagePortMain;
constructor () {
const { port1, port2 } = createPair();
this.port1 = new MessagePortMain(port1);
this.port2 = new MessagePortMain(port2);
}
}
1 change: 1 addition & 0 deletions lib/browser/api/module-list.ts
Expand Up @@ -14,6 +14,7 @@ export const browserModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'inAppPurchase', loader: () => require('./in-app-purchase') },
{ name: 'Menu', loader: () => require('./menu') },
{ name: 'MenuItem', loader: () => require('./menu-item') },
{ name: 'MessageChannelMain', loader: () => require('./message-channel') },
{ name: 'nativeTheme', loader: () => require('./native-theme') },
{ name: 'net', loader: () => require('./net') },
{ name: 'netLog', loader: () => require('./net-log') },
Expand Down
1 change: 1 addition & 0 deletions lib/browser/api/module-names.ts
Expand Up @@ -20,6 +20,7 @@ export const browserModuleNames = [
'nativeTheme',
'net',
'netLog',
'MessageChannelMain',
'Notification',
'powerMonitor',
'powerSaveBlocker',
Expand Down
13 changes: 13 additions & 0 deletions lib/browser/api/web-contents.js
Expand Up @@ -10,6 +10,7 @@ const { internalWindowOpen } = require('@electron/internal/browser/guest-window-
const NavigationController = require('@electron/internal/browser/navigation-controller');
const { ipcMainInternal } = require('@electron/internal/browser/ipc-main-internal');
const ipcMainUtils = require('@electron/internal/browser/ipc-main-internal-utils');
const { MessagePortMain } = require('@electron/internal/browser/message-port-main');

// session is not used here, the purpose is to make sure session is initalized
// before the webContents module.
Expand Down Expand Up @@ -114,6 +115,13 @@ WebContents.prototype.send = function (channel, ...args) {
return this._send(internal, sendToAll, channel, args);
};

WebContents.prototype.postMessage = function (...args) {
if (Array.isArray(args[2])) {
args[2] = args[2].map(o => o instanceof MessagePortMain ? o._internalPort : o);
}
this._postMessage(...args);
};

WebContents.prototype.sendToAll = function (channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument');
Expand Down Expand Up @@ -471,6 +479,11 @@ WebContents.prototype._init = function () {
}
});

this.on('-ipc-ports', function (event, internal, channel, message, ports) {
event.ports = ports.map(p => new MessagePortMain(p));
ipcMain.emit(channel, event, message);
});

// Handle context menu action request from pepper plugin.
this.on('pepper-context-menu', function (event, params, callback) {
// Access Menu via electron.Menu to prevent circular require.
Expand Down
25 changes: 25 additions & 0 deletions lib/browser/message-port-main.ts
@@ -0,0 +1,25 @@
import { EventEmitter } from 'events';

export class MessagePortMain extends EventEmitter {
_internalPort: any
constructor (internalPort: any) {
super();
this._internalPort = internalPort;
this._internalPort.emit = (channel: string, event: {ports: any[]}) => {
if (channel === 'message') { event = { ...event, ports: event.ports.map(p => new MessagePortMain(p)) }; }
this.emit(channel, event);
};
}
start () {
return this._internalPort.start();
}
close () {
return this._internalPort.close();
}
postMessage (...args: any[]) {
if (Array.isArray(args[1])) {
args[1] = args[1].map((o: any) => o instanceof MessagePortMain ? o._internalPort : o);
}
return this._internalPort.postMessage(...args);
}
}
4 changes: 4 additions & 0 deletions lib/renderer/api/ipc-renderer.ts
Expand Up @@ -31,4 +31,8 @@ if (!ipcRenderer.send) {
};
}

ipcRenderer.postMessage = function (channel: string, message: any, transferables: any) {
return ipc.postMessage(channel, message, transferables);
};

export default ipcRenderer;
4 changes: 2 additions & 2 deletions lib/renderer/init.ts
Expand Up @@ -45,9 +45,9 @@ v8Util.setHiddenValue(global, 'ipc', ipcEmitter);
v8Util.setHiddenValue(global, 'ipc-internal', ipcInternalEmitter);

v8Util.setHiddenValue(global, 'ipcNative', {
onMessage (internal: boolean, channel: string, args: any[], senderId: number) {
onMessage (internal: boolean, channel: string, ports: any[], args: any[], senderId: number) {
const sender = internal ? ipcInternalEmitter : ipcEmitter;
sender.emit(channel, { sender, senderId }, ...args);
sender.emit(channel, { sender, senderId, ports }, ...args);
}
});

Expand Down
4 changes: 2 additions & 2 deletions lib/sandboxed_renderer/init.js
Expand Up @@ -54,9 +54,9 @@ const loadedModules = new Map([
// ElectronApiServiceImpl will look for the "ipcNative" hidden object when
// invoking the 'onMessage' callback.
v8Util.setHiddenValue(global, 'ipcNative', {
onMessage (internal, channel, args, senderId) {
onMessage (internal, channel, ports, args, senderId) {
const sender = internal ? ipcRendererInternal : electron.ipcRenderer;
sender.emit(channel, { sender, senderId }, ...args);
sender.emit(channel, { sender, senderId, ports }, ...args);
}
});

Expand Down

0 comments on commit 4ace499

Please sign in to comment.