Go-workflows middleware for redux


License
Apache-2.0
Install
npm install redux-go-workflows@0.0.31

Documentation

js-go-channels

Installation

npm install js-go-channels

Usage

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,5

Redux 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, 1

JS 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.