npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jasonkneen/unithread

v0.2.2

Published

Isomorphic true-multithreading for JavaScript: one API over node:worker_threads and Web Workers. Function shipping, worker pools, RPC proxies, shared memory + Atomics.

Readme

unithread

True multithreading for JavaScript with one API across Node and the browser. Wraps node:worker_threads and Web Workers behind a single surface: function shipping, worker pools, RPC services, and shared memory via SharedArrayBuffer + Atomics.

import { runInThread, Task, WorkerPool, SharedCounter, Mutex, Signal } from "@jasonkneen/unithread";

// One-shot: run a function on a real OS thread
const n = await runInThread((x) => {
  const fib = (i) => (i < 2 ? i : fib(i - 1) + fib(i - 2));
  return fib(x);
}, 35);

// Persistent thread, call repeatedly
const task = await Task.spawn((a, b) => a + b);
await task.run([2, 3]); // 5
await task.terminate();

// RPC service: object of named methods on one thread
const svc = await Task.spawnService({
  square: (x) => x * x,
  hash: (s) => { /* self-contained work */ },
});
await svc.call("square", [12]); // 144

// Pool sized to hardware cores, order-preserving parallel map
const pool = await WorkerPool.create((n) => heavyWork(n));
const results = await pool.map(inputs, (n) => [n]);
await pool.close();

// True shared memory across threads
const counter = new SharedCounter(0);       // Atomics-backed
const mutex   = new Mutex();                // lock() / lockAsync() / withLock()
const signal  = new Signal();               // wait() / waitAsync() / fire()
await runInThread((buf) => {
  new Int32Array(buf); Atomics.add(new Int32Array(buf), 0, 1);
}, counter.buffer);

How it works

  • Node: new Worker(source, { eval: true }) from node:worker_threads.
  • Browser: new Worker(URL.createObjectURL(new Blob([source]))).
  • One env-detecting bootstrap script runs in both worker types and speaks a small id-correlated message protocol (call / method / result / error), so Task, WorkerPool, and RPC behave identically in either runtime.

Rules & caveats (the honest ones)

  • Shipped functions must be self-contained. They cross the thread boundary via Function.prototype.toString() — no closure captures, no imports from outer scope. Pass data as arguments; use SharedArrayBuffer for shared state.
  • Browser SharedArrayBuffer requires cross-origin isolation (COOP/COEP headers). Atomics.wait is forbidden on the browser main thread — use lockAsync() / waitAsync() there. Node has no such restriction.
  • Everything non-shared is copied (structured clone). Pass ArrayBuffers in the transfer list to move instead of copy.
  • Every function shipped via runInThread, Task.spawn, or WorkerPool.create receives one extra trailing argument: a WorkerEnv ({ isMainThread, threadId, runtime, emit, transfer }). Task.spawnService methods are the one exception — they're invoked as plain methods (task.call(name, args)) with no env appended, so reach for globalThis.__unithread inside a service method instead.

Drop-in usage

Node / bundlersnpm install @jasonkneen/unithread, then import { ... } from "@jasonkneen/unithread".

Browser, zero build step — copy dist/unithread.bundle.min.js (8.5 kB) next to your page:

<script type="module">
  import { runInThread, WorkerPool, spawnRemote } from "./unithread.bundle.min.js";
  const svc = await spawnRemote({ square: (x) => x * x });
  console.log(await svc.square(12)); // 144 — computed on a real thread
  await svc.terminate();
</script>

For SharedArrayBuffer in the browser, serve with COOP/COEP (see demo/serve.mjs for a 30-line reference server):

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Everything except shared memory (spawn, pools, RPC proxies, transfers) works without those headers.

Verified (this build)

Node 24 — 21/21 (npm test) and real Chromium via Playwright — 11/11 (npm run test:browser), including the proofs that are impossible without independent OS threads:

  • main thread timers kept firing (~42 ticks) during a 400 ms synchronous worker spin — both runtimes
  • Node main thread blocked on Atomics.wait, released by a worker's notify; browser main thread same via waitAsync
  • 4 threads × 50 k atomic increments = exactly 200 000 on one SharedArrayBuffer — both runtimes
  • mutex-serialized non-atomic read-modify-write exact across 4 threads — both runtimes
  • zero-copy ArrayBuffer transfer worker→main — both runtimes
  • browser suite ran cross-origin-isolated (crossOriginIsolated === true)

Drop-in options

  1. npm tarball: npm i ./unithread-0.2.0 — full package with types.
  2. Single file: copy dist/unithread.bundle.js (13 kB, zero deps) or .min.js (8.5 kB) into your project; import { ... } from "./unithread.bundle.js" works in Node and browsers unchanged.
  3. Browser check: npm run demohttp://localhost:8080. Serves with COOP/COEP already set, so the SharedArrayBuffer proofs run rather than skip.

v0.2 additions

  • spawnRemote(methods) / wrap(task) — Comlink-style proxy: await svc.pow(2, 10).
  • Transferables both directions: pass a transfer list to run/call; return via env.transfer(value, [buffers]). Moved, not copied — source detaches.
  • Streaming: worker calls env.emit(event, data) mid-task; main thread subscribes with task.onEvent(cb). Also on globalThis.__unithread inside service methods.

Agent skill

skill/ packages unithread as a Claude Code skill: an agent can audit a codebase for main-thread hogs, offload them to threads, and prove the main thread was freed.

ln -s "$PWD/skill" ~/.claude/skills/unithread

Then ask an agent to "make this app stop janking". It will measure candidates, refuse to offload anything under ~16 ms, verify the shipped function captures nothing, and report a before/after — reverting if the number did not move.

Build / test / demo

npm install && npm run build   # tsc -> dist/, then esbuild -> dist/unithread.bundle[.min].js
npm test                       # node suite      — 21/21
npm run test:browser           # chromium suite  — 11/11 (needs: npx playwright install chromium)
npm run test:skill             # skill suite     — 28/28 (portability + measurement gates)
npm run demo                   # http://localhost:8080, served with COOP/COEP

npm run demo opens a live page with one switch on it. The same workload (fib(34) on a loop, ~110 ms a job) runs non-stop; the switch moves it between the main thread and a pool of worker threads, and a frame-time trace shows the result immediately:

| work runs on | fps | worst frame | jobs/sec | | --- | --- | --- | --- | | main thread | ~16 | ~133 ms | 7 | | 4 worker threads | ~59 | ~18 ms | 13 |

Measured on a 16-core M-series in Chromium. The frame trace goes from a solid wall of red (every frame missing the 16.7 ms budget) to solid green the instant you flip it.