|
| 1 | +# `sync-message-port` |
| 2 | + |
| 3 | +This package exposes a utility class that encapsulates the ability to send and |
| 4 | +receive messages with arbitrary structure across Node.js worker boundaries. It |
| 5 | +can be used as the building block for synchronous versions of APIs that are |
| 6 | +traditionally only available asynchronously in the Node.js ecosystem by running |
| 7 | +the asynchronous APIs in a worker and accessing their results synchronously from |
| 8 | +the main thread. |
| 9 | + |
| 10 | +See [the `sync-process` package] for an example of `sync-message-channel` in |
| 11 | +action. |
| 12 | + |
| 13 | +[the `sync-process` package]: https://github.com/sass/sync-process |
| 14 | + |
| 15 | +## Usage |
| 16 | + |
| 17 | +1. Use `SyncMessagePort.createChanenl()` to create a message channel that's set |
| 18 | + up to be compatible with `SyncMessagePort`s. A normal `MessageChannel` won't |
| 19 | + work! |
| 20 | + |
| 21 | +2. You can send this `MessageChannel`'s ports across worker boundaries just like |
| 22 | + any other `MessagePort`. Send one to the worker you want to communicate with |
| 23 | + synchronously. |
| 24 | + |
| 25 | +3. Once you're ready to start sending and receiving messages, wrap *both* ports |
| 26 | + in `new SyncMessagePort()`, even if one is only ever going to be sending |
| 27 | + messages and not receiving them. |
| 28 | + |
| 29 | +4. Use `SyncMessagePort.postMessage()` to send messages and |
| 30 | + `SyncMessagePort.receiveMessage()` to receive them synchronously. |
| 31 | + |
| 32 | +```js |
| 33 | +import {Worker} from 'node:worker_threads'; |
| 34 | +import {SyncMessagePort} from 'sync-message-channel'; |
| 35 | +// or |
| 36 | +// const {SyncMessagePort} = require('sync-message-port'); |
| 37 | + |
| 38 | +// Channels must be created using this function. A MessageChannel created by |
| 39 | +// hand won't work. |
| 40 | +const channel = SyncMessagePort.createChannel(); |
| 41 | +const localPort = new SyncMessagePort(channel.port1); |
| 42 | + |
| 43 | +const worker = new Worker(` |
| 44 | + import {workerData} = require('node:worker_threads'); |
| 45 | + import {SyncMessagePort} from 'sync-message-channel'; |
| 46 | +
|
| 47 | + const remotePort = new SyncMessagePort(workerData.port); |
| 48 | +
|
| 49 | + setTimeout(() => { |
| 50 | + remotePort.postMessage("hello from worker!"); |
| 51 | + }, 2000); |
| 52 | +`, { |
| 53 | + workerData: {port: channel.port2}, |
| 54 | + transferList: [channel.port2], |
| 55 | + eval: true, |
| 56 | +}); |
| 57 | + |
| 58 | +// Note that because workers report errors asynchronously, this won't report an |
| 59 | +// error if the worker fails to load because the main thread will be |
| 60 | +// synchronously waiting for its first message. |
| 61 | +worker.on('error', console.error); |
| 62 | + |
| 63 | +console.log(localPort.receiveMessage()); |
| 64 | +``` |
| 65 | + |
| 66 | +## Why synchrony? |
| 67 | + |
| 68 | +Although JavaScript in general and Node.js in particular are typically designed |
| 69 | +to embrace asynchrony, there are a number of reasons why a synchronous API may |
| 70 | +be preferable or even necessary. |
| 71 | + |
| 72 | +### No a/synchronous polymorphism |
| 73 | + |
| 74 | +Although `async`/`await` and the `Promise` API has substantially improved the |
| 75 | +usability of writing asynchronous code in JavaScript, it doesn't address one |
| 76 | +core issue: there's no way to write code that's *polymorphic* over asynchrony. |
| 77 | +Put in simpler terms, there's no language-level way to write a complex function |
| 78 | +that takes a callback and to run that functions synchronously if the callback is |
| 79 | +synchronous and asynchronously otherwise. The only option is to write the |
| 80 | +function twice. |
| 81 | + |
| 82 | +This poses a real, practical problem when interacting with libraries. Suppose |
| 83 | +you have a library that takes a callback option—for example, an HTML |
| 84 | +sanitization library that takes a callback to determine how to handle a given |
| 85 | +`<a href="...">`. The library doesn't need to do any IO itself, so it's written |
| 86 | +synchronously. But what if your callback wants to make an HTTP request to |
| 87 | +determine how to handle a tag? You're stuck unless you can make that request |
| 88 | +synchronous. This library makes that possible. |
| 89 | + |
| 90 | +### Performance considerations |
| 91 | + |
| 92 | +Asynchrony is generally more performant in situations where there's a large |
| 93 | +amount of concurrent IO happening. But when performance is CPU-bound, it's often |
| 94 | +substantially worse due to the overhead of bouncing back and forth between the |
| 95 | +event loop and user code. |
| 96 | + |
| 97 | +As a real-world example, the Sass compiler API supports both synchronous and |
| 98 | +asynchronous code paths to work around the polymorphism problem described above. |
| 99 | +The logic of these paths is exactly the same—the only difference is that the |
| 100 | +asynchronous path's functions all return `Promise`s instead of synchronous |
| 101 | +values. Compiling with the asynchronous path often takes 2-3x longer than with |
| 102 | +the synchronous path. This means that being able to run plugins synchronously |
| 103 | +can provide a substantial overall performance gain, even if the plugins |
| 104 | +themselves lose the benefit of concurrency. |
| 105 | + |
| 106 | +## How does it work? |
| 107 | + |
| 108 | +This uses [`Atomics`] and [`SharedArrayBuffer`] under the covers to signal |
| 109 | +across threads when messages are available, and |
| 110 | +[`worker_threads.receiveMessageOnPort()`] to actually retrieve messages. |
| 111 | + |
| 112 | +[`Atomics`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics |
| 113 | +[`SharedArrayBuffer`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer |
| 114 | +[`Worker.receiveMessageOnPort()`]: https://nodejs.org/api/worker_threads.html#workerreceivemessageonportport |
| 115 | + |
| 116 | +### Can I use this in a browser? |
| 117 | + |
| 118 | +Unfortunately, no. Browsers don't support any equivalent of |
| 119 | +`worker_threads.receiveMessageOnPort()`, even within worker threads. You could |
| 120 | +make a similar package that can transmit only binary data (or data that can be |
| 121 | +encoded as binary) using only `SharedArrayBuffer`, but that's outside the scope |
| 122 | +of this package. |
| 123 | + |
| 124 | +Disclaimer: this is not an official Google product. |
0 commit comments