js-go-channels v0.0.31
js-go-channels
Installation
npm install js-go-channelsUsage
If you want to try out js-go-channels, check out this REPL.
Generators, oh my!
Your app will need to support generators to use this library. Node.js >= 6 supports this out of the box. Modern desktop browsers as well. However, if you need to support mobile, legacy browsers, or legacy Node.js, then you'll need to use Babel to transpile your code. Please see the appendix.
Examples
Basic Usage
import {go, newChannel, close, select, range} from 'js-go-channels';
const ch = newChannel();
go(function*() {
yield ch.put('hello');
close(ch);
// this will throw an error because you can't put on a closed
// channel
yield ch.put('world');
});
go(function*() {
const msg1 = yield ch.take();
console.log(msg1); // {value: hello, done: false}
const msg2 = yield ch.take();
console.log(msg2); // {value: undefined, done: true}
const msg3 = yield ch.take();
console.log(msg3); // {value: undefined, done: true}
});Select
import {go, newChannel, close, select, range} from 'js-go-channels';
const ch1 = newChannel();
const ch2 = newChannel();
go(function*() {
yield ch1.put('hello');
});
go(function*() {
yield ch2.put('world');
});
go(function*() {
for(;;) {
const [msg1, msg2] = yield select(ch1, ch2);
if (msg1) {
console.log(msg1); //`{value: hello, done: false}
}
if (msg2) {
console.log(msg2); //`{value: world, done: false}
}
}
});Range
import {go, newChannel, close, select, range} from 'js-go-channels';
const ch = newChannel();
go(function*() {
for(let i=0; i<10; i++) {
yield ch.put(i);
}
});
range(ch)
.forEach(msg => {
console.log(msg);
if (msg === 5) {
// return false to stop receiving messages
return false
}
});
// output: 1,2,3,4,5Redux Integration
Use redux-thunk, redux-saga, or redux-go-workflows.
Gotchas
Javascript doesn't have multi-return types
In go, you can write the following
ch := make(chan string)
go func() {
val := <-ch
val, more := <- ch
}()JS is a saner language in this regard (words I never thought I would
write). Instead js-go-channel channels return objects of type
{value, done}. You can use destructuring and renaming to get the
same result.
const ch = newChannel()
go(function*() {
const {value: msg1} = yield ch.take() //ignore done
console.log(msg1)
const {value: msg2, done} = yield ch.take()
if (!done) {
console.log(msg2)
}
})You can't yield inside a callback
Can you spot the bug?
const elem = //... some DOM element
const ch = newChannel()
elem.addEventListener('mouseup', function() {
yield ch.put('mouseup')
});If you're using Babel, the above code won't even compile. Instead you
should use an async version of put (which can be a good idea since
blocking UI events doesn't really make sense). Under the hood,
asyncPut uses an infinite buffer.
elem.addEventListener('mouseup', function() {
ch.asyncPut('mouseup') // this works!
});The common golang synchronization pattern won't work.
func main() {
const messages = newChannel()
go(function* () { yield messages.put("ping") })
const {value: msg} = messages.take()
console.log(msg)
}The reason has to do with the way Javascript concurrency
works. Recall, it works by dispatching functions to an async
queue. Whatever function is currently executing will hog the CPU.
That means the take will always synchronously fire before the put,
and to make it block we have to use a callback.
So the following will work just fine. And by "fine", we mean that
main won't finish until it receives a "ping".
func main() {
const messages = newChannel()
go(function* () { yield messages.put("ping") })
go(function* () {
const {value: msg} = yield messages.take()
console.log(msg)
})
}Don't forget to yield
Can you spot the bug?
const output = newChannel()
const input = newChannel()
go(function* () {
output.put("out")
const {value: msg} = yield input.take()
})To make put/take work, you need to yield inside of a "go"
routine. As is, this code will run but silently fail. Currently, the
only workaround is to use types or write a custom eslint rule that
aggressively checks for take/put usage.
for-of loops don't (yet) support range
In go, the code below is valid. That is, range converts a channel to
an asynchronous iterator and then the for loop can iterate over
it. In Javascript, for-of loops do not yet support asynchronous
iterators. Instead we use a custom forEach.
ch := make(chan int)
go func() {
ch <- 0
time.Sleep(1*time.Second)
ch <- 1
close(ch)
}()
for x := range ch {
fmt.Println(x)
}
// output: 0, 1JS version:
const ch = newChannel()
go(function*() {
yield ch.put(0)
// recall we have to use asyncPut inside of a callback
setTimeout(() => ch.asyncPut(1), 1000)
})
range(ch).forEach(x => {
console.log(x)
})Appendix
Legacy Browser support
babel-preset-env makes it
super easy to support legacy browsers. The following shows a sample
.babelrc that compiles for IE 11:
{
"legacy": {
"presets": [
[
"env",
{
"targets": {
"browsers": ["ie >= 11"]
},
"spec": true,
"modules": "commonJS",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
}You can then need to add babel-polyfill as a dependency in the package.json.
That's it! (You then need to configure webpack or equivalent to use Babel.)
Note that babel-preset-env replaces the old babel-preset-es2015 plugin.