Readiness: Implemented
Reference
@uniflowed/std
Go's standard library is the benchmark: a language whose batteries are actually
included. @uniflowed/std is the parts of it JavaScript does not have, written
in Flow, one export path per module so a program pays only for what it imports.
// @flow
import { as, is, wrap } from "@uniflowed/std/errors";
import { Group } from "@uniflowed/std/sync";
import { withTimeout, background } from "@uniflowed/std/context";
const [ctx, cancel] = withTimeout(background(), 5_000);
try {
const group = new Group({ limit: 8 });
for (const id of ids) group.go((signal) => fetchRow(id, signal));
return await group.wait();
} catch (failure) {
if (is(failure, ctx.err())) return cached;
throw wrap("loading the dashboard", failure);
} finally {
cancel();
}
What ships today
Twenty modules. Each is a separate subpath, so importing @uniflowed/std/hex
brings in a hex codec and nothing else.
| Import | Go's name | What it is |
|---|---|---|
@uniflowed/std/errors | errors | wrap, is, as, join, unwrap, chain over Error.cause |
@uniflowed/std/sync | sync, errgroup | WaitGroup, Mutex, Semaphore, once, Group |
@uniflowed/std/context | context | Cancellation, deadlines and typed values over AbortSignal |
@uniflowed/std/bytes | bytes | Searching, splitting and joining Uint8Array, plus Builder |
@uniflowed/std/heap | container/heap | A binary heap with a comparator |
@uniflowed/std/hex | encoding/hex | encode, decode, isValid, dump |
@uniflowed/std/base32 | encoding/base32 | RFC 4648 base32 for byte strings and human-entered secrets |
@uniflowed/std/slices | sort.Search, slices.BinarySearch | Lower-bound search and insertion points for sorted arrays |
@uniflowed/std/list | container/list | A doubly linked list with stable element handles |
@uniflowed/std/csv | encoding/csv | RFC 4180 parsing and writing without split(",") bugs |
@uniflowed/std/binary | encoding/binary | Checked DataView cursor reads and writes, plus Go-compatible varints |
@uniflowed/std/path | path/filepath | Host-independent slash-path cleanup, joining and relative paths |
@uniflowed/std/glob | path/filepath | Slash-path glob matching with *, ?, character classes and recursive ** |
@uniflowed/std/hash | hash/crc32, hash/fnv, hash/adler32 | Checksums and non-cryptographic hashes over bytes or UTF-8 text |
@uniflowed/std/time | time | Duration arithmetic plus cancellable Timer, Ticker and afterFunc |
@uniflowed/std/io | io | Byte Reader/Writer, copy, readAll, limitReader and Web Stream adapters |
@uniflowed/std/bufio | bufio | Buffered byte reads plus Scanner line, word, byte and custom splits |
@uniflowed/std/textproto | net/textproto | MIME header blocks with canonical keys, repeated values and folded lines |
@uniflowed/std/zip | archive/zip | ZIP central-directory reading and writing over store and web DEFLATE streams |
@uniflowed/std/tar | archive/tar | ustar archive reading and writing for regular files and directories |
The root @uniflowed/std is unchanged: it is a declaration surface whose
functions raise NativeRuntimeRequiredError, and it is not what these twenty are.
Everything above runs.
The types are the point
Every generic here infers at the call site, with no annotation and no any
underneath, and that claim is tested rather than asserted:
tests/type-tests/std-inference.js is a file of deliberate misuses, and
uf check has to report every one of them and nothing else.
const queue = new Heap((a, b) => a.due - b.due); // Heap<Job>, from the comparator
const next = queue.pop(); // Job | void
const TRACE = key<string>("trace-id");
withValue(ctx, TRACE, 7); // refused: 7 is not a string
traced.value(TRACE); // string | void
as(failure, HttpError)?.status; // number, from the class alone
Cancellation: why not just AbortSignal
Every context has one — ctx.signal() is what goes to fetch — so this
composes with the platform rather than competing with it. What it adds is the
three things AbortSignal does not have: a tree, so cancelling a request
cancels everything it started; deadlines that only ever shorten, so a library
cannot buy itself more time than its caller allowed; and typed request-scoped
values.
AbortSignal.any and AbortSignal.timeout cover parts of the first two, and
they are the newest things on the interface — Node, Deno, Bun and workerd gained
them at different times. Nothing here uses either.
One thing it asks of you that Go does not. A deadline is a setTimeout, and
on Node a pending timer keeps the process alive. There is no runtime-agnostic
unref — Deno and browsers have none — so the cancel that withTimeout hands
back has to be called, in a finally, on the happy path too.
Where it runs
Everything here is pure Flow over the web platform: Uint8Array,
TextEncoder/TextDecoder, AbortController, Promise, setTimeout. No
node: import, no native binding, no Buffer.
| Host | Status | How that was established |
|---|---|---|
| Node.js | runs | npm/std/std.test.js, over every shipped module |
| Bun 1.3.13 | runs | Eighteen smoke assertions over the first six, by hand, through @uniflowed/host/bun-preload |
| Deno | expected | Not run. Inference from the API surface, below |
| Edge / workers | expected | Not run. Same |
The bottom two rows say "expected" rather than "yes" on purpose, and the
distinction is the one
docs/hosts.md
exists to make: a row nothing has started the runtime for is a claim, not a
result. What the claim rests on is that nothing here imports node: anything,
touches Buffer, or reads process, and that AggregateError — the newest
platform feature any of it needs, returned by errors.join — has been in all
four runtimes since 2020. That is a good reason to expect it to work and it is
not a test.
Why this is JavaScript and not Rust
uf is a native toolchain, so "should this be native" is a fair question for every module. It was measured rather than guessed, and the answer for these modules is no.
The comparison is against Node's own C++ implementations of the same
operations — Buffer#toString("hex"), Buffer#equals, Buffer#indexOf —
reached across the same kind of JavaScript-to-native boundary a binding would
use. That makes it a measurement of what native buys, not an estimate of it.
Node 24 on an M-series laptop, nanoseconds per call:
| Operation | Size | This, in JS | Node, native | Native is |
|---|---|---|---|---|
hex.encode | 16 B | 83 ns | 40 ns | 2.1x faster |
hex.encode | 1 MiB | 1.69 ms | 0.26 ms | 6.5x faster |
hex.decode | 16 B | 80 ns | 72 ns | 1.1x faster |
hex.decode | 1 MiB | 4.54 ms | 0.66 ms | 6.8x faster |
bytes.equal | 16 B | 14 ns | 25 ns | 1.8x slower |
bytes.equal | 64 B | 59 ns | 30 ns | 2.0x faster |
bytes.equal | 1 MiB | 0.89 ms | 0.02 ms | 42x faster |
bytes.indexOf | 1 KiB | 324 ns | 69 ns | 4.7x faster |
bytes.indexOf | 1 MiB | 332 µs | 21 µs | 16x faster |
Three things follow from that table, and only the third is about speed.
The boundary is not free, and below a few dozen bytes it is the whole bill.
bytes.equal on sixteen bytes is faster in JavaScript than the native call
that does the same thing, because comparing sixteen bytes takes less time than
crossing into C++ to do it. Most calls to most of these functions are small: a
32-byte digest, a four-byte delimiter, a header value.
There is no binding to cross anyway. There is no N-API and no WebAssembly
crate anywhere in this repository. "Native" for a @uniflowed/std module is not
a build flag; it is a layer that does not exist yet, and the first module to
need one should be the module that pays for it.
A native module would not be runtime-agnostic. An N-API addon runs on Node and Bun and not on Deno or the edge, which is red line 6. A WebAssembly module runs everywhere and copies its arguments across the boundary — which, for a function whose argument is a byte buffer, is the cost the native implementation was supposed to save.
So the effort went where it was measurable instead. Each of these is one implementation of the same function against another, both in JavaScript:
| Change | Effect |
|---|---|
hex.encode writing ASCII bytes and decoding once, above 128 B | 43 MB/s → 620 MB/s |
hex.encode appending to a string, below 128 B | 520 ns → 150 ns on a 32 B digest |
bytes.indexOf scanning for the first byte natively | 8–10x over the naive double loop |
bytes.Builder over re-allocating per chunk | 11x at 100 chunks, 825x at 10,000 |
heap over sorting on every insert | 161x at 1,000 items, 507x at 10,000 |
The last two are not micro-optimisations. They are the difference between linear and quadratic, which is the difference between a service that is fine in staging and one that falls over in production.
Go's standard library, mapped
Every part of it lands in one of three places, and the point of writing the whole map down is that the second bucket is the only one that is work. Most of Go's standard library is either already in JavaScript or cannot be in a runtime-agnostic module at all, and a survey that only listed the gaps would read like a to-do list nobody would ever finish.
Already in JavaScript — do not reimplement
A port of these would be slower, less correct, and a second name for something that already has one.
| Go | The JavaScript answer |
|---|---|
strings | String.prototype, Intl.Segmenter for grapheme-aware work |
strconv | Number, parseInt/parseFloat, toString(radix) |
math, math/rand | Math, crypto.getRandomValues |
math/bits | Math.clz32, the bitwise operators, BigInt.asUintN |
sort, slices | Array.prototype.sort — stable since ES2019 — toSorted, at, findLast |
maps | Map, Object.groupBy, Object.entries |
encoding/json | JSON |
encoding/base64 | atob/btoa, and Uint8Array.prototype.toBase64 as it lands |
regexp | RegExp. A different engine: Go is RE2 and cannot backtrack, JavaScript can, so a port would change the complexity class and not just the syntax |
net/url | URL, URLSearchParams, URLPattern |
time, for the clock, formatting and parsing | Date, Intl.DateTimeFormat, Temporal, and @uniflowed/temporal |
unicode, unicode/utf8 | String iteration, TextEncoder/TextDecoder, Intl.Segmenter |
fmt | Template literals |
crypto/sha*, crypto/hmac, crypto/aes, crypto/rand | WebCrypto, which crates/uf_check/libdefs/web-crypto.js already types |
compress/gzip, compress/flate | CompressionStream/DecompressionStream, on all four runtimes |
cmp | The comparison operators |
log | console |
flag | @uniflowed/cli |
testing | @uniflowed/test |
net/http, client and server | fetch, Request/Response, @uniflowed/server |
database/sql | Application-owned driver; queries generated by sqlc's Flow target for tested adapters |
embed | Import attributes and the bundler's asset graph |
errors and context belong here partly, and that is exactly why they were
first: Error.cause and AbortSignal are the platform's answers, and neither
has a reader for a chain or a tree of scopes. The two modules that shipped build
on them rather than replacing them.
Missing, and worth having
The first six at the top of this page are tranche 1. slices, base32, list,
csv, binary, path, glob, hash, time, io, bufio, textproto, zip and tar are the first next-tranche
pieces. What remains is still useful, but no next-tranche module is left waiting on this page.
sync.RWMutex is deliberately absent rather than pending: readers and writers
cannot run at the same time in one runtime, so it would buy a fairness policy
and nothing else, and it should arrive with the workload that needs one.
Not this package's to write
Each of these fails the same test — a module in @uniflowed/std has to work on
Node, Bun, Deno and the edge runtimes, or say exactly where it does not.
| Go | Why not |
|---|---|
syscall, runtime, unsafe, reflect, plugin, go/* | No supported host exposes any of it, and three of them describe a compiler that is not this one |
net, for raw TCP, UDP and DNS | Every runtime has its own incompatible sockets and workerd has none. This belongs behind a host capability, the way the file system does, not in a portable module |
os, os/exec, os/signal, os/user | The same. A @uniflowed/std/fs is a capability seam, not a std module |
crypto/tls, crypto/x509 | The host owns the TLS stack |
image/*, html/template, text/template | The ecosystem has answers and they move faster than uf releases. Owning a template engine is red line 5 with extra steps |
sync/atomic | Atomics needs a SharedArrayBuffer; without one there is nothing to be atomic about on a single-threaded runtime |
math/big | BigInt covers the integers. big.Float and big.Rat are real gaps, and are a library rather than a standard library |
expvar, runtime/pprof | uf has crates/uf_profiler and uf inspect |
Where the map lives, so it cannot drift
Not here. This page is the readable half; the checked half is
crates/uf_std/src/registry.rs, where every @uniflowed/std/* specifier
carries a status — ships, declared, planned or declined — and
uf inspect --json emits it under stdModules.
That field exists because the table did not have one, and a table without one
is read as an inventory however it was written. It named forty-five specifiers,
forty-four of which had no file, every one of them constructed by a function
that hardcoded nativeBinding: true and wintertcAligned: true — so it could
not describe a shipped pure-JavaScript module without lying about it, and it
described @uniflowed/std/net as WinterTC-aligned while TcpListener was in
its export list. Four tests now hold the statuses to npm/std: the
shipping specifiers are exactly the manifest's subpaths, each one's export list
is exactly what the parser finds in its file, a shipping module never raises
NativeRuntimeRequiredError and the root always does, and nothing claiming
WinterTC alignment imports node: anything or reads Buffer or process.
uf inspect prints the ships count and the rest as a roadmap beside it, for
the same reason @uniflowed/ui does. A count with a wish in it is worse than
no count.
Edit this pagedocs/app/reference/std/$page.mdx