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-adaptersAdapters:
electronMainEndpointis used to createEndpointobjects in the main process.electronRendererEndpointis used to createEndpointobjects in the rendering process.
Features:
| Feature | Support | Example | Description |
|---|---|---|---|
| get | ✅ | await proxyObj.someValue; |
|
| set | ✅ | 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:
figmaCoreEndpointis used to createEndpointobjects in the main thread of the Figma sandbox.figmaUIEndpointis used to createEndpointobjects in the Figma UI process.
Features:
| Feature | Support | Example | Description |
|---|---|---|---|
| get | ✅ | await proxyObj.someValue; |
|
| set | ✅ | 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
originin figma.ui.postMessage, default is*. - checkProps: Used to check the origin in
propsreturned 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:
targetOriginconfiguration 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:
chromeRuntimePortEndpointis used to createEndpointobjects for extensions based on long-lived connections.chromeRuntimeMessageEndpointis used to createEndpointobjects for extensions based on simple one-off requests.
Features:
| Feature | Support | Example | Description |
|---|---|---|---|
| get | ✅ | await proxyObj.someValue; |
|
| set | ✅ | 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 anEndpointobject within a node process.
Features:
| Feature | Support | Example | Description |
|---|---|---|---|
| get | ✅ | await proxyObj.someValue; |
|
| set | ✅ | 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:
socketIoEndpointis used to create anEndpointobject on the client and server side with socket.io.
Features:
| Feature | Support | Example | Description |
|---|---|---|---|
| get | ✅ | await proxyObj.someValue; |
|
| set | ✅ | 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.ioorsocket.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 anEndpointobject with WebSocket.
Features:
| Feature | Support | Example | Description |
|---|---|---|---|
| get | ✅ | await proxyObj.someValue; |
|
| set | ✅ | 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 iDevelopment
cd core
pnpm run devcd examples/xxx-demo
pnpm run dev
# or
pnpm -r run devBuild
cd core
pnpm run build