Coder Social home page Coder Social logo

comlink's Introduction

Comlink

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.

$ npm install --save comlink

Comlink in action

Browsers support & bundle size

Chrome 56+ Edge 15+ Firefox 52+ Opera 43+ Safari 10.1+ Samsung Internet 6.0+

Browsers without ES6 Proxy support can use the proxy-polyfill.

Size: ~2.5k, ~1.2k gzip’d, ~1.1k brotli’d

Introduction

On mobile phones, and especially on low-end mobile phones, it is important to keep the main thread as idle as possible so it can respond to user interactions quickly and provide a jank-free experience. The UI thread ought to be for UI work only. WebWorkers are a web API that allow you to run code in a separate thread. To communicate with another thread, WebWorkers offer the postMessage API. You can send JavaScript objects as messages using myWorker.postMessage(someObject), triggering a message event inside the worker.

Comlink turns this messaged-based API into a something more developer-friendly by providing an RPC implementation: Values from one thread can be used within the other thread (and vice versa) just like local values.

Examples

main.js

import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
async function init() {
  const worker = new Worker("worker.js");
  // WebWorkers use `postMessage` and therefore work with Comlink.
  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");
// importScripts("../../../dist/umd/comlink.js");

const obj = {
  counter: 0,
  inc() {
    this.counter++;
  },
};

Comlink.expose(obj);

main.js

import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
// import * as Comlink from "../../../dist/esm/comlink.mjs";
function callback(value) {
  alert(`Result: ${value}`);
}
async function init() {
  const remoteFunction = Comlink.wrap(new Worker("worker.js"));
  await remoteFunction(Comlink.proxy(callback));
}
init();

worker.js

importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
// importScripts("../../../dist/umd/comlink.js");

async function remoteFunction(cb) {
  await cb("A string from a worker");
}

Comlink.expose(remoteFunction);

When using Comlink with a SharedWorker you have to:

  1. Use the port property, of the SharedWorker instance, when calling Comlink.wrap.
  2. Call Comlink.expose within the onconnect callback of the shared worker.

Pro tip: You can access DevTools for any shared worker currently running in Chrome by going to: chrome://inspect/#workers

main.js

import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
async function init() {
  const worker = new SharedWorker("worker.js");
  /**
   * SharedWorkers communicate via the `postMessage` function in their `port` property.
   * Therefore you must use the SharedWorker's `port` property when calling `Comlink.wrap`.
   */
  const obj = Comlink.wrap(worker.port);
  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");
// importScripts("../../../dist/umd/comlink.js");

const obj = {
  counter: 0,
  inc() {
    this.counter++;
  },
};

/**
 * When a connection is made into this shared worker, expose `obj`
 * via the connection `port`.
 */
onconnect = function (event) {
  const port = event.ports[0];

  Comlink.expose(obj, port);
};

// Single line alternative:
// onconnect = (e) => Comlink.expose(obj, e.ports[0]);

For additional examples, please see the docs/examples directory in the project.

API

Comlink.wrap(endpoint) and Comlink.expose(value, endpoint?, allowedOrigins?)

Comlink’s goal is to make exposed values from one thread available in the other. expose exposes value on endpoint, where endpoint is a postMessage-like interface and allowedOrigins is an array of RegExp or strings defining which origins should be allowed access (defaults to special case of ['*'] for all origins).

wrap wraps the other end of the message channel and returns a proxy. The proxy will have all properties and functions of the exposed value, but access and invocations are inherently asynchronous. This means that a function that returns a number will now return a promise for a number. As a rule of thumb: If you are using the proxy, put await in front of it. Exceptions will be caught and re-thrown on the other side.

Comlink.transfer(value, transferables) and Comlink.proxy(value)

By default, every function parameter, return value and object property value is copied, in the sense of structured cloning. Structured cloning can be thought of as deep copying, but has some limitations. See this table for details.

If you want a value to be transferred rather than copied — provided the value is or contains a Transferable — you can wrap the value in a transfer() call and provide a list of transferable values:

const data = new Uint8Array([1, 2, 3, 4, 5]);
await myProxy.someFunction(Comlink.transfer(data, [data.buffer]));

Lastly, you can use Comlink.proxy(value). When using this Comlink will neither copy nor transfer the value, but instead send a proxy. Both threads now work on the same value. This is useful for callbacks, for example, as functions are neither structured cloneable nor transferable.

myProxy.onready = Comlink.proxy((data) => {
  /* ... */
});

Transfer handlers and event listeners

It is common that you want to use Comlink to add an event listener, where the event source is on another thread:

button.addEventListener("click", myProxy.onClick.bind(myProxy));

While this won’t throw immediately, onClick will never actually be called. This is because Event is neither structured cloneable nor transferable. As a workaround, Comlink offers transfer handlers.

Each function parameter and return value is given to all registered transfer handlers. If one of the event handler signals that it can process the value by returning true from canHandle(), it is now responsible for serializing the value to structured cloneable data and for deserializing the value. A transfer handler has be set up on both sides of the message channel. Here’s an example transfer handler for events:

Comlink.transferHandlers.set("EVENT", {
  canHandle: (obj) => obj instanceof Event,
  serialize: (ev) => {
    return [
      {
        target: {
          id: ev.target.id,
          classList: [...ev.target.classList],
        },
      },
      [],
    ];
  },
  deserialize: (obj) => obj,
});

Note that this particular transfer handler won’t create an actual Event, but just an object that has the event.target.id and event.target.classList property. Often, this is enough. If not, the transfer handler can be easily augmented to provide all necessary data.

Comlink.releaseProxy

Every proxy created by Comlink has the [releaseProxy]() method. Calling it will detach the proxy and the exposed object from the message channel, allowing both ends to be garbage collected.

const proxy = Comlink.wrap(port);
// ... use the proxy ...
proxy[Comlink.releaseProxy]();

If the browser supports the WeakRef proposal, [releaseProxy]() will be called automatically when the proxy created by wrap() gets garbage collected.

Comlink.finalizer

If an exposed object has a property [Comlink.finalizer], the property will be invoked as a function when the proxy is being released. This can happen either through a manual invocation of [releaseProxy]() or automatically during garbage collection if the runtime supports the WeakRef proposal (see Comlink.releaseProxy above). Note that when the finalizer function is invoked, the endpoint is closed and no more communication can happen.

Comlink.createEndpoint

Every proxy created by Comlink has the [createEndpoint]() method. Calling it will return a new MessagePort, that has been hooked up to the same object as the proxy that [createEndpoint]() has been called on.

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

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

Windows and Web Workers have a slightly different variants of postMessage. If you want to use Comlink to communicate with an iframe or another window, you need to wrap it with windowEndpoint().

window is the window that should be communicate with. context is the EventTarget on which messages from the window can be received (often self). targetOrigin is passed through to postMessage and allows to filter messages by origin. For details, see the documentation for Window.postMessage.

For a usage example, take a look at the non-worker examples in the docs folder.

TypeScript

Comlink does provide TypeScript types. When you expose() something of type T, the corresponding wrap() call will return something of type Comlink.Remote<T>. While this type has been battle-tested over some time now, it is implemented on a best-effort basis. There are some nuances that are incredibly hard if not impossible to encode correctly in TypeScript’s type system. It may sometimes be necessary to force a certain type using as unknown as <type>.

Node

Comlink works with Node’s worker_threads module. Take a look at the example in the docs folder.

Additional Resources


License Apache-2.0

comlink's People

Contributors

0xflotus avatar 3846masa avatar arjunyel avatar benjamind avatar elitescientist avatar felixfbecker avatar gruns avatar hellboywar avatar juanecabellob avatar kyubisation avatar lacolaco avatar laysent avatar michael42 avatar mrmadclown avatar mwszekely avatar mysticatea avatar notwoods avatar phenomnomnominal avatar philippotto avatar renovate-bot avatar renovate[bot] avatar robwormald avatar rocwind avatar ronhe avatar serg-io avatar sqs avatar surma avatar torch2424 avatar twoabove avatar xeoneux avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

comlink's Issues

Document the requirement of "async functions"

Currently the document only includes "Proxy" requirement, but it caught me off-guard that async functions are also used in the code.

Proxy support: iOS >= 10.2
Async function support: iOS >= 10.3

So even if proxy is polyfilled, one must also take care of async functions support too. This incurs the inevitable configuration of babel and regenerator, which is not always desirable.

It would be great if they can be replaced with plain functions returning promises. If not then it should be documented as well in the readme, like proxy polyfill.

Consider not using constructors for API proxy creation

Just subjective opinion here, but a line like this:

    // Note the usage of `await`:
    const app = await new api.App();

is a little confusing to me - it makes me think that api.App is a Promise, but then what type does the Promise resolve? I know it's a thennable that returns itself, but I think that ability of thennables probably goes over most people's heads. I think it'd be a little clearer with just a factory method:

    // Note the usage of `await`:
    const app = await api.createApp();

Support callbacks as function parameters

Hi,
I'm not sure if I'm doing something wrong or there's a bug. What I'm doing is trying to use observable to receive the data.
Working example1:

public ngAfterViewInit(): void {
        this.main().then((ifr: any) => {
            const api = Comlink.proxy(ifr.contentWindow);
            (api as any).CommunicationChannel.getInstance().then((_instance) => {
                 _instance.setUserToken('123').then(() => {
                     _instance.getUserToken().then(res => console.log(res));
                });
            });
        });
    }

    private main(): Promise<any> {
        console.log('in');
        const ifr = document.querySelector('iframe');
        return new Promise((resolve) => ifr.onload = () => {
            resolve(ifr);
        });
    }

Iframe:

import {Comlink} from 'comlinkjs';
import 'rxjs/add/observable/of';
import {Observable} from 'rxjs/Observable';

/**
 * Create a communication channel between the different apps
 */

export default class CommunicationChannel {
    // Instance for the singleton class
    private static _instance: CommunicationChannel;

    // User token
    private _userToken;

    constructor() {
        if (CommunicationChannel._instance) {
            throw new Error('Error: Instantiation failed: Use SingletonClass.getInstance() instead of new.');
        }
    }

    /**
     * Create the instance
     */
    public static getInstance() {
        if (!CommunicationChannel._instance) {
            CommunicationChannel._instance = new CommunicationChannel();
        }
        return Comlink.proxyValue(CommunicationChannel._instance);

    }

    public setUserToken(userToken: string) {
        this._userToken = userToken;
    }


    public getUserToken(): string {
        return this._userToken;
        });
    }

}
// Expose listens for RPC messages on endpoint and applies the operations to rootObj
Comlink.expose({CommunicationChannel}, self.parent);

Changing in observable:

public ngAfterViewInit(): void {
        this.main().then((ifr: any) => {
            const api = Comlink.proxy(ifr.contentWindow);
            (api as any).CommunicationChannel.getInstance().then((_instance) => {
                const userToken = Observable.fromPromise(_instance.getUserToken());
                userToken.subscribe(res => console.log(res));

            });
        });
    }

    private main(): Promise<any> {
        console.log('in');
        const ifr = document.querySelector('iframe');
        return new Promise((resolve) => ifr.onload = () => {
            resolve(ifr);
        });
    }

Iframe same as before but change this method:

public getUserToken(): Observable<string> {
        return new Observable((observer) => {
            setInterval(() => {
                observer.next('hello');
            }, 1000);
        });
    }

Error:

comlink.es6.js:65 Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'MessagePort': function (observer) {
            setInterval(function () {
                observer.next('hello');
            }...<omitted>... } could not be cloned.
    at MessagePort.<anonymous> (http://localhost:4201/vendor.bundle.js:2588:37)
    at <anonymous>

Hosted demo?

It would be nice to have a hosted demo people can play with.

Deconstruction does not work

const {exportA, exportB} = Comlink.proxy(worker);

exportA and exportB won’t work due to how the batching proxy works. Might have to ditch it.

Changelog

I have found v4 release, but I'm not sure what breaking changes are. Commit log is noisy because of a ton of renovate's PRs and merge commits.

Would you provide a changelog?

Transfer handlers broke after upgrading to v4

I'm encountering a bug that appeared after migrating from v3 to v4. It's related to babel's transpilation and proxying callback functions with Comlink.proxy, (Comlink.proxyValue in v3).

Some context:

I declared a transfer handler for Error

export function setTransferHandlers () {
    const errorTransferHandler = {
        canHandle (obj) {
            return obj instanceof Error
        },
        serialize (obj) {
            return [cloneErrorObject(obj), []]
        },
        deserialize (obj) {
            return Object.assign(Error(), obj)
        }
    }

    Comlink.transferHandlers.set('ERROR', errorTransferHandler)
}

Here's a block of my source code where the bug occurs.

const resolved = Promise.resolve()
function nextTickUnthrottled (fn, ...args) {
	resolved.then(() => {
		fn(...args)
	})
}

By this point, fn has been passed thru Comlink.proxy. In my debugging tests, arg is a single Error object. After the fn is called with the Error arg, I expect my error transfer handler to handle it, which it does if my source code is running in the browser. That is, I expect the below func to return true.

canHandle (obj) {
    return obj instanceof Error
},

However, since i'm using babel, this is the code that runs in the browser.

function nextTickUnthrottled (fn) {
        for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
        	args[_key - 1] = arguments[_key];
        }
        resolved.then(function () {
        	fn.apply(void 0, args)
        })
    }

The important bit is fn(...args) -> fn.apply(void 0, args). This results in a different code path for the proxy. Instead of the apply trap being called (which happens with my source), it results in the get trap being called, and THEN the apply trap.

The bug: After upgrading to v4, the obj param in canHandle(obj) is a single element array with the error obj, instead of the error obj itself.

Here's a snippet of comlink's source with my comments to illustrate the differing code path:

function createProxy(ep, path = []) {
    const proxy = new Proxy(new Function(), {
	// BABEL CODE calls 'get' first which returns a new proxy,
        // then the `apply` trap is called on the new proxy, 
        // but my error object has been inserted into an array
        get(_target, prop) {
            ...
            return createProxy(ep, [...path, prop]);
        },
		// SOURCE CODE calls 'apply' first
        apply(_target, _thisArg, rawArgumentList) {
            ...
        },
    });
    return proxy;
}

Let me know if you need more info.
Is this something comlink can work around?

TypeScript compilation fails for WebWorker

Compiling comlink.d.ts for a WebWorker fails with

Using tsc v2.8.3
node_modules/comlinkjs/comlink.d.ts(26,23): error TS2304: Cannot find name 'Window'.
node_modules/comlinkjs/comlink.d.ts(29,44): error TS2304: Cannot find name 'Window'.

My workaround is to remove "Window |" from method definitions for proxy and expose.

Dont we have another option?

More useful type definition

Currently, Comlink.proxy returns just Function type. It is not useful because any language services or editors cannot suggest await new Foo() interface.

My proposal is to define a type of Comlink.proxy function, called PromisedConstructor.

type Promised<T> = {
  [P in keyof T]: T[P] extends (...args: infer A) => infer R
  ? (...args: A) => Promise<R>
  : Promise<T[P]>
};
type PromisedConstructor<T>  = {
  new(...args: any): Promise<Promised<T>>;
};

TypeScript Playground

Usecase (w/ Angular)

import { Injectable } from '@angular/core';
import * as Comlink from 'comlinkjs';
type LoggerType = import('./logger').Logger; // import only types

type Promised<T> = {
  [P in keyof T]: T[P] extends (...args: infer A) => infer R
  ? (...args: A) => Promise<R>
  : Promise<T[P]>
};
type PromisedConstructor<T>  = {
  new(...args: any): Promise<Promised<T>>;
};

const LoggerProxy = Comlink.proxy(
  new (Worker as any)('./logger', { type: 'module' }) // global `Worker` workaround...
) as PromisedConstructor<LoggerType>;

@Injectable({
  providedIn: 'root'
})
export class LoggerService {
  async sendLog(value: any) {
    const logger = await new LoggerProxy(); // type safe
    await logger.log(value); // type-safe
  }
}

I've not found out yet how to extract the original constructor's arguments type. With the above type definition, await new LoggerProxy() 's constructor arguments are all recognized as any[].

Thanks.

MessageChannelAdapter leaks event listeners

The MessageChannelAdapter has a leak. If it is used to transfer a MessagePort, a new event listener is added that never gets cleaned up (because smc.addEventListener is called but smc.removeEventListener is never called). Depending on the usage pattern, this can mean that the array of event listeners grows very quickly and has a significant performance impact on the page.

I saw the v4 branch removes the MessageChannelAdapter. Do you have a solution to this problem in mind in v4?

I was prototyping a way to monkey-patch MessagePort#close to send closed notifications and perform cleanup, to solve this problem. Does that sound like the right solution to you?

(Not directly related to #63, but that also talks about GCing MessagePorts.)

Question about WebExtensions

Hiya!

I was wondering if comlink would be able to work with WebExtensions, since in WebExtensions we have a ton of different "threads", communication because a hassle. comlink would sound like a nice solution but alas I run into some issues:

background.ts:

import {Comlink} from 'comlink';
class MyClass {
    logSomething() {
        console.info('hiya');
    }
}
Comlink.expose(MyClass, window);

popup.ts:

import {Comlink} from 'comlink';
chrome.runtime.getBackgroundPage(async bgPage => {
    if (bgPage) {
        const MyClass = Comlink.proxy(bgPage);
        const instance = await new (MyClass as any)();
        setInterval(() => {
            instance.logSomething();
        }, 5000);
    }
});

Now on the background page I get a total of 2 events before it crashes:

The first one goes ok and looks like this:
image

The second one however:
image

since the next line slices the callPath property we end up with the following error/stacktrace:

Uncaught (in promise) TypeError: Cannot read property 'slice' of undefined at comlink.es6.js:71
(anonymous) 		@ comlink.es6.js:71
postMessage (async)
postMessage 		@ comlink.es6.js:180
(anonymous) 		@ comlink.es6.js:105
async function (async)
(anonymous) 		@ comlink.es6.js:66
15:29:08.559 

I was wondering if I'm doing something wrong or if WebExtensions (at least on chrome) has some implementation differences compared to other postMessage targets?

proxied instance returned via a promise fails to work...

In example below (using master version of comlink) the call to await instance.logSomething() in the resolved promise fails - it appears passing item through resolve calls through to comlink but gets back an incorrect object. Any ideas why this might be happening? Thanks.

// main.js
import * as Comlink from './comlink.js';

const pr = new Promise(async (resolve) => {
    const items = Comlink.proxy(new Worker("worker.js", {type: "module"}));
    const item2 = items['class2']; 
    resolve(item2);
});

pr.then(async (instance) => {
    // this fails with error: TypeError: instance.logSomething is not a function
    await instance.logSomething();
});

// worker.js
import { expose } from "./comlink.js";

const myValue = 42;

class MyClass {
    logSomething() {
        console.log(`myValue = ${myValue}`);
    }
}

const exposedItems = {
    class1: new MyClass(),
    class2: new MyClass(),
}
expose(exposedItems, self);

Q: async transformation to native promise

we are implementing comlink for site <-> iframe communication. Sadly we still need to support IE11 which has no support for async await. Async-await transpiling with babel adds about 28kb (unminified, nogzip) to the bundle.

I see the async keyword used twice. It it ok to convert them to native promises so we don't take this perf hit?
https://github.com/GoogleChromeLabs/comlink/blob/master/comlink.ts#L178
https://github.com/GoogleChromeLabs/comlink/blob/master/comlink.ts#L211

I don't mind doing the work.

Document browser compatibility

Could you please document the browser compatibility, in terms of major dependencies. E.g. is full ES2015 Proxy support required? Is there a way, when using a polyfill, to pre-enumerate known properties?

windowEndpoint smells bad.

WindowEndpoint smells bad. Why does it exist? Why can't you just detect that its the endpoint and manage it. As a consumer of the endpoint I should never have to care.

Problem with 2 way communication to & from iframe

I extended the iframe example a little, see https://gist.github.com/ssured/79471d3e78332e60567012969f716355

When running I get proper results and 2 errors:
image

Line 67 refers to

let that = await irequest.callPath.slice(0, -1).reduce((obj, propName) => obj[propName], rootObj as any);

Changing this line:

if (!event.data.id)

to if (!event.data.id || !event.data.callPath) fixes the problem, but I really don't know if thats OK.

Thanks for the library. Any help on getting 2 way communication done is greatly appreciated.

support exposing multiple classes/instances

@surma great library...

we have been playing with your library and wondering if you have ideas/plans to support exposing multiple classes/instances...

For example...

// main.js
var worker = new Worker('worker.js');

const MyClass = Comlink.proxy(worker, 'MyClass');
const instance = await new MyClass();
await instance.logSomething(); // logs “myValue = 42”

const OtherClass = Comlink.proxy(worker, 'OtherClass');
const otherInst = await new OtherClass();
await otherInst.logSomethingElse(); // logs “myOtherValue = 43”
// worker.js
const myValue = 42;
class MyClass {
  logSomething() {
    console.log(`myValue = ${myValue}`);
  }
}
Comlink.expose(MyClass, 'MyClass', self);

class OtherClass {
  logSomethingElse() {
    console.log(`myOtherValue = ${myValue + 1}`);
  }
}
Comlink.expose(OtherClass, 'OtherClass', self);

i played around and one simple method to implement this is to pass extra parameter string to proxy and expose method (as you can see in example above).

wondering if you have some thoughts on this use case and if this is on your roadmap?

thanks.

Changelog

It could be useful to have a changelog.md file to see what is changed from the previous version.
For example from 2.1 to 2.2 no idea what is the benefit to upgrade

Won't start up in Electron

Using Comlink within Electron causes Electron to crash, as Electron's Node-based background process doesn't have a MessagePort object. Comlink is referring to the MessagePort global when enumerating the transferrable types.

If we can find some other way to test for MessagePort objects which doesn't assume the MessagePort global exists, then Comlink could be used to communicate between Electron's background and renderer processes.

Proxies permanently leak memory

Edit: see comments below for real issue (memory leak)

Objects returned by Comlink calls can't be used for further calls. Example:

<!-- index.html -->
<!doctype html>
<script src="comlink.js"></script>
<script>
"use strict";
{
	const worker = new Worker("worker.js");
	const api = Comlink.proxy(worker);

	async function start()
	{
		const app = await new api.App();
		const ret = await app.getObject();
		await ret.log();		// error
	}
	
	start();
}
</script>
// worker.js
"use strict";

importScripts("comlink.js");

class ReturnedObject {
	log()
	{
		console.log("ReturnedObject log() method");
	}
}

class App {
	getObject()
	{
		return new ReturnedObject();
	}
}

Comlink.expose({App, ReturnedObject}, self);

In index.html, the call to await ret.log() throws TypeError: ret.log is not a function. ret has become an empty object (i.e. {}) which seems to be because it got posted raw. This appears to prevent using comlink with APIs that return other objects, which is quite a severe limit on the complexity of APIs that can be used.

I would be super interested if you figure out a way to fix this without leaking memory, since it's the same problem that is blocking via.js being usable in production.

gitignore dist

It's best practice to not commit compiled outputs because they only create unneeded diffs and merge conflicts, and there's a risk of the dist files getting out of sync with the source.

Maintenance of the webpack loader

It's been almost a year since v3.0 was released and the webpack loader still hasn't been bumped to version 3. With version 4 on the horizon, I was wondering if it would be possible to finally get some love for the webpack loader?

There is a PR open in the loader project currently GoogleChromeLabs/comlink-loader#11.

NPM / Packaging Issues

The current NPM distro has a couple of issues which make comlink hard to use w/ webpack et al:

  • the package.main / package.module fields aren't set, meaning import {Comlink} from 'comlinkjs' fails in most tools
  • the package.typings field is not set, meaning Typescript fails to resolve type definition information
  • the package includes a .ts source file, which makes a mess of the output file structure, as TS transpiles it, and a developer ends up with an output dir like:

input:

src/
src/test.ts
node_modules/
node_modules/comlink
node_modules/comlink/comlink.ts

output

out/
out/src/index.js
out/src/index.d.ts
out/node_modules/comlink/comlink.js
out/node_modules/comlink/comlink.d.ts

Suggested fixes:

  • add the following to the package.json
"main": "comlink.umd.js", //allows npm aware tools to find `require('comlinkjs');
"module": "comlink.es6.js", //allows ESM aware tools (webpack 2+) to do `import {Comlink} from 'comlinkjs'
"typings": "comlink.d.ts" //allows typescript to resolve the typedefs 
  • stop shipping .ts files to NPM (this is generally recommended, as it means developers don't have to recreate the specifics of the library's tsc environment)

Action Required: Fix Renovate Configuration

There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.

Error type: Preset name not found within published preset config (monorepo:angularmaterial). Note: this is a nested preset so please contact the preset author if you are unable to fix it yourself.

[question] - npm "comlink" vs "comlinkjs"

Just installed comlink via npm, I noticed there's another package comlinkjs.
Wanted to know what package to install, I don't want to get "stuck" with an unmaintained package when you inevitably decide to focus on one.. (?)

Cors Error in version 2.0

Version: 2.0.0
Demo:
index.html

<!DOCTYPE html>
<iframe src="http://127.0.0.1:2001">
</iframe>
<script src="https://cdn.jsdelivr.net/npm/comlinkjs@2/comlink.global.min.js"></script>
<script>
  async function main() {
    const ifr = document.querySelector('iframe');
    await new Promise(resolve => ifr.onload = resolve);
    const api = Comlink.proxy(ifr.contentWindow);
    const instanceOfComunication = await api.Comunication.getInstance();
    console.log(await instanceOfComunication.counter);
    await instanceOfComunication.inc();
    console.log(await instanceOfComunication.counter);
  }
  main();
</script>

iframe.html

<!doctype html>
<script src="https://cdn.jsdelivr.net/npm/comlinkjs@2/comlink.global.min.js"></script>
<script>
let instance = null;
class Comunication {
  constructor() {
    this._counter = 0;
  }

  static getInstance() {
      if (!instance)
          instance = new Comunication();
      return Comlink.proxyValue(instance);
  }

  get counter() {
      return this._counter;
  }

  inc() {
      this._counter++;
  }
}
Comlink.expose({Comunication}, self.parent);
</script>

live-server index.html --cors --port=2000
live-server iframe.html --cors --port=2001

Error:

comlink.global.min.js:1 Uncaught DOMException: Blocked a frame with origin "http://127.0.0.1:2001" from accessing a cross-origin frame.
    at j (https://cdn.jsdelivr.net/npm/comlinkjs@2/comlink.global.min.js:1:1577)
    at Object.c [as expose] (https://cdn.jsdelivr.net/npm/comlinkjs@2/comlink.global.min.js:1:376)
    at http://127.0.0.1:2001/:24:9
j @ comlink.global.min.js:1
c @ comlink.global.min.js:1
(anonymous) @ (index):24
comlink.global.min.js:formatted:83 Uncaught (in promise) DOMException: Blocked a frame with origin "http://localhost:2000" from accessing a cross-origin frame.
    at j (https://cdn.jsdelivr.net/npm/comlinkjs@2/comlink.global.min.js:1:1577)
    at Object.a [as proxy] (https://cdn.jsdelivr.net/npm/comlinkjs@2/comlink.global.min.js:1:55)
    at main (http://localhost:2000/:9:25)
    at <anonymous>
function j(a) {
        return 'Window' === a.constructor.name
    }

`npm run build` fails

I did npm i and then npm run build and got

> [email protected] build /home/ian/git/comlink
> rm -rf dist && mkdir dist && npm run compile && npm run mangle_global && npm run minify


> [email protected] compile /home/ian/git/comlink
> tsc --outDir dist -m none && mv dist/comlink.{,global.}js && mv dist/messagechanneladapter.{,global.}js && tsc --outDir dist -m es2015 && mv dist/comlink.{,es6.}js && mv dist/messagechanneladapter.{,es6.}js && tsc -d --outDir dist -m umd && mv dist/comlink.{,umd.}js && mv dist/messagechanneladapter.{,umd.}js

messagechanneladapter.ts(46,37): error TS2345: Argument of type '(event: MessageEvent) => void' is not assignable to parameter of type 'EventListener | EventListenerObject | undefined'.
  Type '(event: MessageEvent) => void' is not assignable to type 'EventListenerObject'.
    Property 'handleEvent' is missing in type '(event: MessageEvent) => void'.
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! [email protected] compile: `tsc --outDir dist -m none && mv dist/comlink.{,global.}js && mv dist/messagechanneladapter.{,global.}js && tsc --outDir dist -m es2015 && mv dist/comlink.{,es6.}js && mv dist/messagechanneladapter.{,es6.}js && tsc -d --outDir dist -m umd && mv dist/comlink.{,umd.}js && mv dist/messagechanneladapter.{,umd.}js`
npm ERR! Exit status 2
npm ERR! 
npm ERR! Failed at the [email protected] compile script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/ian/.npm/_logs/2017-11-23T14_54_50_488Z-debug.log
npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! [email protected] build: `rm -rf dist && mkdir dist && npm run compile && npm run mangle_global && npm run minify`
npm ERR! Exit status 2
npm ERR! 
npm ERR! Failed at the [email protected] build script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/ian/.npm/_logs/2017-11-23T14_54_50_508Z-debug.log

Workly

There is a similar package called Workly. It also provides a simple API to work with a class in a WebWorker and even simpler to use.

ComlinkWorkly
// main.js
import * as Comlink from "comlink";

const MyClass = Comlink.wrap(
  new Worker("worker.js")
);

const instance = await new MyClass();
await instance.logSomething();
// worker.js
import * as Comlink from "comlink";

const myValue = 42;
class MyClass {
  logSomething() {
    console.log(`myValue=${myValue}`);
  }
}
Comlink.expose(MyClass);
import * as workly from "workly";

const MyClass = workly.proxy(class {
  myValue = 42;
  logSomething() {
    console.log(`myValue=${this.myValue}`);
  }
});

const instance = await new MyClass();
await instance.logSomething();

There is also an expose API in Workly but it isn't required for this example

What are the differences and advantages of Comlink over Workly?

Possible to know when remote interface initialised

Would it be possible to know from a proxy, if the backing worker, or iframe is already up and will respond to your requests.

I have situation when i want wait until iframe is ready before i start calling remote api, ideally something like this

var remoteApi = await Comlink.proxy(iframe.contentWindow)
// now i am sure that iframe loaded and i can call the remote api's

Currently to do the same, i am forced to sent a postMessage from iframe when its loaded, and listen for this in main page, and then start using the remote api's using Comlink

Inaccurate typings for nested object properties

It seems like comlink will only return a Promise for the last property in a property access chain:

comlink/comlink.ts

Lines 463 to 475 in 306a9d8

get(_target, property, proxy) {
if (property === "then" && callPath.length === 0) {
return { then: () => proxy };
} else if (property === "then") {
const r = cb({
type: "GET",
callPath
});
return Promise.resolve(r).then.bind(r);
} else {
return cbProxy(cb, callPath.concat(property), (<any>_target)[property]);
}
},

Meaning a call like this should be possible:

const foo = comlink.proxy()
await foo.bar.qux()

However, the TypeScript typings currently always convert all properties to Promises:

: Promisify<T[P]>

Meaning you have to write this:

await (await foo.bar).qux()

I need to do a lot of these nested method calls without intermediate Promise(s). How could we make them in a type safe way?

I think the other side could make the nested bar object explicitly a proxy value with comlink.proxy() so it is never Promisified:

class Foo {
  public bar = comlink.proxy({
    qux: () => 123
  })
}

and comlink.proxy() values would need to be exposed to the type system:

export interface ProxyValue {
  [proxyValueSymbol]: true
}
export function proxyValue<T>(obj: T): T & ProxyValue

Then ProxifiedObject could take that into consideration and not wrap properties into Promises that are proxied values:

declare type ProxiedObject<T> = {
    [P in keyof T]: T[P] extends (...args: infer Arguments) => infer R // if is method
        ? (...args: Arguments) => Promisify<R> // promisify return type
        : (T[P] extends ProxyValue // if it's a proxied value
              ? T[P] // use raw type
              : Promisify<T[P]>) // regular prop, wrap in Promise
}

Which would make this work:

const foo = comlink.proxy<Foo>()
await foo.bar.qux()

Compatibility with React-Native

Hi,
I have found Comlink very useful for RPC between a React-Native app and a web app inside a WebView component.
To make it work, I had to make a few adjustments:

  1. Write an RN end-point that supports 3 functions: AddEventListener, onMessage that executes listeners and can be transferred to the WebView, and send that wraps the WebView's postMessage method.
  2. Write a very simple web endpoint that just wraps window.postMessage and document.addEventListener into an object that supports send and addEventListener. That's because of the way RN injects the postMessage function, and since it only supports string messages.
  3. Drop the call for MessagePort from the RN Comlink module, since it's undefined.
  4. Transpile the Comlink ES6 module into ES5, since ES6 dependencies are unsupported by create-react-app.

I was wondering, if you think that Comlink should support this usage out of the box, or that it would be better to fork it and make the adaptations mentioned above.

Thanks!

New release?

Would you mind cutting a new release? Thanks!

Explain/give examples when a web worker makes sense

Given that a web worker is not "free" and you need to "hop" you data around it should be explained when and when not to use web workers.

Even though they are available for quite some time the adoption ratio is pretty slow (iirc). So given a little head start seems like a good idea 🤗

This library makes it almost "too easy" to use them (which is awesome) but I'm still struggling to find a good use case for them in my day to day tasks 🙈

Publish source maps and declaration maps

Enabling sourceMap allows stepping into comlink code when debugging and to have stack traces that point to the real source, if the source is also included in the npm package either as files or inline in the source map.
declarationMap in tsconfig.json allows tools to map the .d.ts files back to the source, which for example allows cross-repository go-to-definition and find-all-references on sourcegraph.com.

This is currently difficult to add because the package.json is copied into dist/, and TypeScript would then point to ../comlink.ts as the source, which doesn't exist because .. would be node_modules. Why not publish the package root? Comlink is just a single file so it's always imported through the main import anyway.

An example using Service Worker?

When I saw this I assumed I could slot a service worker in the same place a web worker was being used in the example, but service workers don't have self.postMessage, so it doesn't work. I've got it working now by hooking up the proxy through a MessagePort in a message event - is that the best way to do it?

Either way, it would be great to have an example showing the best practise for this.

Error: endpoint does not have all of addEventListener, removeEventListener and postMessage defined

My project is split into two modules, one for the web worker and one for the SPA.
In my Worker I have something similar to the following code:

import * as Comlink from 'comlinkjs'

// [...] Import other stuff


export class Integration {
  ping = () => Promise.resolve('pong')
 // Other stuff here
}

console.log('[Worker] Starting API connection')
customEvent.init().then(() => {
  console.log('[Worker] Connected to the network')
})

Comlink.expose(Integration, self)

This gets transpired and packed with Babel 7 and Webpack.

Then In my SPA instance I have

import * as Comlink from 'comlinkjs'
const workerProcess = typeof Worker !== 'undefined' ? new Worker('web-worker.js') : {}
const service = Comlink.proxy(workerProcess)

function isWorkerAlive () {
  return service.ping().then((value) => value === 'pong')
}

But when I load it, I get an error:

Error: endpoint does not have all of addEventListener, removeEventListener and postMessage defined

What am I doing wrong?
In the documentation there is no explanation on how to import the module, just how to install it. it is not clear to me and I am not sure if I am missing something. Can anybody help?

Angular 6 Failed to execute 'postMessage' on 'Worker'

Hello, I've been trying to use this example to make comlinkjs work in Angular: https://medium.com/lacolaco-blog/enjoyable-webworkers-in-angular-41cfeb0e6519
(example is working when checked out: example

However I can't seem to replicate the example and I always get this error in my project

ERROR Error: Uncaught (in promise): DataCloneError: Failed to execute 'postMessage' on 'Worker': Symbol(Symbol.toPrimitive) could not be cloned.
Error: Failed to execute 'postMessage' on 'Worker': Symbol(Symbol.toPrimitive) could not be cloned.

Here is my setup

//generate-data-worker.ts

import { expose } from 'comlinkjs';

export class GenerateData {
  generateData(source: string) {
    return new Promise((resolve, reject) => {
      resolve( "TEST" );
    });
  }
}

expose({GenerateData}, self);
//generate-data.service.ts
import {Injectable} from '@angular/core';
import {proxy} from 'comlinkjs';


const GenerateDataWokrer = proxy<
  typeof import('../worker/generate-data').GenerateData
  >(
  new (Worker as any)('../worker/generate-data', { type: 'module' })
);

@Injectable({
  providedIn: 'root'
})
export class GenerateDataService {
  async generateData(source: string): Promise<any> {
    const worker = await new GenerateDataWokrer();
    return await worker.generateData(source);
  }
}

calling it as:

this.genDataService.generateData(``).then((value) => {
      console.log(value)
    });

Can you help me understand from where is the error coming from?

proxyValues inside of argument objects are not serialized correctly

Problem: proxyValues inside of argument objects do not work.

Example:

  // Here "git" is a Comlink remote object
  function handleProgress (e) {
    store.progress[owner + '/' + name] = e.loaded / e.total
    update()
  }
  return git.clone({
    dir,
    depth: 1,
    ref,
    onprogress: Comlink.proxyValue(handleProgress),
    url: repo
  })

Expected: the handleProgress callback would be called.

Actual:

client.js:165 Uncaught (in promise) DOMException: Failed to execute 'postMessage' on 'MessagePort': function handleProgress (e) {
    store.progress[owner + '/' + name] = e.loaded / e.total
    update()
  } could not be cloned.

Suggested Solution: Automatically wrap anything that throws a "could not be cloned" error in a ProxyValue. Then callbacks everywhere should Just Work (TM).

How to proxy Observable?

Functions that return Promises work great with comlink, but we have a lot of APIs that use RxJS Observables. Currently these either get cloned (which is not useful) or one needs to proxy them with proxyValue(). However, then methods like .pipe() don't return Observable<T> anymore, but Promise<Observable<T>>.

How could we make it so that an Observable is not "promisified"?

Iframe communication

Hi All,
I just saw this amazing library and I'm trying to use it but honestly I'm struggling a bit.

On the Iframe I did (application1):

export class Comunication {
    private _counter;

    constructor() {
        this._counter = 0;
    }

    get counter() {
        return this._counter;
    }

    inc() {
        debugger
        this._counter++;
    }
}
import {Comlink} from 'comlinkjs';
import {Comunication} from '../../common/comlink/Comunication';
export class LoginCmpstComponent {
constructor() {
            Comlink.expose({Comunication}, Comlink.windowEndpoint(self.parent));
}
public login(): void {
   const com: Comunication = new Comunication();
   com.inc();
}
}

In the parent (application2):

public ngAfterViewInit(): void {
        this.main().then(res => console.log('comLink', res));
    }

public async main() {
        const ifr = document.querySelector('iframe');
        const api = Comlink.proxy(Comlink.windowEndpoint(ifr.contentWindow));
        console.log(`${await api.prototype.counter()}`);   <---- what I should put here ???
    }

Can you please tell me what I should do in mine main()?

thanks

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.