UNPKG

@fine-js/channels

Version:

Bits of Clojure's `core.async` ported to JS

343 lines (241 loc) 9.04 kB
# Channels.js Library for asynchronous in-process communication based on [Clojure's](https://clojure.org) excellent [`core.async`](https://github.com/clojure/core.async). Read through [introductory blog post](https://clojure.org/news/2013/06/28/clojure-clore-async-channels) or watch [Strange Loop presentation](https://www.youtube.com/watch?v=drmNlZVkUeE) for more details and inspiration. It's queues, essentially, except there are `Promise`s on top because JavaScript. <a href="https://www.npmjs.com/package/@fine-js/channels"> ``` npm install @fine-js/channels ``` </a> ## Supported Platforms Library's source code is plain JavaScript with no runtime dependencies that works in Node.js and modern browsers. ### Node.js After installing a dependency, `require` the module as usual: ```js const {chan} = require('@fine-js/channels') console.dir(chan()) ``` ### Browsers A build for browsers is available as a [single-file download](browser.js) and via [UNPKG](https://unpkg.com/browse/@fine-js/channels/) ([docs](https://unpkg.com)). Here is an example of embedding version `0.0.2` in particular: ```html <script src="https://unpkg.com/@fine-js/channels@0.0.2/browser.js" integrity="sha384-aLUgfMcOf6P0qxZ4k0e084VdlxfruOqU0zXhYBSZS28Y07u7Zoo1NBbYnNpNynck" crossorigin="anonymous"></script> <script> const {chan} = window.finejs.channels console.dir(chan()) </script> ``` Alternatively, you can build your own version with [Browserify](http://browserify.org) or any other bundler, as well as serve `require.resolve('@fine-js/channels/browser.js')` from your server directly. ## Overview *TODO…* ### Terminology **Channel** is a plain object with two operations: - `put(ch, val)` puts a value onto the channel - `take(ch)` takes a value from the channel **Port** is a description of either operation: - `ch` is a port representing taking a value from channel `ch` - `[ch, val]` is a port representing putting `val` onto the channel `ch` Each channel can be backed by a **buffer** of given capacity. **Parking** is creating and returning a promise that will possibly never resolve, or resolve at indefinite time in the future. This is in contrast to **immediately** which is used in this document to roughly mean guaranteed resolution in the near future (often, after a couple of microtask ticks). ## API ###### Basics - [`chan()`](#chan) creates a channel - [`put()`](#put) puts a value onto the channel - [`take()`](#take) takes a value from the channel - [`close()`](#close) closes a channel ###### Core - [`alt()`](#alt) runs at most one of the given operations returning result of handler - [`alts()`](#alts) runs at most one of the given ports - [`poll()`](#poll) takes a value from channel, when immediately possible - [`offer()`](#offer) puts a value onto channel, when immediately possible - [`timeout()`](#timeout) creates a self-closing channel ###### Buffers - [`unbuffered()`](#unbuffered) for directly connecting writers to readers - [`buffer()`](#buffer) up to given capacity - [`dropping()`](#dropping) ignores writes over capacity - [`sliding()`](#sliding) writes over capacity remove oldest written value ###### JavaScript Extras - [`ch[Symbol.asyncIterator]`](#iter) makes channel iterable with [`for await…of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) <a id="chan"></a> ### `chan()` <small>[](#api)</small> Creates a channel with an optional buffer: - `chan()` unbuffered channel - `chan(capacity)` blocking buffer of given capacity - `chan(buffer)` passed buffer ```js chan() chan(10) chan(sliding(1)) ``` <a id="put"></a> ### `put()` <small>[](#api)</small> Puts a value onto the channel. `null` or `undefined` are not allowed. Will park if no buffer space is available. Resolves to `true` unless channel is already closed. ```js put(ch, 42) ch.put(42) ``` <a id="take"></a> ### `take()` <small>[](#api)</small> Takes a value from the channel. Will park if nothing is available. Resolves to a value taken or `null` if channel is already closed. ```js take(ch) ch.take() ``` <a id="close"></a> ### `close()` <small>[](#api)</small> Closes given channel. The channel will no longer accept any puts (they will be ignored). Data in the channel remains available for taking, until exhausted, after which takes will return `null`. If there are any pending takes, they will be dispatched with `null`. Any parked puts will remain so until a taker releases them. Closing a closed channel is a no-op. Returns `null`. ```js close(ch) ch.close() ``` <a id="timeout"></a> ### `timeout()` <small>[](#api)</small> Returns a channel that will close after given number of millisecods. ```js timeout(100) ``` <a id="alt"></a> ### `alt()` <small>[](#api)</small> Executes one of several channel operations via `alts()` and passess its result to a handler. Accepts a list of operations follwed by corresponding handler and options to pass to `alts()`, where: - each operation is either - single channel to take from - array of ports - each handler is either - a function accepting `(val, channel)` - anything else Each channel may only appear once. Resolves to handler's result if it's a function, handler itself otherwise. ```js alt([ a, (val) => console.log('read %o from a', val), [b, c], (val, ch) => console.log('read %o from %o', val, ch), [[ch, 42]], 'wrote 42 to ch', ]) alt([ [[worker1, task], [worker2, task], [worker3, task]], `task ${task.id} queued`, ], {default: () => `try again later in ${waittime()} seconds`}) alt([ results, (val) => ({status: 200, text: val}), timeout(150), {status: 504}, ], {priority: true}) ``` <a id="alts"></a> ### `alts()` <small>[](#api)</small> Completes at most one of several ports. Unless the `priority` option is `true`, if more than one port is ready, a non-deterministic choice will be made. If no operation is ready and a `default` value is supplied, `[default-val, alts.default]` will be returned. Otherwise `alts` will park until the first operation to become ready completes. Resolves to `[val port]` of the completed operation, where `val` is the value taken for takes, and a boolean (`true` unless already closed, as per `ch.put`) for puts. ```js alts([ch1, ch2, ch3]) alts([ [worker1, task], [worker2, task], [worker3, task], ], {default: 'try again later'}) alts([ results, timeout(150), ], {priority: true}) ``` <a id="poll"></a> ### `poll()` <small>[](#api)</small> Takes a value from the channel, when immediately possible. Resolves a value taken if successful, `null` otherwise. ```js poll(ch) ``` <a id="offer"></a> ### `offer()` <small>[](#api)</small> Puts a value into the channel, when immediately possible. Resolves to `true` if offer succeeds. ```js offer(ch, 42) ``` <a id="unbuffered"></a> ### `unbuffered()` <small>[](#api)</small> Giving this buffer to a channel will make it act as a synchronisation point between readers and writers: puts are parked until the value is taken by consumer. This is a buffer channel will get when you call `chan()` or `chan(0)`. <a id="buffer"></a> ### `buffer()` <small>[](#api)</small> Creates a blocking buffer: - `buffer()` with capacity 1 - `buffer(capacity)` with given capacity. When full, puts will be parked. ```js ch(16) ch(buffer(16)) ``` <a id="dropping"></a> ### `dropping()` <small>[](#api)</small> Creates a dropping non-blocking buffer: - `dropping()` with capacity 1 - `dropping(capacity)` with given capacity When full, puts will complete puts, but values discarded. ```js ch(dropping(3000)) ``` <a id="sliding"></a> ### `sliding()` <small>[](#api)</small> Creates a sliding non-blocking buffer: - `sliding()` with capacity 1 - `sliding(capacity)` with given capacity When full, puts will complete puts, thier values buffered, but oldest elements in buffer will be dropped. ```js ch(sliding(1)) ``` <a id="iter"></a> ### `ch[Symbol.asyncIterator]()` <small>[](#api)</small> Returns Async Iterator over values being put onto the channel. Not meant to be used directly, instead: ```js // Simply consuming everything until the channel closes. for await (const message of ch) console.log(message) // Note that the loop's body will NOT see every single message being put onto the channel, // since, effectively, this calls `take()` on each iteration and somewhat equivalent to: while (!closed(ch)) console.log(await ch.take()) // Reading until we see a sentinel value for await (const byte of ch) if (byte === 0) break; ``` Read more over at [MDN](https://developer.mozilla.org/en-US/): - [`for await…of`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) - [`Symbol.asyncIterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator) - [Iteration protocols](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols)