API reference

Concurrency & workers

4/4Coverage2/4Strict (zero caveats)

Real OS threads with cross-thread messaging: Node's worker_threads, ported from Node's own lib/internal/worker, plus the web's Worker/onmessage shape. Each Worker is an isolate — on its own thread it evaluates its modules afresh — and messages are structured clones of any value, with SharedArrayBuffer/Atomics for shared memory and MessagePorts moving between threads in a transfer list. The one structural difference: a worker's file is compiled into the program, so new Worker takes a path literal.

Exact Works, with caveats Not supported

Worker

Works, with caveats
new Worker(filename: string | URL, options?: WorkerOptions): Worker

Spawns a real OS thread running one of the program's worker modules as an isolate: on its thread it evaluates every builtin module and the modules the file imports afresh, then the file. The Worker is Node's (an EventEmitter with 'online', 'message', 'error', 'exit', postMessage, terminate() resolving the exit code, ref/unref, threadId); the web's onmessage/onerror shape and a worker's self.onmessage/self.postMessage work too. Messages are structured clones of any value; a SharedArrayBuffer keeps its memory, and a MessagePort in the transfer list moves to the other thread. An uncaught error in the worker reaches the parent's 'error', then 'exit' with code 1.

Parameters
filenamestring | URL
A path literal (or __filename) naming a worker module; resolved relative to the file it is written in.
optionsWorkerOptions
workerData (any cloneable value), transferList, name.
Returns

Worker — An EventEmitter with postMessage, terminate, ref/unref and threadId.

const a = new Worker('./sum_worker.ts', { workerData: "left" });
a.on('message', (partial: number) => { total += partial; a.terminate(); });
a.postMessage(left);
See it in a full example →
⚠ Differences from standard
  • A worker's file is compiled in: new Worker takes a path literal (resolved relative to the file it is written in) or __filename; a computed path or eval: true code fails at new Worker
  • terminate() takes effect at the worker's next event-loop turn: a worker busy in synchronous code runs on until it yields
  • A transferred ArrayBuffer is copied, not detached
  • A worker shares process.env with its parent and writes stdout/stderr directly; the stdio/env/resourceLimits/argv/execArgv options and getHeapSnapshot() are not ported
  • An 'uncaughtException' listener in a worker runs but the worker still ends with code 1
  • No combining with http.listen({workers:N})

BroadcastChannel

Exact
new BroadcastChannel(name: string): BroadcastChannel

Name-keyed pub/sub across threads through a process-wide registry: each other open channel of the name gets its own structured clone of the message, delivered on its own thread's loop. An open channel holds the loop until close(), as Node's does; onmessage, addEventListener/removeEventListener and ref()/unref() work same-thread and across workers.

Parameters
namestring
The channel name (any string).
Returns

BroadcastChannel — An endpoint with postMessage, onmessage, addEventListener and close.

const bc = new BroadcastChannel('news');
setTimeout(() => {
  bc.postMessage("markets up in Thessaloniki");
  bc.close();
}, 100);
See it in a full example →

MessageChannel / MessagePort

Exact
new MessageChannel(): { port1: MessagePort, port2: MessagePort }

An entangled port pair. A port is Node's NodeEventTarget: on('message') or onmessage starts it, addEventListener works, close() closes both ends and each emits 'close', and ref()/unref()/hasRef() control whether it holds the loop. A port moves to another thread in a postMessage transfer list or a Worker's transferList, and is detached on the sending side; one outside the transfer list is a DataCloneError. Globals, and importable from worker_threads.

Returns

{ port1, port2 } — A pair of entangled MessagePorts.

const ch = new MessageChannel();
const w = new Worker('./port_worker.ts', { workerData: ch.port2, transferList: [ch.port2] });
ch.port1.onmessage = (e: MessageEvent) => { console.log("shouted back: " + e.data); };
See it in a full example →

klain:sync goroutines, channels, select

Works, with caveats
import { go, Channel, select, defaultCase } from 'klain:sync'; go(fn: () => void); new Channel<T>(capacity?); select(...cases)

An explicitly-non-Node opt-in: Go's concurrency model, not a JS approximation. go(fn) spawns a cheap goroutine onto a GMP work-stealing scheduler (one logical processor + pinned OS thread per GOMAXPROCS, global run-queue backstop, half-queue stealing) in an embedded C runtime linked only when klain:sync is imported. Channel<T> is a Go channel: synchronous blocking send/receive (not await) that parks the goroutine — the OS thread keeps running others — as an unbuffered rendezvous or buffered ring, with close() and `for (const v of ch)` range. select(...) is Go's random-fair select over recvCase/sendCase/defaultCase. Cooperative preemption is complete: a sysmon thread flags an over-budget goroutine and compiler-inserted safepoints at every function entry and loop back-edge yield it, so no goroutine can starve the others.

Parameters
fn() => void
go(fn): the goroutine body; captures channels from its enclosing scope.
capacitynumber
new Channel<T>(capacity): 0/omitted is an unbuffered rendezvous; >0 is a buffered ring.
...casesSelectCase[]
select(...): ch.recvCase(handler) / ch.sendCase(value, handler) / defaultCase(handler); runs one ready case, or blocks until one fires (unless a defaultCase is present).
Returns

void | Channel<T> — go returns nothing; new Channel<T> returns a channel with .send(v: T)/.receive(): T/.close(); receive() gives undefined once the channel is closed and drained.

⚠ Differences from standard
  • Channel ops are synchronous blocking, not await; a value crosses as its 8-byte any word (number/string/object/boolean).
  • select cases are ch.recvCase(v => ...) / ch.sendCase(x, () => ...) / defaultCase(() => ...), at most 64 per select; a recv handler on a closed, drained channel gets undefined, with no separate closed flag (Go's v, ok) — the for-of range ends on close.
  • Fixed goroutine stacks (default 256 KiB, tunable via KLAINSYNC_STACK_KB); true growable/moving stacks are gated on precise stack maps. Blocking-syscall P-handoff is a bounded sysmon rescue-M approximation. send-after-close / double-close abort the process (Go-panic parity); native-only.