6.8 KiB
Introduction
This is an idiomatic, minimally-opinionated Channel
type for JavaScript that's inspired by Go's concurrency model. It works in browsers and in Node.js. If you know how to use Array
then you already know most of how to use Channel
.
Why
Go's concurrency model is amazing and with JavaScript's async/await feature we have the basis for it as well. All that's missing is a Channel
type. There are existing libraries that implement one but they're generally ports from other languages and don't feel like they were made for JavaScript. I wanted an idiomatic Channel
type that's simple and minimally-opinionated.
This document assumes you're familiar with Go's concurrency model and why you'd want to use it. For explanatory background, read my blog article on the subject.
Requirements
ES 2017
Installation
$ npm install @nodeguy/channel
Basic Use
Create a channel with Channel()
.
To send an value to a channel, use push
. To receive an value from a channel, use shift
. Always precede the method calls with await
:
const assert = require('assert')
const Channel = require('@nodeguy/channel')
const channel = Channel()
;(async () => {
await channel.push(42)
})()
;(async () => {
assert.equal(await channel.shift(), 42)
})()
The push
and shift
methods are usually called in different async functions. They represent the two different ends of the channel and act to coordinate the behavior of the async functions.
API
New Properties
The following properties don't have equivalents in Array
.
close() -> async
Closes the channel so that no more values can be pushed to it. Returns a promise that resolves when any remaining pushes in flight complete.
Attempting to push to a closed channel will throw an exception and shifting from a closed channel will immediately return undefined
.
readOnly() -> Channel
Returns a version of the channel that provides only read methods.
value
Set to the most recently shift
ed value. This is useful when used in combination with select
.
writeOnly() -> Channel
Returns a version of the channel that provides only write methods.
Channel.select(methods) -> async channel
Channel.select
attempts to call multiple channel methods
in parallel and returns the channel of the first one that succeeds. Only the winning method is executed to completion—the other methods have no effect.
Examples
Imagine you're at a party and your next conversation depends on whom you run into first: Alice, Bob, or Charlie.
switch (await Channel.select(alice.shift(), bob.shift(), charlie.push(`Hi!`)) {
case alice:
console.log(`Alice said ${alice.value}.`)
break
case bob:
console.log(`Bob said ${bob.value}.`)
break
case charlie:
console.log(`I said "hi" to Charlie.`)
break
}
Be careful of unintended side effects, however. Even though only one value is pushed in the following example, the counter is incremented twice.
let counter = 0
const increment = () => {
counter++
return counter
}
await Channel.select(alice.push(increment()), bob.push(increment()))
assert.equal(counter, 2)
Sometimes you don't want to wait until a method completes. You can use this pattern to return immediately even if no methods are ready:
const closed = Channel()
closed.close()
switch (await Channel.select(alice.shift(), bob.shift(), closed.shift()) {
case alice:
console.log(`Alice said ${alice.value}.`)
break
case bob:
console.log(`Bob said ${bob.value}.`)
break
default:
console.log(`No one has anything to say yet.`)
}
You can also arrange it so that the select completes within a timeout:
const timeout = Channel()
setTimeout(timeout.close, 1000)
switch (await Channel.select(alice.shift(), bob.shift(), timeout.shift()) {
case alice:
console.log(`Alice said ${alice.value}.`)
break
case bob:
console.log(`Bob said ${bob.value}.`)
break
default:
console.log(`I stopped listening after one second.`)
}
Array-like Methods
These methods are similar to the equivalently named methods of Array
.
Channel
Channel([bufferLength = 0]) -> Channel
Create a new Channel
with an optional buffer.
Channel.of(...values) -> Channel
Channel.from(iterable | stream.Readable) -> Channel
Create a new Channel
from an iterable or a Node.js readable stream.
Channel Object
filter(callbackfn[, thisArg]) -> Channel
forEach(callbackfn[, thisArg]) -> async
The promise returned by forEach
resolves when the channel is closed:
const toArray = async (channel) => {
const array = []
await channel.forEach((value) => {
array.push(value)
})
return array
}
If callbackfn
is async then forEach
will wait for it before iterating to the next value:
const pipe = async (source, sink) => {
await source.forEach(sink.push)
sink.close()
}
join(separator) -> async String
map(callbackfn[, thisArg]) -> Channel
push(value) -> async bufferLength
Unlike Array
's method, push
accepts only one value
at a time.
Sends the value into the channel and returns a promise that resolves when the value has been shifted out or placed in the buffer.
- Throws a
TypeError
when attempting to push to a closed channel. - Throws a
TypeError
when attempting to pushundefined
because it's a reserved value used to indicate a closed channel.
reduce(callbackfn[, initialValue])
shift() -> Promise
Returns a promise that resolves when an value is received from the channel. Closed channels always return undefined
immediately.
slice(start, end) -> Channel
Contributing
Please submit an issue if you would like me to make Channel
more Array
-like (e.g., by adding another Array
method).
Similar Projects
Copyright
Copyright 2017 David Braun
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
these files except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
. Unless required by
applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the specific language
governing permissions and limitations under the License.