@jalapenojs/go-channels
TypeScript icon, indicating that this package has built-in type declarations

0.0.49 • Public • Published

@jalapenojs/go-channels

@jalapenojs/go-channels is a library for handling asynchronous messages using channels, which are message queues. See below for more details.

Installation

yarn add @jalapenojs/go-channels

Basic Usage

import { close, go, newChannel } from "@jalapenojs/go-channels";

// create a channel
const ch = newChannel();

go(function* () {
  // put messages into the channel, which is an infinite queue
  yield ch.put("hello");
  yield ch.put("world");

  // close the channel
  close(ch);
});

go<typeof ch>(function* () {
  while (true) {
    const msg = yield ch.take();
    if (msg.done) break;
    console.log(msg.value);
  }
  // outputs:
  // "hello"
  // "world"
});

See CodeSandbox

Select

import { go, newChannel, select, InferResult } from "@jalapenojs/go-channels";

const ch1 = newChannel();
const ch2 = newChannel();

go(function* () {
  yield ch1.put("hello");
});

go(function* () {
  yield ch2.put("world");
});

type C1 = InferResult<typeof ch1>;
type C2 = InferResult<typeof ch2>;

go(function* () {
  while (true) {
    const [msg1, msg2]: [C1, C2] = yield select(ch1, ch2);
    if (msg1) {
      console.log(msg1); //`{value: hello, done: false}
    }
    if (msg2) {
      console.log(msg2); //`{value: world, done: false}
    }
  }
});

See CodeSandbox

Range

import { go, newChannel, range } from "@jalapenojs/go-channels";

const ch = newChannel<number>();

go(function* () {
  for (let i = 1; 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

See CodeSandbox

Overview

What

As stated in the introduction, @jalapenojs/go-channels is a library for handling asynchronous messages.However, unlike browser events, two key differences are:

  1. Subscribers are automatically unregistered after the first event.
  2. Messages are sent to subscribers in a round-robbing fashion, based on registration order.
const ch = newChannel<number>();

go<typeof ch>(function* subscriber1() {
  while (true) console.log(yield ch.take()); //0, 2, 4, 6, 8, ...
});

go<typeof ch>(function* subscriber2() {
  while (true) console.log(yield ch.take()); //1, 3, 5, 7, ...
});

go(function* producer() {
  let len = 0;
  while (true) yield ch.put(len++);
});

See CodeSandbox

Why

To be perfectly honest, at the time (almost 5 years ago), generators seemed really cool and GoLang concurrency was way easier than anything I had worked back then (including async/await). I resurrected this project because perhaps channels can provide an easier mental model for working with asynchronous events than React hooks 😄.

The inspiration comes from GoLang Channels and redux-saga.

GoLang Channels are +30 year technology for concurrency that provides a simpler mental model than standard concurrency patterns. See the Wikipedia Article for more details.

Redux Integration

Use:

React Integration

Coming soon!

Gotchas and Limitations

In no particular order:

No deadlock-detection support 😢

GoLang tells you when there is deadlock. That would be really cool to add but I'm not even sure if it's possible.

You can't yield inside a callback

Fortunately, the following will not compile:

const elem = //... some DOM element
const ch = newChannel()
elem.addEventListener('mouseup', function() {
  yield ch.put('mouseup'); // compile error
});

Instead you should use an async version of put (which can be a good idea since blocking UI events doesn't really make sense).

elem.addEventListener("mouseup", function () {
  ch.asyncPut("mouseup"); // this works!
});

This common golang synchronization pattern won't work.

function main() {
  const messages = newChannel();
  go(function* () {
    yield messages.put("ping");
  });
  // The desired behavior is to stop execution until a message
  // is received and exit *after* the ping
  const { value: msg } = messages.take();
  console.log(msg);
  // unfortunately, this never prints ping and
  // always exits immediately 😞
}

The reason is because Javascript is synchronous. (And also because you have to yield the take inside a generator for it to have an effect.)

However, the following will work just fine. And by "fine", we mean that even though main finishes before the generators execute, the generators will still print out the ping.

function main() {
  const messages = newChannel();
  go(function* () {
    yield messages.put("ping");
  });
  go<typeof messages>(function* () {
    const { value: msg } = yield messages.take();
    console.log(msg); // prints "ping"
  });
}

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 write a custom eslint rule that aggressively checks for take/put usage.

No asynchronous generator support (coming soon!)

In go, the code below is valid.

ch := make(chan int)
go func() {
	ch <- 0
	time.Sleep(1*time.Second)
	ch <- 1
	close(ch)
}()

This library does not (yet) support asynchronous generators, and so you can do the following:

const ch = newChannel();
go(function* () {
  yield ch.put(0);
  setTimeout(() => {
    ch.asyncPut(1);
    close(ch);
  }, 1000);
});

Roadmap

  • asynchronous generator support
  • React support 🚀
  • An eslint plugin for detect missing yields.
  • for-of support for range.

Readme

Keywords

none

Package Sidebar

Install

npm i @jalapenojs/go-channels

Weekly Downloads

0

Version

0.0.49

License

MIT

Unpacked Size

29.6 kB

Total Files

9

Last publish

Collaborators

  • frankandrobot