Skip to content

Implementation of comlink adapters for different application platforms

License

Notifications You must be signed in to change notification settings

kinglisky/comlink-adapters

Repository files navigation

comlink-adapters

Implementation of comlink adapters for different application platforms

English   |   简体中文

Introduction

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:

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.

Guide

Installation

# npm
npm i comlink comlink-adapters -S
# yarn
yarn add comlink comlink-adapters
# pnpm
pnpm add comlink comlink-adapters

Electron Adapters

Adapters:

  • electronMainEndpoint is used to create Endpoint objects in the main process.
  • electronRendererEndpoint is used to create Endpoint 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
})();

Figma Adapters

Adapters:

  • figmaCoreEndpoint is used to create Endpoint objects in the main thread of the Figma sandbox.
  • figmaUIEndpoint is used to create Endpoint 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;
}
// 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;
}
// 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
})();

Chrome Extensions Adapters

Adapters:

  • chromeRuntimePortEndpoint is used to create Endpoint objects for extensions based on long-lived connections.
  • chromeRuntimeMessageEndpoint is used to create Endpoint 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;
    }
);

Node Process Adapters

Adapters:

  • nodeProcessEndpoint: Used for creating an Endpoint 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
})();

Socket.io Adapters

Adapters:

  • socketIoEndpoint is used to create an Endpoint 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 or socket.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
})();

WebSocket Adapters

Adapters:

  • webSocketEndpoint: Used for creating an Endpoint 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
})();

Development

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

About

Implementation of comlink adapters for different application platforms

Resources

License

Stars

Watchers

Forks

Packages

No packages published