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

quickbeam-js

v0.1.0

Published

OTP patterns in JavaScript, on the BEAM — Supervisors, GenServers, Registries, Pools, Tasks, and Applications built on QuickBEAM primitives.

Readme


The elevator pitch

You write JavaScript. You want Erlang's reliability — self-healing processes, supervision trees, message-passing resilience. quickbeam-js gives you exactly that.

import { GenServer, Supervisor, Pool, Registry, Application } from "quickbeam-js";

// A GenServer is just a class with a few lifecycle methods
class Counter extends GenServer {
  async init() {
    return { count: 0 };               // initial state
  }

  async handleCall("inc", [by], state) {
    const next = state.count + by;
    return { reply: next, state: { count: next } };
  }

  async handleCall("get", [], state) {
    return { reply: state.count, state };
  }

  async handleCast("reset", [], _state) {
    console.log("resetting…");
    return { state: { count: 0 } };
  }
}

// A supervisor restarts workers on failure — automatically
const sup = await Supervisor.start({
  strategy: "one_for_one",
  children: [
    { id: "cnt", start: () => Counter.startLink({ name: "my_counter" }) },
  ],
});

// Call it — like GenServer.call/2 in Elixir, but JS-native
const n = await GenServer.call("my_counter", "inc", [3]);
console.log(n); // 3

// Cast it — fire and forget
GenServer.cast("my_counter", "reset", []);

That's it. No Elixir modules. No .ex files. The only Elixir you need is the bootstrap line that starts QuickBEAM.


Installation

Prerequisites

  • Elixir ≥ 1.17 — QuickBEAM runs on the BEAM VM
  • Node.js ≥ 18 — for npm install and TypeScript tooling

Step 1: Add QuickBEAM to your Elixir project

In mix.exs:

defp deps do
  [
    {:quickbeam, "~> 0.10"}
  ]
end

Run mix deps.get.

Step 2: Install the JS library

npm install quickbeam-js

This gets you TypeScript types (import { GenServer } from "quickbeam-js") and source files.

Step 3: Choose your integration path

You have two options:

Path A: Bundle your app + quickbeam-js together (recommended)

Wrap your entry point and let QuickBEAM's OXC bundler resolve imports through node_modules:

# lib/my_app/bundle.ex
defmodule MyApp.Bundle do
  @app_js QuickBEAM.JS.bundle_file!("assets/js/app.ts", drop_console: false)
  def app_js, do: @app_js
end
// assets/js/app.ts — your entry point
import { Application, GenServer, Supervisor, Pool } from "quickbeam-js";

class Worker extends GenServer {
  async init() { return { count: 0 }; }
  async handleCall("work", [data], state) {
    const result = await processJob(data);
    return { reply: result, state: { count: state.count + 1 } };
  }
}

Application.start({
  id: "my_app",
  env: { port: 8080 },
  supervisor: {
    strategy: "one_for_one",
    children: [
      { id: "pool", start: () => Pool.start({ name: "workers", size: 4, child: Worker }) },
    ],
  },
});

Now in your Elixir supervision tree:

children = [
  {QuickBEAM, name: :app, script: MyApp.Bundle.app_js()}
]

The bundler reads assets/js/app.ts, follows every import, inlines all of quickbeam-js, and produces a single JS string — ready for the BEAM.

Path B: Drop in the pre-built bundle (quickstart)

The npm package ships a pre-built single-file bundle at node_modules/quickbeam-js/dist/quickbeam-js.bundle.js. Copy it into your project and load it directly:

# In your supervision tree:
children = [
  {QuickBEAM, name: :app, script: "priv/quickbeam-js.bundle.js"}
]

The bundle sets globalThis.QuickbeamJs, so your own scripts can reference it:

// priv/my_script.js — loaded in a second QuickBEAM runtime
const { GenServer, Supervisor } = QuickbeamJs;

class Counter extends GenServer {
  async init() { return { count: 0 }; }
  async handleCall("inc", [], state) {
    const next = state.count + 1;
    return { reply: next, state: { count: next } };
  }
}

await GenServer.startLink(Counter, { name: "counter" });
const n = await GenServer.call("counter", "inc", []);
console.log(n); // 1

Want the Elixir helpers as a module?

Copy lib/quickbeam_js.ex from the npm package into your project's lib/ directory, or reference it directly with a path dependency:

# mix.exs
{:quickbeam_js, path: "node_modules/quickbeam-js"}

Then you get QuickbeamJs.prebuilt_bundle_path/0 and QuickbeamJs.bundle_app!/1 at your fingertips.

Which path? Use Path A for production apps — you get tree-shaking, TypeScript checking, and your app and quickbeam-js are one optimised bundle. Use Path B for prototyping and quick experiments.

