🦀 Introducing Antiox
Rust- and Tokio-like async primitives for TypeScript
Channels, streams, mutex, select, time, and 12 more mods.
$ 𝚗𝚙𝚖 𝚒𝚗𝚜𝚝𝚊𝚕𝚕 𝚊𝚗𝚝𝚒𝚘𝚡
Code snippets & GitHub below
---
We did an assessment at
@rivet_dev of the bugs in our TypeScript codebases. The #1 issue – by far – was with async concurrency bugs.
Every time we use a Promise, AbortController, or setTimeout, an exponential number of edge cases are created. Reasoning about async code becomes incredibly difficult very quickly.
But here's the catch: these classes of errors are completely absent from our Rust codebases. And it's not for the reasons you usually hear about "Rust safety."
Why? Tokio (popular async Rust runtime) provides S-tier async primitives that make handling concurrency clean and simple.
So we rebuilt them all in TypeScript.
---
Concurrency:
JavaScript is a single threaded runtime. But the second you start running multiple promises in parallel, your potential bugs start increasing exponentially.
How Antiox helps:
The most common pattern is pairing a channel (aka stream) with a task (background Promise) to build an actor-like system. All communication is done via channels. This helps us manage concurrency control, setup/teardown race conditions, and observability.
Almost everything we do in Rivet's Rust code follows this model 1:1 using Tokio.
See the screenshot in the thread for an example.
Other primitives that we use frequently:
- Select: switch but for async promises
- Mutex & RwLock: control concurrent access to a resource
- OnceCell: initialize something async globally once
- Unreachable: type safe error on switch statement fallthroughs
- Watch: notify on value change
- Time: interval, sleep, timeout, etc
- A bunch more
---
Comparable libraries:
Effect is a lightweight runtime that does a great job solving this problem already. I recommend evaluating Effect as it is a more comprehensive library for error handling, concurrency, and all-needs-TypeScrypt. However for our use case: it was still too heavy for us as we ship inside of our library in the interest of staying lean and minimal overhead. It's also (personally) very hard to reason about memory allocations in Effect, so we prefer to use vanilla TS whenever possible. We looked at effect-smol too, but it does not give us required functionality so we'd have to ship the full Effect runtime as a dependency of RivetKit & co if we used it.
Antiox does not tackle error handling like Rust. Consider better-result or Effect for this. We personally prefer using the native JS runtime error handling.
There are other libraries that try to make TypeScript more Rust-y. However, these are focused on things like Result, ADT, and match. Antiox focuses on providing minimal memory allocations and overhead, e.g. we do not provide a `match({ ... })` handler that requires allocating an object for a fancy switch statement.
There are other libraries for async primitives in TypeScript. But we know Rust like the back of our hand and the APIs incredibly well designed, thanks to the hard work of many WGs and RFCs. Other async libraries tend to have learning curves and huge gaps in their APIs that we don't find with Rust's APIs. Plus LLMs know Rust/Tokio very well, and we're finding this translates to Antiox.
We recommend paring Antiox with:
-
@dillon_mulroy's better-result for Rust-like error handling
- Pino for Tracing-like logging (but lacks spans)
- Zod for Serde-like (duh)
- Need to find: thiserror replacement
---
Quite frankly, an LLM can usually one-shot most of these modules. We're not doing anything hard here. But having this all in one package has removed significant duplicate code within our codebases and we hope it can help you too.
---
Currently supported modules:
- antiox/panic (199 B)
- antiox/sync/mpsc (1.4 KB)
- antiox/sync/oneshot (625 B)
- antiox/sync/watch (677 B)
- antiox/sync/broadcast (936 B)
- antiox/sync/semaphore (845 B)
- antiox/sync/notify (466 B)
- antiox/sync/mutex (606 B)
- antiox/sync/rwlock (778 B)
- antiox/sync/barrier (528 B)
- antiox/sync/select (260 B)
- antiox/sync/once_cell (355 B)
- antiox/sync/cancellation_token (357 B)
- antiox/sync/drop_guard (169 B)
- antiox/sync/priority_channel (1.0 KB)
- antiox/task (932 B)
- antiox/time (530 B)
- antiox/stream (3.0 KB)
- antiox/collections/deque (493 B)
- antiox/collections/binary_heap (492 B)
"Antiox" = "Anti Oxide" & short for antioxidant
(And let's be honest, we usually wish we were writing Rust instead of TypeScript. But the world runs on JS.)