npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

worker-async

v1.1.0

Published

A promise-based rpc interface for web workers

Downloads

76

Readme

worker-async

Provides a simple promise-based RPC interface to communicate between web workers and the main thread, instead of messing around with postMessage and event listeners:

// on the worker thread:
import promisify from 'worker-async';

promisify(self, {
    async increment(num) {
        return num + 1;
    }
});

// on the main thread:
import promisify from 'worker-async';

const worker = new Worker(...);
const { remote } = promisify(worker);
await remote.increment(42); // returns 43

Function arguments and return values can be anything supported by the browser's structured clone algorithm. Errors thrown by the function will show up at caller with the correct message, stack, and other properties.

Install

npm install worker-async

Full-duplex communication

worker-async allows the worker thread to call the main thread in the same way:

// on the worker thread:
import promisify from 'worker-async';

const { remote: main } = promisify(self, {
    async increment(num) {
        await main.log(`received ${num}`); // call `log` in the main thread
        return num + 1;
    }
});

// on the main thread:
import promisify from 'worker-async';

const worker = new Worker(...);
const { remote } = promisify(worker, {
    // expose `log` to the worker:
    async log(str) {
        console.log(`LOG: ${str}`);
    }
}); 
    
await remote.increment(42); // logs 42, returns 43

This can be very useful in scenarios where functionality is only available on one side of the connection (e.g., DOM manipulation, analytics tracking etc.)

Multiple results with Async Generators

// on the worker thread:
import promisify from 'worker-async';

promisify(self, {
    async *getItems() {
        let i = 0;
        while (true) {
            yield i++;
        }
    }
});

// on the main thread:
import promisify from 'worker-async';

const worker = new Worker(...);
const { remote } = promisify(worker);
for await (const num of remote.getItems()) {
    console.log(num);
    if (num > 10) break;
}

Async generators are automatically supported with the same semantics as normal javascript (i.e., the next item is not fetched till you ask for it; if you exit the loop early, the remote side will exit as well.)

Webpack

See full working example with webpack/worker-loader/typescript/next.js here. In particular, worker-loader requires us to create the Worker instance in a slightly different way:

-   const worker = new Worker(...);
+   const worker = require('./example.worker')();
    const { remote } = promisify(worker);

Typings

This library is written in Typescript; you get full typings for everything:

// on the worker thread:
import promisify from 'worker-async';

class Remote {
    async increment(num: number) {
        return num + 1;
    }
}

promisify(self, new Remote());

// on the main thread:
import promisify from 'worker-async';
import { Remote } from './remote'; // imported only for typings
// Remote is not called directly, so it will _not_ be included in the main bundle.

const worker = new Worker(...);
const { remote } = promisify<Remote>(worker);
await remote.increment('abc'); // type-mismatch error

Compatibility

The default import uses Proxy and ES2017 features supported by all evergreen browsers (Chrome, Firefox, Safari, Edge.) If you need to support IE or other old browsers, you can use this alternate import that targets ES3 and doesn't use proxies:

import MessageHandler from 'worker-async/lib/es3/messageHandler';

const handler = new MessageHandler(worker, host);
worker.addEventListener('message', handler.onMessage);

await handler.bind('increment')(42);

This library does not use evals, so you don't need to worry about CSP.

Complex interfaces

The second argument (host) passed to promisify supports the following types:

  • A function:

    // on the worker thread:
    promisify(self, num => num + 1);
    
    // on the main thread:
    const { remote } = promisify(worker);
    await remote(42); // returns 43
  • A plain object: all functions in the object and its children are exposed to the other side:

    promisify(self, {
        increment: num => num + 1,
        http: {
            async fetch(options) { ... }
        }
    });
    
    // on the main thread:
    await remote.http.fetch(...);
  • A class object: in addition to the object and its children, all functions in its prototype chain are also exposed:

    // on the main thread:
    class BaseLogger {
        async log(str: string) {
            console.log(`LOG: ${str}`);
        }
    }
    
    class ChildLogger extends BaseLogger {
    }
    
    class Main {
        logger = new ChildLogger();
    }
    
    promisify(worker, new Main());
    
    // on the worker thread:
    const { remote: main } = promisify(self);
    await main.logger.log('foo');

Not supported

Since we have to make a remote/async call, anything that is accessed synchronously cannot be exposed to the other side, for example:

class Main {
    value = 42; // primitive values are not exposed

    get foo() { } // getters/setters are synchronous, so not exposed
}

Multiple promisifies

You can create multiple promisified objects on the same worker, with each host object getting its own stream so the RPC calls don't conflict with each other. This is useful in scenarios where you need to control the execution state while the remote call is running. This is normally done by passing callback functions or event emitters, but since postMessage doesn't allow us to send complex objects or functions, we need to send over a reference (i.e., the stream ID) instead.

The following example demonstrates this pattern by using an AbortController to cancel a remote method:

// in the worker thread:
promisify(self, {
    async fetch(abortStream: number) {
        // we'd normally take in an AbortSignal, but since it can't be
        // sent to a worker, we create a new AbortController here and 
        // expose it to the main thread at a predetermined stream:
        const ctrl = new AbortController();
        const { handler } = promisify(self, ctrl);
        handler.stream = abortStream;
        try {
            // wait for host thread to abort:
            await new Promise(r => setTimeout(r, 1000));
            
            return ctrl.signal.aborted;
        } finally {
            // stop listening to messages in this stream. REQUIRED:
            // you'll have a memory leak in the worker otherwise.
            handler.stop();
        }
    }
})

// in the main thread:
const worker = new Worker(...);
const { remote } = promisify(worker);

fetch() {
    // create a promisified remote AbortController and have it talk on 
    // a separate stream so it doesn't conflict with `remote`:
    const { remote: ctrl, handler } = promisify<AbortController>(worker);
    try {
        const abortStream = handler.stream = Math.random(); // can be any number/stream
        const promise = remote.fetch(abortStream);

        // while fetch is executing, abort the remote controller:
        ctrl.abort();

        console.log('isAborted? ', await promise); // logs `true`
    } finally {
        // stop listening to messages in this stream. REQUIRED:
        // you'll have a memory leak in the worker otherwise.
        handler.stop();
    }
}

// in the real world, we'd probably tie in the remote controller
// with an already existing AbortSignal:
signal.addEventListener('abort', () => ctrl.abort());