🤖 AI‑assisted setup

Copy this prompt into Claude, ChatGPT, Cursor, or any other AI coding tool. It will scaffold an entire quickbeam-js project in one shot — no Elixir knowledge needed.

Set up a new QuickBEAM + quickbeam-js project called "my_app" in the current
directory. Follow these steps exactly:

1. SYSTEM CHECK
   - If Elixir is not available, install it via mise:
     - If mise is not installed: curl https://mise.run | sh
     - Then: mise install elixir@latest && mise use elixir@latest
   - Verify: elixir --version outputs Elixir >= 1.17
   - If Node.js is not available: mise install node@lts && mise use node@lts

2. PROJECT SCAFFOLDING
   - Run: mix new my_app --sup
   - cd my_app

3. DEPENDENCIES
   - Add to mix.exs deps:
       {:quickbeam, "~> 0.10"}
   - Run: mix deps.get
   - Run: npm init -y && npm install quickbeam-js typescript
   - Create tsconfig.json with target ES2022, strict mode

4. APP ENTRY POINT
   - Create assets/js/app.ts:
     ```typescript
     import { Application, GenServer, Supervisor, Pool } from "quickbeam-js";

     class Worker extends GenServer {
       async init(args: { id: number }) {
         return { id: args.id, processed: 0 };
       }
       async handleCall("work", [job], state) {
         const result = `worker ${state.id} processed: ${job}`;
         return { reply: result, state: { ...state, processed: state.processed + 1 } };
       }
     }

     Application.start({
       id: "my_app",
       env: { port: 4000 },
       supervisor: {
         strategy: "one_for_one",
         children: [
           { id: "pool", start: () => Pool.start({
               name: "workers", size: 4, child: Worker, childArgs: { id: 0 }
           })},
         ],
       },
     });
     ```

5. BUNDLER MODULE
   - Create lib/my_app/bundle.ex:
     ```elixir
     defmodule MyApp.Bundle do
       @app_js QuickBEAM.JS.bundle_file!("assets/js/app.ts", drop_console: false)
       def app_js, do: @app_js
     end
     ```

6. SUPERVISION TREE
   - In lib/my_app/application.ex, add QuickBEAM to the children list:
     ```elixir
     children = [
       {QuickBEAM, name: :app, script: MyApp.Bundle.app_js()}
     ]
     ```

7. VERIFY
   - Run: mix compile --force
   - Run: iex -S mix
   - The app should boot without errors
   - The pool of 4 workers is now running on the BEAM

8. DONE
   - Print a summary of what was created and how the user can interact with it

Replace "my_app" with your project name and adjust Worker to do your actual work. The scaffolded project compiles at build time — your entire app + quickbeam-js becomes one JS string, loaded onto the BEAM.


Tour

Every feature below runs inside QuickBEAM — a real BEAM process gets spawned, monitored, and messaged. The TypeScript types are exact; the examples compile.

1. GenServer — the core pattern

GenServer is the foundation. It's a long-lived process with synchronous call, asynchronous cast, and a handleInfo hook for arbitrary messages.

class Cache extends GenServer {
  async init(args: { ttl: number }) {
    return { ttl: args.ttl, entries: new Map() };
  }

  async handleCall("get", [key], state) {
    const entry = state.entries.get(key);
    if (!entry) return { reply: undefined, state };
    if (Date.now() - entry.ts > state.ttl) {
      state.entries.delete(key);
      return { reply: undefined, state };
    }
    return { reply: entry.value, state };
  }

  async handleCall("set", [key, value], state) {
    state.entries.set(key, { value, ts: Date.now() });
    return { reply: "ok", state };
  }

  // handleInfo catches everything that's not a call or cast —
  // system messages, direct Beam.send(), monitor exits, etc.
  async handleInfo(msg: { type: "evict_stale" }, state) {
    const cutoff = Date.now() - state.ttl;
    for (const [k, v] of state.entries) {
      if (v.ts < cutoff) state.entries.delete(k);
    }
    return { state };
  }

  async terminate(reason, state) {
    console.log("cache shutting down:", reason, `had ${state.entries.size} entries`);
  }
}

const pid = await GenServer.startLink(Cache, { name: "cache", args: { ttl: 60000 } });

await GenServer.call("cache", "set", ["user:42", { name: "Alice" }]);
const user = await GenServer.call("cache", "get", ["user:42"]);
// => { name: "Alice" }

2. Supervisor — crash recovery, automatically

Supervisors watch children via BEAM monitors. When a child exits, the supervisor restarts it according to the strategy — no polling, no health checks, no try/catch wrappers. The VM tells you the instant it's down.

class Worker extends GenServer {
  async init(id: string) {
    return { id, processed: 0 };
  }

