Comlink

repository·main·Indexed 11 days ago

https://github.com/googlechromelabs/comlink

A tiny (1.1kB) RPC (Remote Procedure Call) implementation for Web Workers that uses ES6 Proxies to simplify communication. Comlink allows developers to interact with objects in different threads, Service Workers, or iframes as if they were local asynchronous function calls, hiding the complexity of the postMessage API. Version 4.4.2.

Tokens
4.3K
Snippets
15
Records
22
Agent score
93%

What's inside Comlink

  1. How Comlink works: RPC over postMessage

    main

    Comlink is a tiny (1.1kB) RPC (Remote Procedure Call) implementation for the Web Workers postMessage API. It uses ES6 Proxies to remove the mental barrier of manual messaging. Instead of sending and receiving messages, you can interact with values in a different thread as if they were local.

    Key Mental Model: When you use a proxy returned by Comlink.wrap(), all property access and function invocations are inherently asynchronous. As a rule of thumb: If you are using the proxy, put await in front of it.

  2. Run a simple function in a worker with Comlink

    main

    Comlink allows you to treat a Web Worker as if it were a local object by wrapping the worker's endpoint. This enables you to call functions defined inside the worker directly from your main thread as if they were asynchronous methods on a local object.

    To implement this pattern:

    1. In the Worker: Use Comlink.expose(object, endpoint) to make an object available to the main thread.
    2. In the Main Thread: Use Comlink.wrap(worker) to create a proxy object that mirrors the exposed object's interface.
    // In the worker (worker.js)
    import * as Comlink from 'comlink';
    
    const api = {
      add(a, b) {
        return a + b;
      },
    };
    
    Comlink.expose(api);
    
    // In the main thread (main.js)
    import * as Comlink from 'comlink';
    
    const worker = new Worker('worker.js');
    const api = Comlink.wrap(worker);
    
    async function run() {
      const result = await api.add(1, 2);
      console.log(result); // 3
    }
    
    run();
  3. Use remote event listeners and event targets with TransferHandlers

    main

    Comlink allows you to use remote event listeners or event targets by implementing a TransferHandler. This mechanism enables communication patterns where an object on one side of the Comlink boundary can trigger events that are handled on the other side, effectively bridging the gap between local and remote event systems.

    /* This example demonstrates how a TransferHandler enables remote event listeners or event targets. */
  4. Use Comlink with SharedWorker

    main

    When using SharedWorker, you must account for the port property used for communication:

    1. In the Main Thread: Call Comlink.wrap(worker.port) instead of wrapping the worker instance directly.
    2. In the Worker: Call Comlink.expose(value, port) inside the onconnect callback, using the port provided by the connection event.
    // main.js
    import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
    async function init() {
      const worker = new SharedWorker("worker.js");
      // Use the port property
      const obj = Comlink.wrap(worker.port);
      await obj.inc();
    }
    // worker.js
    importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
    const obj = { counter: 0, inc() { this.counter++; } };
    
    onconnect = function (event) {
      const port = event.ports[0];
      Comlink.expose(obj, port);
    };
  5. Pass callbacks between a website and a worker

    main
    Comlink allows you to pass functions (callbacks) between the main thread and a worker. When you pass a function via Comlink, it is automatically wrapped in a proxy, allowing the receiving side to call it as if it were a local function. Note that because these functions are being called across a message boundary, they behave asynchronously.
  6. Use callbacks with Comlink.proxy()

    main

    Since functions are neither structured cloneable nor transferable, you cannot pass them directly through postMessage. To pass a function (like a callback) from the main thread to a worker, wrap it in Comlink.proxy(callback). This sends a proxy instead of attempting to clone the function.

    // main.js
    import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
    function callback(value) {
      alert(`Result: ${value}`);
    }
    async function init() {
      const remoteFunction = Comlink.wrap(new Worker("worker.js"));
      // Wrap the callback in a proxy so it can be sent to the worker
      await remoteFunction(Comlink.proxy(callback));
    }
    init();
    // worker.js
    importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
    async function remoteFunction(cb) {
      await cb("A string from a worker");
    }
    Comlink.expose(remoteFunction);
  7. Run a simple function in a Web Worker

    main

    To use Comlink with a standard Web Worker, follow these steps:

    1. In the Worker (worker.js): Define your object and use Comlink.expose(obj) to make it available.
    2. In the Main Thread (main.js): Create the worker, then use Comlink.wrap(worker) to get a proxy object that allows you to call methods on the worker asynchronously.
    // main.js
    import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
    async function init() {
      const worker = new Worker("worker.js");
      const obj = Comlink.wrap(worker);
      alert(`Counter: ${await obj.counter}`);
      await obj.inc();
      alert(`Counter: ${await obj.counter}`);
    }
    init();
    // worker.js
    importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
    const obj = {
      counter: 0,
      inc() {
        this.counter++;
      },
    };
    Comlink.expose(obj);
  8. Set up Comlink between a website and a Service Worker

    main

    This example demonstrates how to establish a Comlink connection where the Service Worker acts as the exposed endpoint and the website acts as the client. In this pattern, you use Comlink.expose inside the Service Worker to make its functions/objects available, and Comlink.wrap in the main thread (the website) to interact with them via a proxy.

    // In the Service Worker:
    import * as Comlink from 'comlink';
    
    const serviceWorkerApi = {
      // your API methods here
    };
    
    Comlink.expose(serviceWorkerApi);
    
    // In the Website (Main Thread):
    import * as Comlink from 'comlink';
    
    const worker = navigator.serviceWorker.controller;
    const api = Comlink.wrap(worker);
    
    // Use the api as if it were local
    await api.someMethod();
  9. Manage Proxy lifecycle with releaseProxy and finalizer

    main

    proxy[Comlink.releaseProxy]()

    Manually detaches the proxy and the exposed object from the message channel, allowing both ends to be garbage collected. If the browser supports WeakRef, this is called automatically when the proxy is garbage collected.

    Comlink.finalizer

    If an exposed object has a property named [Comlink.finalizer], this function is invoked when the proxy is released (either manually or via GC). Note that once the finalizer runs, the endpoint is closed and no further communication is possible.

    const proxy = Comlink.wrap(port);
    // ... use the proxy ...
    proxy[Comlink.releaseProxy]();
  10. Create new endpoints and Window communication

    main

    proxy[Comlink.createEndpoint]()

    Returns a new MessagePort that is hooked up to the same object as the proxy. This allows you to pass a new communication channel to another thread.

    const port = myProxy[Comlink.createEndpoint]();
    const newProxy = Comlink.wrap(port);

    Comlink.windowEndpoint(window, context = self, targetOrigin = "*")

    Used to communicate with an iframe or another window. Since windows use a slightly different postMessage variant than Workers, you must wrap the window with this method.