1 | # Comlink
|
2 |
|
3 | Comlink makes [WebWorkers][webworker] 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.
|
4 |
|
5 | At a more abstract level it is an RPC implementation for `postMessage` and [ES6 Proxies][es6 proxy].
|
6 |
|
7 | ```
|
8 | $ npm install --save comlink
|
9 | ```
|
10 |
|
11 | ![Comlink in action](https://user-images.githubusercontent.com/234957/54164510-cdab2d80-4454-11e9-92d0-7356aa6c5746.png)
|
12 |
|
13 | ## Browsers support & bundle size
|
14 |
|
15 | ![Chrome 56+](https://img.shields.io/badge/Chrome-56+-green.svg?style=flat-square)
|
16 | ![Edge 15+](https://img.shields.io/badge/Edge-15+-green.svg?style=flat-square)
|
17 | ![Firefox 52+](https://img.shields.io/badge/Firefox-52+-green.svg?style=flat-square)
|
18 | ![Opera 43+](https://img.shields.io/badge/Opera-43+-green.svg?style=flat-square)
|
19 | ![Safari 10.1+](https://img.shields.io/badge/Safari-10.1+-green.svg?style=flat-square)
|
20 | ![Samsung Internet 6.0+](https://img.shields.io/badge/Samsung_Internet-6.0+-green.svg?style=flat-square)
|
21 |
|
22 | Browsers without [ES6 Proxy] support can use the [proxy-polyfill].
|
23 |
|
24 | **Size**: ~2.5k, ~1.2k gzip’d, ~1.1k brotli’d
|
25 |
|
26 | ## Introduction
|
27 |
|
28 | 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.
|
29 |
|
30 | 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.
|
31 |
|
32 | ## Examples
|
33 |
|
34 | ### [Running a simple function](./docs/examples/01-simple-example)
|
35 |
|
36 | **main.js**
|
37 |
|
38 | ```javascript
|
39 | import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
|
40 | async function init() {
|
41 | const worker = new Worker("worker.js");
|
42 | // WebWorkers use `postMessage` and therefore work with Comlink.
|
43 | const obj = Comlink.wrap(worker);
|
44 | alert(`Counter: ${await obj.counter}`);
|
45 | await obj.inc();
|
46 | alert(`Counter: ${await obj.counter}`);
|
47 | }
|
48 | init();
|
49 | ```
|
50 |
|
51 | **worker.js**
|
52 |
|
53 | ```javascript
|
54 | importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
|
55 | // importScripts("../../../dist/umd/comlink.js");
|
56 |
|
57 | const obj = {
|
58 | counter: 0,
|
59 | inc() {
|
60 | this.counter++;
|
61 | },
|
62 | };
|
63 |
|
64 | Comlink.expose(obj);
|
65 | ```
|
66 |
|
67 | ### [Callbacks](./docs/examples/02-callback-example)
|
68 |
|
69 | **main.js**
|
70 |
|
71 | ```javascript
|
72 | import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
|
73 | // import * as Comlink from "../../../dist/esm/comlink.mjs";
|
74 | function callback(value) {
|
75 | alert(`Result: ${value}`);
|
76 | }
|
77 | async function init() {
|
78 | const remoteFunction = Comlink.wrap(new Worker("worker.js"));
|
79 | await remoteFunction(Comlink.proxy(callback));
|
80 | }
|
81 | init();
|
82 | ```
|
83 |
|
84 | **worker.js**
|
85 |
|
86 | ```javascript
|
87 | importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
|
88 | // importScripts("../../../dist/umd/comlink.js");
|
89 |
|
90 | async function remoteFunction(cb) {
|
91 | await cb("A string from a worker");
|
92 | }
|
93 |
|
94 | Comlink.expose(remoteFunction);
|
95 | ```
|
96 |
|
97 | ### [`SharedWorker`](./docs/examples/07-sharedworker-example)
|
98 |
|
99 | When using Comlink with a [`SharedWorker`](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) you have to:
|
100 |
|
101 | 1. Use the [`port`](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker/port) property, of the `SharedWorker` instance, when calling `Comlink.wrap`.
|
102 | 2. Call `Comlink.expose` within the [`onconnect`](https://developer.mozilla.org/en-US/docs/Web/API/SharedWorkerGlobalScope/onconnect) callback of the shared worker.
|
103 |
|
104 | **Pro tip:** You can access DevTools for any shared worker currently running in Chrome by going to: **chrome://inspect/#workers**
|
105 |
|
106 | **main.js**
|
107 |
|
108 | ```javascript
|
109 | import * as Comlink from "https://unpkg.com/comlink/dist/esm/comlink.mjs";
|
110 | async function init() {
|
111 | const worker = new SharedWorker("worker.js");
|
112 | /**
|
113 | * SharedWorkers communicate via the `postMessage` function in their `port` property.
|
114 | * Therefore you must use the SharedWorker's `port` property when calling `Comlink.wrap`.
|
115 | */
|
116 | const obj = Comlink.wrap(worker.port);
|
117 | alert(`Counter: ${await obj.counter}`);
|
118 | await obj.inc();
|
119 | alert(`Counter: ${await obj.counter}`);
|
120 | }
|
121 | init();
|
122 | ```
|
123 |
|
124 | **worker.js**
|
125 |
|
126 | ```javascript
|
127 | importScripts("https://unpkg.com/comlink/dist/umd/comlink.js");
|
128 | // importScripts("../../../dist/umd/comlink.js");
|
129 |
|
130 | const obj = {
|
131 | counter: 0,
|
132 | inc() {
|
133 | this.counter++;
|
134 | },
|
135 | };
|
136 |
|
137 | /**
|
138 | * When a connection is made into this shared worker, expose `obj`
|
139 | * via the connection `port`.
|
140 | */
|
141 | onconnect = function (event) {
|
142 | const port = event.ports[0];
|
143 |
|
144 | Comlink.expose(obj, port);
|
145 | };
|
146 |
|
147 | // Single line alternative:
|
148 | // onconnect = (e) => Comlink.expose(obj, e.ports[0]);
|
149 | ```
|
150 |
|
151 | **For additional examples, please see the [docs/examples](./docs/examples) directory in the project.**
|
152 |
|
153 | ## API
|
154 |
|
155 | ### `Comlink.wrap(endpoint)` and `Comlink.expose(value, endpoint?, allowedOrigins?)`
|
156 |
|
157 | 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][endpoint] and `allowedOrigins` is an array of
|
158 | RegExp or strings defining which origins should be allowed access (defaults to special case of `['*']` for all origins).
|
159 |
|
160 | `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.
|
161 |
|
162 | ### `Comlink.transfer(value, transferables)` and `Comlink.proxy(value)`
|
163 |
|
164 | 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][structured clone table] for details.
|
165 |
|
166 | If you want a value to be transferred rather than copied — provided the value is or contains a [`Transferable`][transferable] — you can wrap the value in a `transfer()` call and provide a list of transferable values:
|
167 |
|
168 | ```js
|
169 | const data = new Uint8Array([1, 2, 3, 4, 5]);
|
170 | await myProxy.someFunction(Comlink.transfer(data, [data.buffer]));
|
171 | ```
|
172 |
|
173 | 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.
|
174 |
|
175 | ```js
|
176 | myProxy.onready = Comlink.proxy((data) => {
|
177 | /* ... */
|
178 | });
|
179 | ```
|
180 |
|
181 | ### Transfer handlers and event listeners
|
182 |
|
183 | It is common that you want to use Comlink to add an event listener, where the event source is on another thread:
|
184 |
|
185 | ```js
|
186 | button.addEventListener("click", myProxy.onClick.bind(myProxy));
|
187 | ```
|
188 |
|
189 | While this won’t throw immediately, `onClick` will never actually be called. This is because [`Event`][event] is neither structured cloneable nor transferable. As a workaround, Comlink offers transfer handlers.
|
190 |
|
191 | 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:
|
192 |
|
193 | ```js
|
194 | Comlink.transferHandlers.set("EVENT", {
|
195 | canHandle: (obj) => obj instanceof Event,
|
196 | serialize: (ev) => {
|
197 | return [
|
198 | {
|
199 | target: {
|
200 | id: ev.target.id,
|
201 | classList: [...ev.target.classList],
|
202 | },
|
203 | },
|
204 | [],
|
205 | ];
|
206 | },
|
207 | deserialize: (obj) => obj,
|
208 | });
|
209 | ```
|
210 |
|
211 | 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.
|
212 |
|
213 | ### `Comlink.releaseProxy`
|
214 |
|
215 | Every proxy created by Comlink has the `[releaseProxy]()` method.
|
216 | Calling it will detach the proxy and the exposed object from the message channel, allowing both ends to be garbage collected.
|
217 |
|
218 | ```js
|
219 | const proxy = Comlink.wrap(port);
|
220 | // ... use the proxy ...
|
221 | proxy[Comlink.releaseProxy]();
|
222 | ```
|
223 |
|
224 | If the browser supports the [WeakRef proposal], `[releaseProxy]()` will be called automatically when the proxy created by `wrap()` gets garbage collected.
|
225 |
|
226 | ### `Comlink.finalizer`
|
227 |
|
228 | 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.
|
229 |
|
230 | ### `Comlink.createEndpoint`
|
231 |
|
232 | Every proxy created by Comlink has the `[createEndpoint]()` method.
|
233 | 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.
|
234 |
|
235 | ```js
|
236 | const port = myProxy[Comlink.createEndpoint]();
|
237 | const newProxy = Comlink.wrap(port);
|
238 | ```
|
239 |
|
240 | ### `Comlink.windowEndpoint(window, context = self, targetOrigin = "*")`
|
241 |
|
242 | 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()`.
|
243 |
|
244 | `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`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage).
|
245 |
|
246 | For a usage example, take a look at the non-worker examples in the `docs` folder.
|
247 |
|
248 | ## TypeScript
|
249 |
|
250 | 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>`.
|
251 |
|
252 | ## Node
|
253 |
|
254 | Comlink works with Node’s [`worker_threads`][worker_threads] module. Take a look at the example in the `docs` folder.
|
255 |
|
256 | [webworker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
|
257 | [umd]: https://github.com/umdjs/umd
|
258 | [transferable]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects
|
259 | [messageport]: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort
|
260 | [examples]: https://github.com/GoogleChromeLabs/comlink/tree/master/docs/examples
|
261 | [dist]: https://github.com/GoogleChromeLabs/comlink/tree/master/dist
|
262 | [delivrjs]: https://cdn.jsdelivr.net/
|
263 | [es6 proxy]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
|
264 | [proxy-polyfill]: https://github.com/GoogleChrome/proxy-polyfill
|
265 | [endpoint]: src/protocol.ts
|
266 | [structured cloning]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
|
267 | [structured clone table]: structured-clone-table.md
|
268 | [event]: https://developer.mozilla.org/en-US/docs/Web/API/Event
|
269 | [worker_threads]: https://nodejs.org/api/worker_threads.html
|
270 | [weakref proposal]: https://github.com/tc39/proposal-weakrefs
|
271 |
|
272 | ## Additional Resources
|
273 |
|
274 | - [Simplify Web Worker code with Comlink](https://davidea.st/articles/comlink-simple-web-worker)
|
275 |
|
276 | ---
|
277 |
|
278 | License Apache-2.0
|