Facades Over Threads
How AtollJS makes real JavaScript multithreading feel like ordinary code
Problem. Workers exist, but
postMessageergonomics make them not worth it: you abandon your framework's idioms, hand-roll a protocol, and serialize every interaction through a stringly-typed pipe.Fix. Make the boundary invisible. The function, component, or injectable you were going to write anyway is the contract — typed pools, shared memory, and DOM streaming live underneath the call site.
JavaScript has had real threads for fifteen years, and the honest answer
to "why isn't everything multithreaded" is that the API makes you earn
it. postMessage gives you a pipe and a serialization boundary; the
protocol on top — request ids, response matching, error plumbing,
progress events — is work nobody assigned you but everybody pays for.
AtollJS is built around one thesis:
The code you were going to write anyway — a function, a component class, an injectable service — IS the contract. The boundary lives underneath it, invisible at the call site.
Here's what that looks like at each level.
The problem, concretely
Two scenarios the framework was built against:
The frozen UI. A telecom incident explorer — a live table of a million alarm records. Type in the search box and the input lags; scroll and frames drop. The correct fix is "do it in a worker," which means abandoning your framework's renderer and hand-rolling a DOM sync protocol. So nobody does it.
The stalled API. A NestJS endpoint aggregates a million records.
Every request parks the event loop for tens of milliseconds — so every
other request waits. Node has worker_threads, but a useful worker
needs bootstrap, task routing, failure handling, and DI on both sides.
The capability is in the platform. The ergonomics are what kill it.
The foundation the facades sit on
One shared primitive: defineSharedMemory.
export const incidentsMemory = defineSharedMemory({
lists: {
incidents: field.list({
schema: reef.object({
id: reef.u32(), severity: reef.int(0, 3), status: reef.int(0, 2),
site: reef.string(10), /* … */
}),
count: 1_000_000,
}),
},
signals: { seedProgress: field.number() },
});The schema compiles to a deterministic byte layout over a
SharedArrayBuffer — identical on the main thread and in every worker.
Both sides import the same object; reads are zero-copy; writes bump a
version counter that Atomics watchers sleep on. No message touches the
hot path.
The facade ladder
A function call. defineWorker on the worker side,
connectWorker/workerClient on the main thread. The client is a Proxy
typed by typeof your worker — import type means the worker module
never enters your bundle. Cancellation, timeouts, backpressure, crash
respawn: included.
Framework DI. Angular apps get provideAtoll / injectAtollPool —
the pool is a provider like any other. Nothing about your component tree
changes.
A component class. islandComponent turns a worker-side component
class into a local-looking shell component. React, Vue, Solid, Svelte,
Angular — the same million-record table renders inside the worker in
all five, streaming DOM ops to the main thread. Re-renders land around
2–3 ms in the worker; the main thread touches ~22 live DOM rows.
| Framework | Worker re-render on scroll jump | DOM rows |
|---|---|---|
| Vue | ~3 ms | 22 |
| Solid | ~2 ms | 22 |
| Svelte | ~3 ms | 22 |
| Angular | ~3 ms | 22 |
| React | ~3 ms | 22 |
A service. NestJS gets @AtollService({ pool }) at class level —
inject normally, every method dispatches to the pool, and the consumer
literally cannot import Atoll to call it. That last part is the test of
a real facade: DashboardService composes ReportService with zero
framework imports.
Why this shape matters
Facades fail in two familiar ways. They leak — and you're back to
managing the boundary by hand — or they hide failure, and you find out
in production. AtollJS picks neither: call sites stay idiomatic, but the
failure modes are typed and loud — TaskTimeoutError,
WorkerCrashedError, PoolQueueFullError, a validator warning and a
local-execution fallback when a pool can't spawn.
Try it
npm install @atolljs/coreThe repo has per-framework examples, a NestJS app, and the million-row benchmark — all wired to run. The docs site walks each binding end to end.
The next time a feature makes your tab stutter or your p99 sag, before reaching for memoization or pagination, ask whether the work belongs in a worker. If the answer used to be "not worth the plumbing" — that's the assumption this project exists to retire.
Sources: shared-memory contract ·
reef schemas ·
worker pools & tasks ·
islands engine ·
NestJS bindings —
repository: github.com/jwhenry3/atolljs,
examples in examples/.