  async handleCall("work", [data], state) {
    // If this throws, the BEAM notifies the supervisor.
    // The supervisor restarts the GenServer — fresh state, ready to go.
    const result = await riskyOperation(data);
    return { reply: result, state: { ...state, processed: state.processed + 1 } };
  }
}

const sup = await Supervisor.start({
  strategy: "one_for_one",       // only restart the crashed child
  max_restarts: 5,               // at most 5 restarts…
  max_seconds: 10,               // …in a 10-second window
  children: [
    {
      id: "worker1",
      start: () => Worker.startLink({ name: "w1", args: "worker-1" }),
      restart: "permanent",      // always restart
    },
    {
      id: "cache",
      start: () => Cache.startLink({ name: "cache", args: { ttl: 30000 } }),
      restart: "transient",      // restart only on abnormal exit
      shutdown: "infinity",      // wait forever during graceful shutdown
    },
    {
      id: "one-shot",
      start: () => OneShot.startLink({ name: "os" }),
      restart: "temporary",      // never restart
    },
  ],
});

// Dynamic children at runtime
await Supervisor.startChild(sup, {
  id: "worker2",
  start: () => Worker.startLink({ name: "w2", args: "worker-2" }),
});

// Terminate on demand
await Supervisor.terminateChild(sup, "worker2");

Strategies:

| Strategy | On child crash… | |----------|------------------| | one_for_one | Restart only the failed child | | one_for_all | Restart every child | | rest_for_one | Restart the failed child and all started after it |

3. Registry — process discovery

The registry maps names to PIDs. When a registered process exits, its entry is automatically removed — no stale references.

const reg = await Registry.start("users");

// Register
const alice = await UserSession.startLink({ name: "alice" });
reg.register("alice", alice, { role: "admin" });

// Look up
const [pid, meta] = reg.lookup("alice");
// pid = <0.123.0>, meta = { role: "admin" }
await GenServer.call(pid, "get_profile", []);

// Match by pattern
for (const [pid, meta] of reg.match((key, _val) => key.startsWith("admin:"))) {
  Beam.send(pid, { type: "alert", text: "Deploy starting" });
}

// Duplicate keys for pub/sub groups
const rooms = await Registry.start("rooms", { keys: "duplicate" });
rooms.register("room:lobby", bob);
rooms.register("room:lobby", charlie);
// => both bob and charlie are under "room:lobby"

4. Pool — worker checkout, with backpressure

A fixed-size pool of GenServer workers. Checkout blocks when the pool is exhausted; checkin returns a worker to the idle queue. Built on top of a supervisor, so dead workers are automatically replaced.

class Renderer extends GenServer {
  async handleCall("render", [template, data], state) {
    const html = compile(template, data);     // runs inside QuickBEAM's real DOM
    return { reply: html, state };
  }
}

const pool = await Pool.start({
  name: "renderers",
  size: 4,                // 4 CSS-renderer processes
  child: Renderer,
  strategy: "fifo",       // or "lifo" for most-recently-used
});

// Checkout → use → checkin (bare metal)
const worker = await pool.checkout();
const html = await GenServer.call(worker, "render", ["home", { user: "alice" }]);
pool.checkin(worker);

// Transaction — auto-checkin, even on throw
const html = await pool.transaction(async (worker) => {
  return await GenServer.call(worker, "render", ["home", { user: "bob" }]);
});

// Inspect
pool.status(); // { size: 4, active: 2, idle: 2, overflow: 0 }

5. Task — async/await one‑shots

Spawn a process to do one thing, then await the result. Fire-and-forget is one method call away.

// Await a result
const ref = Task.async(() => fetchJson("https://api.example.com/data"));
const data = await Task.await(ref, 10_000);   // timeout after 10s

// Cancel if needed
ref.cancel();

// Fire and forget — no await required
Task.start(() => sendAnalytics("page_view", { path: "/home" }));

6. Application — the top-level boot

Wrap your supervision tree in an application. It's the entry-point you hand to QuickBEAM's Elixir bootstrap.

import { Application } from "quickbeam-js";

Application.start({
  id: "my_app",
  env: { port: 8080, log_level: "debug" },

  supervisor: {
    strategy: "one_for_one",
    children: [
      { id: "registry", start: () => Registry.start("app_registry") },
      {
        id: "pool",
        start: () => Pool.start({
          name: "workers",
          size: 8,
          child: MyWorker,
        }),
      },
      {
        id: "api",
        start: () => ApiServer.startLink({ name: "api", args: { port: 8080 } }),
      },
    ],
  },
});

7. Error handling — _tag discriminated unions

Every quickbeam-js error extends the native Error with a _tag property — a string literal that TypeScript can narrow. Chain causes like Go's %w, walk them with findCause.

