Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Show HN: Ts-Chan – Go-Like Concurrency Primitives for TypeScript/JavaScript (github.com/joeycumines)
60 points by joeycumines on Nov 5, 2023 | hide | past | favorite | 73 comments
Hey HN,

I’m sharing ts-chan, an NPM package providing Go-like concurrency primitives, including channels and select statements, for TypeScript and JavaScript, supporting Node.js, Deno, Bun, and browsers.

This is something I've built to make implementing Go-style "control loops" feasible in JavaScript, but there are many possible applications.

Highlights:

- Features a FIFO processing Chan class and versatile Select class for concurrency control. - Supports buffered channel and channel close semantics very close to Go's. - TypeScript-first implementation. - Defines a simple "channel protocol" inspired by JavaScript's iteration protocols, that's used by `Select`, and implemented by `Chan`. - Makes an effort to mitigate cycles caused by the behavior of JavaScript's microtask queue. - Ongoing project with active iteration for a production-ready module (pre-v1, so the API isn't guaranteed to be stable, but the implementation itself is).

NPM: ts-chan GitHub: github.com/joeycumines/ts-chan

Thanks!



It might be worthwhile to expand a bit in the docs on the problems you see with JS concurrency (why you think it sucks, maybe some examples) and then show how ts-chan fixes those problems.


That's a good idea, thanks :)

Honestly, I've been struggling to come up with examples that aren't extremely contrived, but are still self-contained enough to easily demonstrate. It might actually be easier to just document patterns, though.


Agreed. I don’t know go. So I still don’t understand what this is trying to solve


I think y'all have very fair points, for the record.

Unfortunately it is a lot easier to write documentation for an audience that shares the same context / background / experience. The README was written with an audience in mind consisting primarily of those familiar with Go (or more convoluted "communicating sequential processes" implementations), who were frustrated that things that are very easy in Go, are so much harder in JS. It's not something I considered deeply, but I was imagining that it was unlikely that someone would be searching for "channels in JS" without a base level of understanding.

I work/have worked with some pretty talented people, but (in the past) I've found it difficult to convey the value of the sorts of patterns that `ts-chan` is intended to enable, to those without first-hand experience with such patterns.

Documentation is hard :P


I’m pretty sure you could articulate how exactly this “better” pattern works versus the default “bad” one. Right now the readme is a pile of illegible jargon to me as a non-go person.


Sure, I intend to give it a shot.

I will say though, I personally get the impression that attempts at "concurrency" in JavaScript (in production code) are quite rare, which I attribute to how difficult it is.

That is to say, I don't know if there really _is_ a "default pattern".


> That is to say, I don't know if there really _is_ a "default pattern".

Here:

    const results = await Promise.all([task1(), task2()]);
Could you give a side by side comparison (with and without ts-chan) so we can better understand what kind of problem it is attempting to solve?

My understanding was that Go channels / CSP solves concurrency in a multithreaded environment where reads/writes need to coordinated, but since JavaScript is single threaded, I'm not sure I understand why they would be useful in JavaScript. In JavaScript concurrent tasks can simply communicate by writing/reading shared variables.


I am working on better examples, but they are going to take me a while, at the rate I'm currently going.

To be clear, `ts-chan` is not intended to target any use case already addressed by promises or async/await.

You mentioned CSP so I'll assume you've got context re: that topic. I believe I understand your point re: synchronisation between threads, which is fair, but I'd point out that race conditions still exist in JavaScript - I'd even say they are common, at least in my experience. It is easiest to maintain the integrity of the internal state of complex data structures when only a single logical process can mutate that state at a time.

Example in a similar vein: Firewall daemon that accepts commands over RPC, and performs system configuration, in a linear, blocking fashion, to avoid blowing things up (say it runs `iptables` and/or `nft` commands, under the hood). It would be trivial to have a select statement, with a channel per command (or just one, perhaps), receiving the input payload. In JS, the response would probably be via callback, rather than a ping-pong channel recv then send, or the like.

It wasn't a firewall daemon (although it did interact with firewalld and more), but that's exactly a pattern I've implemented in Go, for a past employer. I don't imagine anyone is keen to implement such a thing in JavaScript, but it's a pattern that applies to anything that mutates state, especially if that state is fragile or complex.


IME, race conditions are quite rare and pretty easy to solve in JS, because the flow of code execution is only susceptible to be interrupted at known locations (async function calls). Here's an example of how you could solve the problem you mentioned in a few lines of JavaScript:

    function createRunExclusive() {
      let runningTask = Promise.resolve();
      return async (asyncFn) => {
        runningTask = runningTask.then(async () => {
          return await asyncFn();
        });
        return runningTask;
      }
    }

    // Example usage:
    // The idea is that any command that should not overlap should use the same "runExclusive" function
    const runExclusive = createRunExclusive();
    function handleIpTablesCommand() {
      runExclusive(async () => {
        await doSomethingWithIpTables();
      })
    }
Although it's probably best to just use one of the queue libraries on npm. This one for example: https://www.npmjs.com/package/p-queue


Hey, that's a neat little trick to implement locking in JS, thanks.

I oversimplified my example perhaps - it also involved handling interruptions (certain system events), maintaining a lifecycle (set up and tear down), and scenarios where it allowed a certain subset of operations to be performed, while performing one of several operations. That last requirement was due to it using shell scripts to perform configuration of the system, and it needing to extract runtime and configuration information from the main daemon.

Still though, thanks very much for your comments, I've enjoyed reading them.


> It is easiest to maintain the integrity of the internal state of complex data structures when only a single logical process can mutate that state at a time.

I agree and this is exactly what js event loop provides. So I don’t understand ts-chan


An operation may take longer than a single tick of the event loop, and may have it's own rules regarding state transitions.

To be clear, I'm not saying "don't do any communication by sharing state", just that there are use cases where it's possible to make it much simpler to reason about.

As an example, you might control the state of "making a HTTP request to perform a search", within the frontend of a single page app that has a map, search filters, and results.

One strategy is to use a buffered channel (1 element), and, when the search filters are updated, drain then re-send the request to the channel.

The logic processing these requests would then just need to sit there, iterating on / receiving from the channel. It could also support cancellation, if that was desired.

(I'd imagine the results would be propagated via some other mechanism, e.g. to a store implementation)


Sounds like a generator then?


Generators have lots of really nice uses, yep.

I'm not sure what specifically you were imagining, but I've added an example of how "vanilla JS" can achieve fan-in, using an AsyncGenerator: https://github.com/joeycumines/ts-chan/blob/main/docs/patter...

It uses one of the patterns suggested in a comment chain above, which I think is pretty neat, and wasn't one that readily occurred to me: https://news.ycombinator.com/item?id=38163562

I'm not making a case for using ts-chan for any situation where a simple generator-based solution suffices. I wouldn't call the example solution (in my first link) simple, but it's something I'd personally be ok with maintaining. Like, I'd approve a PR containing something similar without significant qualms, _if_ there was a significant enough motivator, and it was sufficiently unit tested. I might suggest `ts-chan` as an alternative, to make it easier to maintain, but wouldn't be particularly concerned either way.

That's all very subjective, though :)


I think it would be useful to generally explain what these primitives do and how they interact with each other. A lot of JS/TS users haven’t used golang, but would appreciate a better solution if they understand it (me included).

Regarding the default vs better, a comparative example with a real concurrent task coded with/out your library would be my preferred way to understand it clearly.


I'll definitely keep that in mind, thanks :)


I somewhat disagree with this take, as I felt the intro and "The microtask queue: a footgun" [1] section in the README does an adequate job of laying out the 'why' and the problems with JS's concurrency model. However, it does presume some understanding of Go's channels, so a more explicit example contrasting ts-chan with native JS concurrency could better clarify its benefits for those less familiar. Granted, there is an /example directory, but the benchmarking complexity muddles the readability. Regardless, upon a quick run-through, it looks to be an A-grade library that seems promising for practical use, plus well-referenced, composed, and quite thorough.

[1] https://github.com/joeycumines/ts-chan#the-microtask-queue-a...


Maybe I'm dumb but that section didn't explain the problem to me in the slightest.


Not necessarily, and after giving it more thought, I somewhat retract my previous comment. You do make a good point; it's explained well, but not in concrete terms without assumptions. So, I'll take a stab at it: The core idea is that async functions A and B can communicate through Chan instances, with the Select class overseeing multiple Chan operations, waiting for one to be ready before proceeding. While ts-chan might seem unnecessary for just two async functions, what if you had to manage 8, 16, 32, or more? At some point, Promise.all won't cut it, and that's where ts-chan comes to the rescue. It defines a protocol for channels to better manage communication between asynchronous functions in JS, offering a structure similar to how goroutines communicate in Go.


The only thing that told me was that the author is used to Go primitives and doesn’t like switching to Javascript.

That might be completely wrong, but it’s the impression I get when someone says that something the rest of the world uses without issue sucks.


I might be completely wrong, but the impression I got from your comment is that you haven't been exposed to many implementations using non-trivial concurrency :)

Fair call though, I guess. It doesn't really matter, but I'm certainly used to TypeScript and JavaScript.


> you haven't been exposed to many implementations using non-trivial concurrency

That is entirely accurate. I struggle to imagine scenarios in which I’d need two parallel routines to communicate with each other.


Maybe because we probably shouldn’t be doing that in JavaScript. I primarily use Go and work with Go routines often but I’ve never wanted to do anything remotely close to it in JavaScript. If I wanted proper concurrency, I would be using Go, not JS


Real JS concurrency = Worker threads, which already make use of channels for communication.

Promises are for asynchronous programming, which are not concurrent.


First instalment of docs/examples complete, feedback appreciated: https://news.ycombinator.com/item?id=38183241


Any comparison with the web-standard channel messaging API? https://developer.mozilla.org/en-US/docs/Web/API/Channel_Mes...


I actually considered including one in the README, but there were (are) lots of other topics to consider.

The tl;dr is that there is some cross over, but the use cases and patterns they support are largely different. Message channels are two-way, channels are one way. (I believe) message channels are 1-1, while `ts-chan`'s channels are n-n, though each send corresponds to a single receive.

The primary use case I aim to address is coordination between independent, asynchronous operations (concurrency in JavaScript). Having an analogue to Go's "select statement" is fairly key to that, and I am not quite sure how to articulate the value it provides, but it does not overlap with message channels.

Example using a single channel: `ts-chan` may be used to implement a set of workers, which accept requests from n sources (e.g. incoming http requests in a web server), then perform some operation, as a mechanism to bound the concurrency of that operation. Personally, I find it much easier to model it as:

1. Start the desired number of workers

2. Wire up workers to await incoming requests then process them, in a loop

3. Send requests to said workers

Rather than (for example) tracking the number of running operations, starting a new worker only if possible, after enqueuing the operation, then having the workers need to operate like "check for any work, increment number of operations, perform operation, check for any work (loop), otherwise decrement number of operations".

The `ts-chan` pattern (which is, really, just a Go pattern) also makes it much easier when the requirements are more complex, e.g. if the requester needs to wait for a response, and might timeout before the operation is even started - in which case the operation should be cancelled.

I should probably include demonstrative examples for all that, heh.


> I should probably include demonstrative examples for all that, heh.

Documentation is a lot of work!

Thanks for the detailed reply, that makes a lot of sense.


I really like Go channels, and I was looking for something like this in JS.

One of the earlier implementations is this one, albeit not maintained anymore: https://github.com/NodeGuy/channel I think I prefer its more concise API, e.g. for select (https://github.com/NodeGuy/channel/blob/main/API.md#examples). ts-chan's API looks a bit too verbose to my taste.

Here's an example from ts-chan:

  import {recv, Chan, Select} from 'ts-chan';

  const ch1 = new Chan<number>();
  const ch2 = new Chan<string>();

  void sendsToCh1ThenEventuallyClosesIt();
  void sendsToCh2();

  const select = new Select([recv(ch1), recv(ch2)]);
  for (let running = true; running;) {
    const i = await select.wait();
    switch (i) {
    case 0: {
      const v = select.recv(select.cases[i]);
      if (v.done) {
        running = false;
        break;
      }
      console.log(`rounded value: ${Math.round(v.value)}`);
      break;
    }
    case 1: {
      const v = select.recv(select.cases[i]);
      if (v.done) {
        throw new Error('ch2 unexpectedly closed');
      }
      console.log(`uppercase string value: ${v.value.toUpperCase()}`);
      break;
    }
    default:
      throw new Error('unreachable');
    }
  }

I would consider rewriting the API to something like this:

  import { receive, Channel, select } from 'ts-chan';

  const ch1 = new Channel<number>();
  const ch2 = new Channel<string>();

  void sendsToCh1ThenEventuallyClosesIt();
  void sendsToCh2();

  for (let running = true; running;) {
   switch(await select([receive(ch1), receive(ch2)])) {
      case ch1: {
        if (ch1.done) {
          running = false;
          break;
        }
        console.log(`rounded value: ${Math.round(ch1.value)}`);
      }
      case ch2: {
        if (ch1.done) {
          throw new Error('ch2 unexpectedly closed');
        }
        console.log(`uppercase string value: ${ch2.value.toUpperCase()}`);
        break;
      }
    }
  }


I've been considering adding a `select` function, but I was concerned with the overhead of recreating the select case each time, and wanted to reserve it for a time once I've mulled over my options (I'm aiming to avoid making breaking changes, even though it's pre-v1).

With receives, there's a value attached, and that presents some difficulties regarding typing (for TypeScript).

You have given me an idea, however: I _might_ be able to wrangle together a mechanism that avoids making it a two-step deal (get the index, then resolve the type). It'd probably support only a subset of the functionality, though.

It's on my list to add a simple example to the README using the `Chan` class. Communicating with a single channel (with or without abort support) is very simple, and I'm happy with the API for that.


In addition to my other comment, I agree with the parent here. I appreciate the attempt to mirror golang’s naming, but it would be vastly more readable if you used Channel instead of Chan, etc.


I think my question is why do this if JavaScript is a single threaded language anyway? If you’ve got concurrent processes running in JavaScript, they’re not actually “concurrent”, right? If you’re stuck using JS aren’t you fundamentally better off separating them into two distinct process packages, and having them communicate via some IPC system?

Furthermore; why not just *avoid doing concurrency intensive work in JS* in the first place? Isn’t that why people learned Go?

Seems like a solution looking for a problem kind of, but that’s my low effort critique so maybe I should stuff it.


I'm working on improving documentation and examples, still a WIP, but you may find this interesting: https://news.ycombinator.com/item?id=38183241


I mean isn't this what we have RxJS for?


The thing that made me love Golang was the Time channel, where you can have a channel execute a function every X milliseconds, without having to worry about blocking threads with time.sleep() etc. It made a lot of stuff I wanted to write so much easier I basically went all in on Go and haven't regretted it.


That one is actually pretty easy in JavaScript - in the past I've implemented the same sort of behavior using async generators.

(I also like Go's time.Ticker behavior quite a lot)


Do you have an example? For my case it was an internal lambda service that could add functions with specific intervals to the time.Ticker match, basically you edit a config to add a path to an executable and an interval and then I added it to my script


Sure do, I haven't actually used this one (I implemented it just then) but it should do the trick: https://gist.github.com/joeycumines/6206f2a6cd79875c7c164738...


Actually, it was fun, so I've made it into an actual package.

The one I slapped together works, but this one is better (fixes issues with actually stopping it properly): https://github.com/joeycumines/generator-ticker

On NPM as generator-ticker.


Thanks!


Addendum: My first attempt would drift over time, if the receiver was slow.

Hurt my head a little, but it's fixed now, and I'm at least moderately confident it's correct - unit tested the behavior step by step using `ts-chan`, actually. Will probably use that test as another example use case.


How is it different from setInterval in JS?


Technically speaking? Go has parallelism by default, I suppose. But if your functions take negligible time to execute and the interval is sufficiently large enough then nothing from a user perspective.


Yeah I was designing an internal lambda server so mostly parallelism, all I had to do was have a dynamic match and everything else was out the box for the load I needed


Yeah, I'm sure a well-written parallel Rust or C++ program (using just the CPU cores) can do better, but Go really hits a Pareto sweet spot in terms of "effort vs speedup" return on investment.


I’m not the best systems dev, when I get these options I pick easy!


This doesn't make JavaScript actually run in parallel, does it? I.e if there is any IO to wait on then JS runs, but if there are multiple pure JS asyncs scheduled to run then these will still run one at a time?

Secondly, there is async iteration which is already pretty good syntax wise.


Yep, JS is still single threaded, and all that.

I agree async iteration is pretty good - the `Chan` class implements both `Iterable` and `AsyncIterable`.

For quite a few scenarios, async generators are a better choice. They do have limitations, however. For example, you _can_ implement fan-in, but there's no easy mechanism to fan-in in a blocking manner, from sources that aren't known ahead of time. It's also hard to implement a "fair" mechanism, that doesn't bias certain inputs (like you might desire if multiplexing logs, for example).


The value proposition of the library became much clearer, thanks for explaining!


Couldn’t you spin N workers and assign work to them?


Fan-in is the other way, but yeah you can do batches of concurrent operations in JS fairly easily.

I still like "pipelining" over that, but that can also be implemented with just an iterator, pretty trivially.


But, correct me if I'm wrong, JS in the browser is single threaded and service workers kind of make it multi threaded.

How do you extend JS to become multi threaded when it's not capable to do so?


It might be nice to implement go's defer here too (e.g. for closing the channel) to keep some of the DX.

I guess a JavaScript defer could just be enqueueMicrotask and an anonymous arrow function?


If you want it to run on exiting the function in reverse order, something like Python's context managers would probably be necessary.

I've dabbled in that too, though I don't actually use it for anything, currently: https://github.com/Mcsavvy/contextlib/pull/1


Could someone explain how this adds concurrency to JS without changing the VM/runtime?


Concurrency is just the ability to handle multiple tasks at the same time, to use my quickly googled definition.

JavaScript already supports concurrency, e.g. handling more than one HTTP request concurrently, despite the runtime being single-threaded in nature. Also provided by JavaScript is the ability to fork and join with other asynchronous tasks, e.g. using `Promise.all` to wait for multiple HTTP requests to complete.

The purpose of `ts-chan` is to make it easier to coordinate concurrent operations, via communication, which definitely isn't something JavaScript makes easy.

So part of this is addressing usability issues, and providing necessary boilerplate. I've modelled most of that after Go's channels.

Another is providing a mitigation for a common gotcha (well, it's gotten me), when attempting to implement behavior that involves multiple independent asynchronous operations, communicating with one another. Specifically, JS has a "microtask queue", an optimisation I believe was created to power async/await. Details / links to MDN here: https://github.com/joeycumines/ts-chan#the-microtask-queue-a...


Thanks. I was confused by the terminology. I thought Concurrency = Parallelism, however i realise now that concurrency does not mean that the code is being executed at the same time (not considering waiting for I/O as "executing") while Parallelism means executing at the same time.


Concurrency is not parallelism - you can multitask without (adding) threads.


Regarding the first example in the readme; where is the result variable declared?


Ah, that example is demonstrating an internal mechanism used to ensure the high-level asynchronous API calls take at least one tick of the event loop to complete.

There is no `result` variable - I was just attempting to convey that "some async stuff happens, then it waits if we can't verify that it's not the same tick as when the call started". The "async stuff", in the actual implementation, is sending or receiving.


I mean the foot gun is using TS/JS itself for such things instead of already using a lang that is good at this like Go.


I'd agree with you for personal projects, but there are plenty of companies etc using TS/JS for backend systems, my current place of work included.


I'm a front end dev which was working mostly for the last 10 years with ts/js and I think we need to stop that trend to write everything in ts/js.


It's based on the JavaScript Promise type. Where is the concurrency?

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

Asynchronous is not the same as concurrent.


The concurrency comes from providing mechanisms to communicate between independent, asynchronous chains of operations.

Here is an example, that I based on an example from within the Go spec: https://github.com/joeycumines/ts-chan/blob/main/examples/co...

Here is the same example in Go: https://github.com/joeycumines/ts-chan/blob/main/examples/co...

The JS example is, obviously, much slower, and is fairly contrived - I wouldn't expect anyone to actually want to do that, specifically. The JS one is more complicated primarily because I used it in a benchmark, so needed a mechanism to stop the concurrent, asynchronous chains of operations, once it reached the target number of primes.

I hope that explains it for you.


It is not what is widely understood as concurrency and the documentation for Promise type is clear in that regard.

Nothing happens concurrently (at the same time), only asynchronously (at different times).

From the documentation:

> Note that JavaScript is single-threaded by nature, so at a given instant, only one task will be executing, although control can shift between different promises, making execution of the promises appear concurrent. Parallel execution in JavaScript can only be achieved through worker threads.

I hope that explains it for you.


Concurrency != Parallel execution


In JS, without worker threads or similar situations, you don't have real race conditions. Therefore, concurrency primitives like mutexes, read/write locks, condition variables, barriers, semaphores, etc. are not necessary. Or Go-like channels.

You can use a global variable and it will be fine because it won't be accessed simultaneously.

So then how is this not cargo culting Go? imitating the observable parts of Go without truly understanding what it does?


What makes data races not a "real race condition"?

Anyhow, do what you will. I don't get the impression that you are genuinely interested in this topic.


Thanks for conceding the point. Have fun.


Excuse my multiple comments, just trying to understand this better since I’m very intrigued by your library.

What does the JS “if x != x” check do? I don’t see the equivalent in the golang example?


From the concurrent prime sieve example?

That's just something I copy and pasted from the documentation for the benchmark package I used. Allegedly, it is to prevent "certain compiler optimisations", which is something I'm familiar with when it comes to benchmarking Go code, for example.

I lack specific knowledge as to whether it's actually necessary, sorry :)


Ah, gotcha!

I thought maybe it was something to do with concurrency, and somehow x is affected by a race condition there.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: