{"id":23730509,"url":"https://github.com/vishwam/worker-async","last_synced_at":"2026-02-19T14:30:18.046Z","repository":{"id":57399212,"uuid":"230041712","full_name":"vishwam/worker-async","owner":"vishwam","description":"A simple promise-based interface to communicate between web workers and the main thread","archived":false,"fork":false,"pushed_at":"2021-02-16T01:11:25.000Z","size":34,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-31T10:47:59.268Z","etag":null,"topics":["async","browser","promise","rpc","web-worker","worker"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vishwam.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-12-25T04:33:45.000Z","updated_at":"2024-06-07T12:15:46.000Z","dependencies_parsed_at":"2022-09-12T18:20:33.742Z","dependency_job_id":null,"html_url":"https://github.com/vishwam/worker-async","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vishwam%2Fworker-async","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vishwam%2Fworker-async/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vishwam%2Fworker-async/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vishwam%2Fworker-async/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vishwam","download_url":"https://codeload.github.com/vishwam/worker-async/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239841730,"owners_count":19705980,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","browser","promise","rpc","web-worker","worker"],"created_at":"2024-12-31T02:38:53.836Z","updated_at":"2026-02-19T14:30:17.995Z","avatar_url":"https://github.com/vishwam.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# worker-async\nProvides a simple promise-based RPC interface to communicate between web workers and the main thread, instead of messing around with postMessage and event listeners:\n```js\n// on the worker thread:\nimport promisify from 'worker-async';\n\npromisify(self, {\n    async increment(num) {\n        return num + 1;\n    }\n});\n\n// on the main thread:\nimport promisify from 'worker-async';\n\nconst worker = new Worker(...);\nconst { remote } = promisify(worker);\nawait remote.increment(42); // returns 43\n```\n\nFunction arguments and return values can be anything supported by the browser's [structured clone algorithm](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). Errors thrown by the function will show up at caller with the correct message, stack, and other properties.\n\n\n## Install\n```sh\nnpm install worker-async\n```\n\n## Full-duplex communication\nworker-async allows the worker thread to call the main thread in the same way:\n```js\n// on the worker thread:\nimport promisify from 'worker-async';\n\nconst { remote: main } = promisify(self, {\n    async increment(num) {\n        await main.log(`received ${num}`); // call `log` in the main thread\n        return num + 1;\n    }\n});\n\n// on the main thread:\nimport promisify from 'worker-async';\n\nconst worker = new Worker(...);\nconst { remote } = promisify(worker, {\n    // expose `log` to the worker:\n    async log(str) {\n        console.log(`LOG: ${str}`);\n    }\n}); \n    \nawait remote.increment(42); // logs 42, returns 43\n```\n\nThis can be very useful in scenarios where functionality is only available on one side of the connection (e.g., DOM manipulation, analytics tracking etc.)\n\n\n## Multiple results with Async Generators\n```js\n// on the worker thread:\nimport promisify from 'worker-async';\n\npromisify(self, {\n    async *getItems() {\n        let i = 0;\n        while (true) {\n            yield i++;\n        }\n    }\n});\n\n// on the main thread:\nimport promisify from 'worker-async';\n\nconst worker = new Worker(...);\nconst { remote } = promisify(worker);\nfor await (const num of remote.getItems()) {\n    console.log(num);\n    if (num \u003e 10) break;\n}\n```\n\nAsync 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.)\n\n\n## Webpack\nSee full working example with webpack/worker-loader/typescript/next.js [here](https://github.com/vishwam/worker-async-nextjs#readme). In particular, [worker-loader](https://github.com/webpack-contrib/worker-loader) requires us to create the Worker instance in a slightly different way:\n```diff\n-   const worker = new Worker(...);\n+   const worker = require('./example.worker')();\n    const { remote } = promisify(worker);\n```\n\n\n## Typings\nThis library is written in Typescript; you get full typings for everything:\n```ts\n// on the worker thread:\nimport promisify from 'worker-async';\n\nclass Remote {\n    async increment(num: number) {\n        return num + 1;\n    }\n}\n\npromisify(self, new Remote());\n\n// on the main thread:\nimport promisify from 'worker-async';\nimport { Remote } from './remote'; // imported only for typings\n// Remote is not called directly, so it will _not_ be included in the main bundle.\n\nconst worker = new Worker(...);\nconst { remote } = promisify\u003cRemote\u003e(worker);\nawait remote.increment('abc'); // type-mismatch error\n```\n\n\n## Compatibility\nThe default import uses [Proxy](https://caniuse.com/#feat=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](https://zombiecodekill.com/2016/02/11/ecmascript-3-5-and-2015-browser-compatibility-cheat-sheet/) and doesn't use proxies:\n```js\nimport MessageHandler from 'worker-async/lib/es3/messageHandler';\n\nconst handler = new MessageHandler(worker, host);\nworker.addEventListener('message', handler.onMessage);\n\nawait handler.bind('increment')(42);\n```\n\nThis library does not use evals, so you don't need to worry about [CSP](https://developer.mozilla.org/docs/Web/HTTP/Headers/Content-Security-Policy).\n\n\n## Complex interfaces\nThe second argument (`host`) passed to `promisify` supports the following types:\n* A function:\n    ```js\n    // on the worker thread:\n    promisify(self, num =\u003e num + 1);\n\n    // on the main thread:\n    const { remote } = promisify(worker);\n    await remote(42); // returns 43\n    ```\n\n* A plain object: all functions in the object and its children are exposed to the other side:\n    ```js\n    promisify(self, {\n        increment: num =\u003e num + 1,\n        http: {\n            async fetch(options) { ... }\n        }\n    });\n\n    // on the main thread:\n    await remote.http.fetch(...);\n    ```\n\n* A class object: in addition to the object and its children, all functions in its prototype chain are also exposed:\n    ```ts\n    // on the main thread:\n    class BaseLogger {\n        async log(str: string) {\n            console.log(`LOG: ${str}`);\n        }\n    }\n\n    class ChildLogger extends BaseLogger {\n    }\n\n    class Main {\n        logger = new ChildLogger();\n    }\n\n    promisify(worker, new Main());\n\n    // on the worker thread:\n    const { remote: main } = promisify(self);\n    await main.logger.log('foo');\n    ```\n\n### Not supported\nSince we have to make a remote/async call, anything that is accessed synchronously cannot be exposed to the other side, for example:\n```js\nclass Main {\n    value = 42; // primitive values are not exposed\n\n    get foo() { } // getters/setters are synchronous, so not exposed\n}\n```\n\n## Multiple promisifies\nYou 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. \n\nThe following example demonstrates this pattern by using an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to cancel a remote method:\n```ts\n// in the worker thread:\npromisify(self, {\n    async fetch(abortStream: number) {\n        // we'd normally take in an AbortSignal, but since it can't be\n        // sent to a worker, we create a new AbortController here and \n        // expose it to the main thread at a predetermined stream:\n        const ctrl = new AbortController();\n        const { handler } = promisify(self, ctrl);\n        handler.stream = abortStream;\n        try {\n            // wait for host thread to abort:\n            await new Promise(r =\u003e setTimeout(r, 1000));\n            \n            return ctrl.signal.aborted;\n        } finally {\n            // stop listening to messages in this stream. REQUIRED:\n            // you'll have a memory leak in the worker otherwise.\n            handler.stop();\n        }\n    }\n})\n\n// in the main thread:\nconst worker = new Worker(...);\nconst { remote } = promisify(worker);\n\nfetch() {\n    // create a promisified remote AbortController and have it talk on \n    // a separate stream so it doesn't conflict with `remote`:\n    const { remote: ctrl, handler } = promisify\u003cAbortController\u003e(worker);\n    try {\n        const abortStream = handler.stream = Math.random(); // can be any number/stream\n        const promise = remote.fetch(abortStream);\n\n        // while fetch is executing, abort the remote controller:\n        ctrl.abort();\n\n        console.log('isAborted? ', await promise); // logs `true`\n    } finally {\n        // stop listening to messages in this stream. REQUIRED:\n        // you'll have a memory leak in the worker otherwise.\n        handler.stop();\n    }\n}\n\n// in the real world, we'd probably tie in the remote controller\n// with an already existing AbortSignal:\nsignal.addEventListener('abort', () =\u003e ctrl.abort());\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvishwam%2Fworker-async","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvishwam%2Fworker-async","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvishwam%2Fworker-async/lists"}