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

experimental-threads

v0.0.8

Published

Highly experimental multithreading runtime in JavaScript using lexical scope analysis and shared memory rehydration. Works in Deno, Bun and Node.

Readme

experimental-threads

Status License: MIT TypeScript Node.js Deno Bun

experimental-threads is a concurrency library for server-side JavaScript and TypeScript (Node.js, Deno, Bun). It runs inline closures in Web Workers — no separate entry files, no manual message passing. Variables from the enclosing scope are captured automatically via static AST analysis and transferred into the worker context.

The API is structurally similar to thread spawning in systems languages like Rust or Go.

Installation

npm install experimental-threads

Usage

Spawning a thread

spawn captures the closure's free variables and returns a script string. Wrapping it in eval() bridges the local scope at the call site, serializes the captured variables, and runs the closure in a worker.

import { spawn } from "experimental-threads";
import * as bcrypt from "bcrypt";

const userRequest = { username: "admin", password: "correct_horse_battery_staple" };
const saltRounds = 12;

// 'userRequest' and 'saltRounds' are captured from the enclosing scope,
// cloned, and transferred to the worker automatically.
const hash = await eval(spawn(async () => {
  return await bcrypt.hash(userRequest.password, saltRounds);
}));

console.log(hash); // "$2b$12$..."

Note: The eval() wrapper is required — it is what bridges the call site's lexical scope into the generated script string. See Architecture for details.

Shared memory and mutexes

Web Workers run in separate V8 isolates, so module-level objects (including locks) are independent in each worker. Global<T> fixes this by pinning a SharedArrayBuffer-backed resource to its source location, ensuring all isolates share the same underlying memory.

import { Global, Mutex, spawn } from "experimental-threads";

// This Mutex wraps a SharedArrayBuffer. Because it is Global<T>, every
// worker that imports this module gets the same underlying memory buffer.
const sharedLock = new Global(new Mutex(new SharedArrayBuffer(4)));

// Main thread: acquire the lock and write an initial value
{
  using guard = await sharedLock.value.lock();
  new Int32Array(guard.value)[0] = 1;
}

await eval(spawn(async () => {
  using guard = await sharedLock.value.lock();
  const view = new Int32Array(guard.value);

  console.log(view[0]); // 1
  view[0] = 2;

  // Workers can spawn nested sub-threads
  await eval(spawn(async () => {
    using guard = await sharedLock.value.lock();
    new Int32Array(guard.value)[0] = 3;
  }));
}));

{
  using guard = await sharedLock.value.lock();
  console.log(new Int32Array(guard.value)[0]); // 3
}

MutexGuard implements Symbol.dispose, so the using keyword releases the lock automatically at scope exit. You can also call guard.unlock() explicitly.

Semaphore

Semaphore controls access to a resource with a fixed number of permits.

import { Semaphore } from "experimental-threads";

const sem = new Semaphore(3); // 3 concurrent permits

{
  using _permit = await sem.acquire();
  // up to 3 holders at a time
}
// permit released automatically

sem.release(1); // or release manually

API

spawn<T>(fn: () => T): WorkerScript<T>

Statically analyzes the closure, identifies its free variables, and returns a script string encoding the worker bootstrap. Must be called with eval() to capture runtime values.

shutdown(): void

Terminates all pooled workers and clears internal caches. Required for clean process exit (e.g., at the end of tests).

Global<T extends SharedStruct | SharedArrayBuffer>

Wraps a SharedArrayBuffer-backed value and gives it a stable identity across isolates derived from its source location (file + line + column). Instantiating Global<T> at the same call site in any worker will point to the same underlying memory as the main thread.

Mutex<T>

An async mutual exclusion lock backed by Atomics.waitAsync.

  • await mutex.lock(): Promise<MutexGuard<T>> — acquires the lock
  • guard.unlock() / guard[Symbol.dispose]() — releases it
  • Supports using guard = await mutex.lock() for automatic release

Semaphore

An async counting semaphore backed by Atomics.waitAsync.

  • await semaphore.acquire(amount?: number) — decrements permits, blocks if insufficient; returns a disposable guard
  • semaphore.release(amount?: number) — restores permits and wakes waiters

Architecture

Lexical scope capture

JavaScript has no built-in way to inspect the variables captured by a closure. experimental-threads extracts them at the call site:

  1. Call site resolutionspawn() reads the V8 stack trace to find its own call site (file, line, column).
  2. AST analysis — the source file is parsed with the TypeScript Compiler API. The AST is traversed to locate the spawn() call and identify its closure's free variables — identifiers referenced inside the function but defined outside it.
  3. Code generation — a standalone worker entry script is produced from the caller's source, with relative import paths rewritten to absolute file:// URLs so they resolve from the .workers/ directory.
  4. Scope bridgingspawn() returns a code snippet of the form __worker_wrapper__({a, b, c}, ...). Evaluating this with eval() in the caller's scope captures the runtime values of the free variables. Those values are structured-cloned (with Transferable objects zero-copy transferred) and sent to the worker.

Shared memory hydration

Because each V8 isolate runs module code independently, a new Mutex() in a worker creates a fresh, unrelated lock. Global<T> solves this with location-based identity:

  • On the main thread, new Global(value) registers the underlying SharedArrayBuffer under a key derived from the call site.
  • On a worker, the same constructor intercepts the allocation. During bootstrap, the main thread sends its full memory registry to the worker. The Global<T> constructor looks up its key and hydrates from the parent's buffer rather than allocating a new one.

This guarantees that sharedLock.value in a worker is backed by the same SharedArrayBuffer as in the main thread.

Worker pooling

Workers are pooled by a signature derived from the call site and the set of captured variable names. An idle worker is reused for subsequent identical calls. Workers that remain idle for 30 seconds are terminated. A warning is logged if the total active worker count exceeds 4× hardware concurrency.

Limitations

  • eval is required. The scope-bridging mechanism depends on evaluating the generated script in the caller's lexical scope. This restricts usage to trusted, server-side code. Never pass user-provided input through spawn or eval.

License

MIT — see LICENSE.