Comlink makes WebWorkers enjoyable. Comlink is a tiny library (1.1kB), that removes the mental barrier of thinking about postMessage and hides the fact that you are working with workers.
At a more abstract level it is an RPC implementation for postMessage and ES6 Proxies.
The core implementation of comlink is based on postMessage
and ES6 Proxy. In theory, a comlink adapter can be implemented in any JavaScript environment that supports Proxy
and postMessage
bi-directional communication mechanisms, making it possible to use it in environments other than WebWorkers. The implementation of the adapter can refer to node-adapter.
Some advanced features of comlink require the use of MessageChannel and MessagePort for transmission, and some platform adapters may not support these features. These advanced features include:
- Constructing remote proxy objects with
new ProxyTarget()
- Comlink.proxy
- Comlink.createEndpoint
The currently implemented adapters are as follows:
We welcome you to raise issues or to contribute to the development of adapters for other application platforms.
# npm
npm i comlink comlink-adapters -S
# yarn
yarn add comlink comlink-adapters
# pnpm
pnpm add comlink comlink-adapters
Adapters:
electronMainEndpoint
is used to createEndpoint
objects in the main process.electronRendererEndpoint
is used to createEndpoint
objects in the rendering process.
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ✅ | await new ProxyObj(); |
|
proxy function | ✅ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
|
createEndpoint | ✅ | proxyObj[comlink.createEndpoint](); |
Not recommended to use |
release | ✅ | proxyObj[comlink.releaseProxy](); |
Support for createEndpoint
is provided, but it is not recommended to use. The internal implementation bridges MessagePort and MessagePortMain, which results in poor efficiency.
electronMainEndpoint:
interface ElectronMainEndpointOptions {
sender: WebContents;
ipcMain: IpcMain;
messageChannelConstructor: new () => MessageChannelMain;
channelName?: string;
}
interface electronMainEndpoint {
(options: ElectronMainEndpointOptions): Endpoint;
}
- sender: The renderer WebContents object to communicate with.
- ipcMain: The IpcMain object in Electron.
- messageChannelConstructor: Constructor of MessageChannel, using MessageChannelMain in the main process.
- channelName: The IPC channel identifier, default is
__COMLINK_MESSAGE_CHANNEL__
. Multiple pairs of comlink endpoints can be created via channelName.
// main.ts
import { ipcMain, MessageChannelMain } from 'electron';
import { expose } from 'comlink';
import { electronMainEndpoint } from 'comlink-adapters';
import type { WebContents, IpcMainEvent } from 'electron';
const add = (a: number, b: number) => a + b;
const senderWeakMap = new WeakMap<WebContents, boolean>();
const ackMessage = (sender: WebContents) => {
sender.postMessage('init-comlink-endponit:ack', null);
};
ipcMain.on('init-comlink-endponit:syn', (event: IpcMainEvent) => {
if (senderWeakMap.has(event.sender)) {
ackMessage(event.sender);
return;
}
// expose add function
expose(
add,
electronMainEndpoint({
ipcMain,
messageChannelConstructor: MessageChannelMain,
sender: event.sender,
})
);
ackMessage(event.sender);
senderWeakMap.set(event.sender, true);
});
electronRendererEndpoint:
interface ElectronRendererEndpointOptions {
ipcRenderer: IpcRenderer;
channelName?: string;
}
interface electronRendererEndpoint {
(options: ElectronRendererEndpointOptions): Endpoint;
}
- ipcRenderer: The IpcRenderer object in Electron.
- channelName: IPC channel identifier.
// renderer.ts
import { ipcRenderer } from 'electron';
import { wrap } from 'comlink';
import { electronRendererEndpoint } from 'comlink-adapters';
import type { Remote } from 'comlink';
type Add = (a: number, b: number) => number;
(async function () {
const useRemoteAdd = () => {
return new Promise<Remote<Add>>((resolve) => {
ipcRenderer.on('init-comlink-endponit:ack', () => {
resolve(wrap<Add>(electronRendererEndpoint({ ipcRenderer })));
});
ipcRenderer.postMessage('init-comlink-endponit:syn', null);
});
};
const remoteAdd = await useRemoteAdd();
const sum = await remoteAdd(1, 2);
// output: 3
})();
Adapters:
figmaCoreEndpoint
is used to createEndpoint
objects in the main thread of the Figma sandbox.figmaUIEndpoint
is used to createEndpoint
objects in the Figma UI process.
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ❌ | await new ProxyObj(); |
The Core thread does not support MessageChannel, and the MessagePort cannot be transferred between Core and UI threads |
proxy function | ❌ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
Same as above |
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
Same as above |
release | ✅ | proxyObj[comlink.releaseProxy](); |
figmaCoreEndpoint:
interface FigmaCoreEndpointOptions {
origin?: string;
checkProps?: (props: OnMessageProperties) => boolean | Promise<boolean>;
}
interface figmaCoreEndpoint {
(options: FigmaCoreEndpointOptions): Endpoint;
}
- origin: Configuration of
origin
in figma.ui.postMessage, default is*
. - checkProps: Used to check the origin in
props
returned by figma.ui.on('message', (msg, props) => {}).
// core.ts
import { expose } from 'comlink';
import { figmaCoreEndpoint } from 'comlink-adapters';
expose((a: number, b: number) => a + b, figmaCoreEndpoint());
figmaUIEndpoint:
interface FigmaUIEndpointOptions {
origin?: string;
}
interface figmaUIEndpoint {
(options: FigmaUIEndpointOptions): Endpoint;
}
- origin:
targetOrigin
configuration in window:postMessage of UI iframe, default is*
// ui.ts
import { wrap } from 'comlink';
import { figmaUIEndpoint } from 'comlink-adapters';
(async function () {
const add = wrap<(a: number, b: number) => number>(figmaUIEndpoint());
const sum = await add(1, 2);
// output: 3
})();
Adapters:
chromeRuntimePortEndpoint
is used to createEndpoint
objects for extensions based on long-lived connections.chromeRuntimeMessageEndpoint
is used to createEndpoint
objects for extensions based on simple one-off requests.
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ❌ | await new ProxyObj(); |
API does not support passing MessagePort |
proxy function | ❌ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
Same as above |
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
Same as above |
release | ✅ | proxyObj[comlink.releaseProxy](); |
The two main types of communication in Chrome Extensions are long-lived connections and simple one-off requests. For the use of comlink, it is more recommended to use long-lived connections, which are simpler and easier to understand. Note that when using communication between extensions, you need to configure externally_connectable in manifest.json
first.
chromeRuntimePortEndpoint:
interface chromeRuntimePortEndpoint {
(port: chrome.runtime.Port): Endpoint;
}
port A Port
object created by runtime.connect or tabs.connect.
Communication between the front and background pages within the extension:
// front.ts (content scripts/popup page/options page)
import { wrap } from 'comlink';
import { chromeRuntimePortEndpoint } from 'comlink-adapters';
(async function () {
const port = chrome.runtime.connect({
name: 'comlink-message-channel',
});
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimePortEndpoint(port)
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// background.ts
import { expose } from 'comlink';
import { chromeRuntimePortEndpoint } from 'comlink-adapters';
chrome.runtime.onConnect.addListener(function (port) {
if (port.name === 'comlink-message-channel') {
expose(
(a: number, b: number) => a + b,
chromeRuntimePortEndpoint(port)
);
}
});
Communication between different extensions:
// extension A background
import { wrap } from 'comlink';
import { chromeRuntimePortEndpoint } from 'comlink-adapters';
(async function () {
const targetExtensionId = 'B Extension ID';
const port = chrome.runtime.connect(targetExtensionId, {
name: 'comlink-message-channel',
});
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimePortEndpoint(port)
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// extension B background
import { expose } from 'comlink';
import { chromeRuntimePortEndpoint } from 'comlink-adapters';
chrome.runtime.onConnectExternal.addListener((port) => {
if (port.name === 'comlink-message-channel') {
expose(
(a: number, b: number) => a + b,
chromeRuntimePortEndpoint(port)
);
}
});
chromeRuntimeMessageEndpoint:
interface chromeRuntimeMessageEndpoint {
(options?: { tabId?: number; extensionId?: string }): Endpoint;
}
- tabId The tab id of the page to communicate with
- extensionId The id of the extension to communicate with
If neither tabId
nor extensionId
is provided, it means that the communication is between internal pages of the plugin.
Communication between internal pages and background pages of the plugin:
// popup page/options page
import { wrap } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
(async function () {
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimeMessageEndpoint()
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// background
import { expose } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
expose((a: number, b: number) => a + b, chromeRuntimeMessageEndpoint());
Communication between content scripts and background pages:
// content scripts
import { wrap } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
(async function () {
await chrome.runtime.sendMessage('create-expose-endpoint');
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimeMessageEndpoint()
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// background
import { expose } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message === 'create-expose-endpoint') {
expose(
(a: number, b: number) => a + b,
chromeRuntimeMessageEndpoint({ tabId: sender.tab?.id })
);
sendResponse();
return true;
}
sendResponse();
return true;
});
Communication between different extensions:
// extension A background
import { wrap } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
(async function () {
const targetExtensionID = 'B Extension ID';
chrome.runtime.sendMessage(targetExtensionID, 'create-expose-endpoint');
const remoteAdd = wrap<(a: number, b: number) => number>(
chromeRuntimeMessageEndpoint()
);
const sum = await remoteAdd(1, 2);
// output: 3
})();
// extension B background
import { expose } from 'comlink';
import { chromeRuntimeMessageEndpoint } from 'comlink-adapters';
chrome.runtime.onMessageExternal.addListener(
(message, sender, sendResponse) => {
if (message === 'create-expose-endpoint') {
expose(
(a: number, b: number) => a + b,
chromeRuntimeMessageEndpoint({
extensionId: sender.id,
})
);
sendResponse();
return true;
}
sendResponse();
return true;
}
);
Adapters:
nodeProcessEndpoint
: Used for creating anEndpoint
object within a node process.
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ✅ | await new ProxyObj(); |
|
proxy function | ✅ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
|
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
Not supported for MessagePort passing |
release | ✅ | proxyObj[comlink.releaseProxy](); |
nodeProcessEndpoint:
interface nodeProcessEndpoint {
(options: {
nodeProcess: ChildProcess | NodeJS.Process;
messageChannel?: string;
}): Endpoint;
}
- nodeProcess: Refers to a node process or node child_process.
- messageChannel: Used to separate channels in process communication. Different endpoints can be created with different
messageChannel
, defaulting to__COMLINK_MESSAGE_CHANNEL__
.
// child.ts
import { nodeProcessEndpoint } from 'comlink-adapters';
import { expose } from 'comlink';
expose(
(a: number, b: number) => a + b,
nodeProcessEndpoint({ nodeProcess: process })
);
// main.ts
import { fork } from 'node:child_process';
import { nodeProcessEndpoint } from 'comlink-adapters';
import { wrap } from 'comlink';
(async function () {
const add = wrap<(a: number, b: number) => number>(
nodeProcessEndpoint({ nodeProcess: fork('child.ts') })
);
const sum = await add(1, 2);
// output: 3
})();
Adapters:
socketIoEndpoint
is used to create anEndpoint
object on the client and server side with socket.io.
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ✅ | await new ProxyObj(); |
|
proxy function | ✅ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
|
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
Passing of MessagePort is not supported |
release | ✅ | proxyObj[comlink.releaseProxy](); |
socketIoEndpoint:
interface SocketIoEndpointOptions {
socket: ServerSocket | ClientSocket;
messageChannel?: string;
}
interface socketIoEndpoint {
(options: SocketIoEndpointOptions): Endpoint;
}
- socket: The socket instance created by
socket.io
orsocket.io-client
. - messageChannel: The event name used for sending/listening to comlink messages through socket instances. Different endpoints can be created by different messageChannel. The default is COMLINK_MESSAGE_CHANNEL.
// server.ts
import Koa from 'koa';
import { createServer } from 'http';
import { Server } from 'socket.io';
import { expose } from 'comlink';
import { socketIoEndpoint } from '@socket/adapters';
const app = new Koa();
const httpServer = createServer(app.callback());
const io = new Server(httpServer, {});
io.on('connection', (socket) => {
expose((a: number, b: number) => a + b, socketIoEndpoint({ socket }));
});
httpServer.listen(3000);
// client.ts
import { io } from 'socket.io-client';
import { wrap } from 'comlink';
import { socketIoEndpoint } from 'comlink-adapters';
(async function () {
const socket = io('ws://localhost:3000');
const add = wrap<(a: number, b: number) => number>(
socketIoEndpoint({ socket })
);
const sum = await add(1, 2);
// output: 3
})();
Adapters:
webSocketEndpoint
: Used for creating anEndpoint
object with WebSocket.
Features:
Feature | Support | Example | Description |
---|---|---|---|
set | ✅ | await proxyObj.someValue; |
|
get | ✅ | await (proxyObj.someValue = xxx); |
|
apply | ✅ | await proxyObj.applySomeMethod(); |
|
construct | ✅ | await new ProxyObj(); |
|
proxy function | ✅ | await proxyObj.applySomeMethod(comlink.proxy(() => {})); |
|
createEndpoint | ❌ | proxyObj[comlink.createEndpoint](); |
Not supported for MessagePort passing |
release | ✅ | proxyObj[comlink.releaseProxy](); |
webSocketEndpoint:
import type { WebSocket as LibWebSocket } from 'ws';
interface webSocketEndpoint {
(options: {
webSocket: WebSocket | LibWebSocket;
messageChannel?: string;
}): Endpoint;
}
- webSocket: A WebSocket instance or one created using ws library.
- messageChannel: Used to separate channels in WebSocket communication. Different endpoints can be created with different
messageChannel
, defaulting to__COMLINK_MESSAGE_CHANNEL__
.
// server.ts
import { WebSocketServer } from 'ws';
import { expose } from 'comlink';
import { webSocketEndpoint } from 'comlink-adapters';
const wss = new WebSocketServer({ port: 8888 });
wss.addListener('connection', (ws: WebSocket) => {
expose(
(a: number, b: number) => a + b,
webSocketEndpoint({ webSocket: ws })
);
});
// client.ts
import WebSocket from 'ws';
import { webSocketEndpoint } from 'comlink-adapters';
import { wrap } from 'comlink';
(async function () {
const ws = new WebSocket('ws://localhost:8888');
const add = wrap<(a: number, b: number) => number>(
webSocketEndpoint({ webSocket: ws })
);
const sum = await add(1, 2);
// output: 3
})();
Install
pnpm i
Development
cd core
pnpm run dev
cd examples/xxx-demo
pnpm run dev
# or
pnpm -r run dev
Build
cd core
pnpm run build