import { BeamOtpError, findCause } from "quickbeam-js";

class DbError extends Error {
  constructor(public readonly sql: string, cause: Error) {
    super(`DB error: ${sql}`, { cause });
    this.name = "DbError";
  }
}

try {
  const data = await GenServer.call("nonexistent", "get", []);
} catch (err) {
  // Narrow by _tag — zero ambiguity
  if (err instanceof BeamOtpError && err._tag === "BeamOtpError:noproc") {
    console.log("Nobody's home. Starting fallback…");
  }

  // Walk the cause chain to find root
  const dbErr = findCause(err, DbError);
  if (dbErr) {
    console.error("Offending query:", dbErr.sql);
  }

  // Or: err.findCause(DbError) if it's already a BeamOtpError
}

All error tags:

| _tag | When | |--------|------| | BeamOtpError:timeout | Call / checkout deadline exceeded | | BeamOtpError:exit | Target process exited mid-call | | BeamOtpError:noproc | No process registered under that name | | BeamOtpError:already_started | Name already taken | | BeamOtpError:not_found | Child or registry key not found | | BeamOtpError:shutdown | Supervisor is shutting down | | BeamOtpError:restart_limit | Too many restarts too fast |

8. Utilities — batteries included

import { sleep, retry, withTimeout } from "quickbeam-js";

// Non-blocking sleep (yields to the BEAM scheduler)
await sleep(100);

// Retry with exponential backoff
const result = await retry(
  () => flakyApiCall(),
  { maxAttempts: 5, baseDelayMs: 50, maxDelayMs: 2000 },
);

// Timeout any promise
const data = await withTimeout(
  () => fetch("https://slow.example.com").then(r => r.json()),
  3000,
  "slow-http-call",
);
// throws BeamOtpError:timeout if > 3s

9. Low‑level Beam primitives — full control

quickbeam-js re‑exports QuickBEAM's entire Beam.* API. When you don't need OTP wrappers, go direct:

import { Beam } from "quickbeam-js";

// Identity
const self = Beam.self();                 // opaque BeamPid object

// Spawn a raw process (script string — QuickBEAM native)
const pid = Beam.spawn(`
  Beam.onMessage((msg) => console.log("child got:", msg));
`);

// Raw message passing
Beam.send(pid, { type: "poke" });
Beam.onMessage((msg) => console.log("received:", msg));

// Monitor — get notified when a process dies
Beam.monitor(pid, (reason) => {
  console.log("process exited with:", reason);
});

// Links — bidirectional crash propagation
Beam.link(pid);    // if they crash, I crash
Beam.unlink(pid);  // severed

// Name registration
Beam.register("my_service");             // registers self
const found = Beam.whereis("my_service"); // PID or null

// Generate unique references
const ref = Beam.makeRef();

// Inspect
Beam.inspect(pid); // "#PID<0.123.0>"

// Time
Beam.sleep(50);
Beam.nanoseconds();

// Cluster
Beam.nodes(); // [:"app@host"]

// Cross-runtime calls
const result = await Beam.call("service@other", "get_state");

// Elixir ↔ JS bridge (from the Elixir side)
QuickBEAM.set_global(runtime, "config", %{port: 3000});
// …in JS:
// config.port === 3000

How it works

QuickBEAM embeds QuickJS (a full ES2022 engine) into the BEAM, then bridges JS processes to Erlang processes. quickbeam-js is a pure‑JavaScript library that sits on top of that bridge, providing the same abstractions you'd find in Elixir's OTP.

┌──────────────────────────────────────────────────┐
│ quickbeam-js (TypeScript)                        │
│   Supervisor · GenServer · Pool · Registry · …   │
├──────────────────────────────────────────────────┤
│ QuickBEAM (Zig + QuickJS + lexbor)               │
│   Beam.spawn · Beam.send · Beam.monitor · …      │
├──────────────────────────────────────────────────┤
│ BEAM VM (Erlang/OTP)                             │
│   Processes · Links · Monitors · Message Queues  │
└──────────────────────────────────────────────────┘

What it is not

  • Not a new JS engine. It runs on QuickBEAM's embedded QuickJS.
  • Not an Elixir library. You don't write .ex files to use it (just the one bootstrap line).
  • Not a framework. Use the pieces you need — raw Beam.*, a single GenServer, or a full Application tree.

Testing

Unit tests run against a mock BEAM implementation — no QuickBEAM required:

npm test           # 61 tests, pure JS
npx tsc --noEmit   # strict type check

Integration tests run against a real QuickBEAM runtime — this is the gold standard:

mix test           # 19 E2E tests on the real BEAM

License

MIT © monotykamary