Packages
duskmoon_quickbeam
9.13.0
10.0.3
10.0.2
10.0.1
10.0.0
9.16.16
9.16.15
9.16.14
9.16.13
9.16.12
9.16.11
9.16.10
9.16.9
9.16.7
9.16.6
9.16.2
9.16.1
9.16.0
9.14.0
9.13.1
9.13.0
9.12.3
9.12.2
9.12.1
9.12.0
9.11.0
9.10.1
9.10.0
9.9.10
9.9.9
9.9.8
9.9.7
9.9.6
9.9.4
9.9.3
9.9.2
9.9.1
9.9.0
9.8.0
9.7.1
9.7.0
9.6.4
9.6.3
9.6.2
9.6.1
9.6.0
Duskmoon fork of QuickBEAM — JavaScript runtime for the BEAM with Web APIs backed by OTP.
Current section
Files
Jump to
Current section
Files
duskmoon_quickbeam
README.md
README.md
# QuickBEAM
JavaScript runtime for the BEAM — Web APIs backed by OTP, native DOM,
and a built-in TypeScript toolchain.
JS runtimes are GenServers. They live in supervision trees, send and
receive messages, and call into Erlang/OTP libraries — all without
leaving the BEAM.
## Installation
```elixir
def deps do
[{:duskmoon_quickbeam, "~> 9.0"}]
end
```
Requires Zig 0.15+ (installed automatically by Zigler, or use system Zig).
## Quick start
```elixir
{:ok, rt} = QuickBEAM.start()
{:ok, 3} = QuickBEAM.eval(rt, "1 + 2")
{:ok, "HELLO"} = QuickBEAM.eval(rt, "'hello'.toUpperCase()")
# State persists across calls
QuickBEAM.eval(rt, "function greet(name) { return 'hi ' + name }")
{:ok, "hi world"} = QuickBEAM.call(rt, "greet", ["world"])
QuickBEAM.stop(rt)
```
## BEAM integration
JS can call Elixir functions and access OTP libraries:
```elixir
{:ok, rt} = QuickBEAM.start(handlers: %{
"db.query" => fn [sql] -> MyRepo.query!(sql).rows end,
"cache.get" => fn [key] -> Cachex.get!(:app, key) end,
})
{:ok, rows} = QuickBEAM.eval(rt, """
const rows = await Beam.call("db.query", "SELECT * FROM users LIMIT 5");
rows.map(r => r.name);
""")
```
JS can also send messages to any BEAM process:
```javascript
// Get the runtime's own PID
const self = Beam.self();
// Send to any PID
Beam.send(somePid, {type: "update", data: result});
// Receive BEAM messages
Beam.onMessage((msg) => {
console.log("got:", msg);
});
// Monitor BEAM processes
const ref = Beam.monitor(pid, (reason) => {
console.log("process died:", reason);
});
Beam.demonitor(ref);
```
### `Beam` API reference
| Category | API | Description |
|---|---|---|
| **Bridge** | `Beam.call(name, ...args)` | Call an Elixir handler (async) |
| | `Beam.callSync(name, ...args)` | Call an Elixir handler (sync) |
| | `Beam.send(pid, message)` | Send a message to a BEAM process |
| | `Beam.onMessage(callback)` | Receive BEAM messages |
| **Process** | `Beam.self()` | PID of the owning GenServer |
| | `Beam.spawn(script)` | Spawn a new JS runtime as a BEAM process |
| | `Beam.register(name)` | Register the runtime under a name |
| | `Beam.whereis(name)` | Look up a registered runtime |
| | `Beam.monitor(pid, callback)` | Monitor a process for exit |
| | `Beam.demonitor(ref)` | Cancel a monitor |
| | `Beam.link(pid)` / `Beam.unlink(pid)` | Bidirectional crash propagation |
| **Distribution** | `Beam.nodes()` | List connected BEAM nodes |
| | `Beam.rpc(node, runtime, fn, ...args)` | Remote call to another node |
| **Utilities** | `Beam.sleep(ms)` / `Beam.sleepSync(ms)` | Async/sync sleep |
| | `Beam.hash(data, range?)` | Non-cryptographic hash (`:erlang.phash2`) |
| | `Beam.escapeHTML(str)` | Escape `& < > " '` |
| | `Beam.which(bin)` | Find executable on PATH |
| | `Beam.peek(promise)` / `Beam.peek.status(promise)` | Read promise result without await |
| | `Beam.randomUUIDv7()` | Monotonic sortable UUID |
| | `Beam.deepEquals(a, b)` | Deep structural equality |
| | `Beam.nanoseconds()` | Monotonic high-res timer |
| | `Beam.uniqueInteger()` | Monotonically increasing unique integer |
| | `Beam.makeRef()` | Create a unique BEAM reference |
| | `Beam.inspect(value)` | Pretty-print any value (including PIDs/refs) |
| **Semver** | `Beam.semver.satisfies(version, range)` | Check version against Elixir requirement |
| | `Beam.semver.order(a, b)` | Compare two semver strings |
| **Password** | `Beam.password.hash(password, opts?)` | PBKDF2-SHA256 hash |
| | `Beam.password.verify(password, hash)` | Constant-time verification |
| **Introspection** | `Beam.version` | QuickBEAM version string |
| | `Beam.systemInfo()` | Schedulers, memory, atoms, OTP release |
| | `Beam.processInfo()` | Memory, reductions, message queue |
## Supervision
Runtimes and context pools are OTP children with crash recovery:
```elixir
children = [
{QuickBEAM,
name: :renderer,
id: :renderer,
script: "priv/js/app.js",
handlers: %{
"db.query" => fn [sql, params] -> Repo.query!(sql, params).rows end,
}},
{QuickBEAM, name: :worker, id: :worker},
# Context pool for high-concurrency use cases
{QuickBEAM.ContextPool, name: MyApp.JSPool, size: 4},
]
Supervisor.start_link(children, strategy: :one_for_one)
{:ok, html} = QuickBEAM.call(:renderer, "render", [%{page: "home"}])
```
The `:script` option loads a JS file at startup. If the runtime crashes,
the supervisor restarts it with a fresh context and re-evaluates the script.
Individual `Context` processes are typically started dynamically (e.g.
from a LiveView `mount`) and linked to the connection process.
## Context Pool
For high-concurrency scenarios (thousands of connections), use
`ContextPool` instead of individual runtimes. Many lightweight JS
contexts share a small number of runtime threads:
```elixir
# Start a pool with N runtime threads (defaults to scheduler count)
{:ok, pool} = QuickBEAM.ContextPool.start_link(name: MyApp.JSPool, size: 4)
# Each context is a GenServer with its own JS global scope
{:ok, ctx} = QuickBEAM.Context.start_link(pool: MyApp.JSPool)
{:ok, 3} = QuickBEAM.Context.eval(ctx, "1 + 2")
{:ok, "HELLO"} = QuickBEAM.Context.eval(ctx, "'hello'.toUpperCase()")
QuickBEAM.Context.stop(ctx)
```
Contexts support the full API — `eval`, `call`, `Beam.call`/`callSync`,
DOM, messaging, browser/node APIs, handlers, and supervision:
```elixir
# In a Phoenix LiveView
def mount(_params, _session, socket) do
{:ok, ctx} = QuickBEAM.Context.start_link(
pool: MyApp.JSPool,
handlers: %{"db.query" => &MyApp.query/1}
)
{:ok, assign(socket, js: ctx)}
end
```
The context is linked to the LiveView process — it terminates and
cleans up automatically when the connection closes. No explicit
`terminate` callback needed.
### Granular API groups
Contexts can load individual API groups instead of the full browser bundle:
```elixir
QuickBEAM.Context.start_link(pool: pool, apis: [:beam, :fetch]) # 231 KB
QuickBEAM.Context.start_link(pool: pool, apis: [:beam, :url]) # 108 KB
QuickBEAM.Context.start_link(pool: pool, apis: false) # 58 KB
QuickBEAM.Context.start_link(pool: pool) # 429 KB (all browser APIs)
```
Available groups: `:fetch`, `:websocket`, `:worker`, `:channel`,
`:eventsource`, `:url`, `:crypto`, `:compression`, `:buffer`, `:dom`,
`:console`, `:storage`, `:locks`. Dependencies auto-resolve.
### Per-context resource limits
```elixir
{:ok, ctx} = QuickBEAM.Context.start_link(
pool: pool,
memory_limit: 512_000, # per-context allocation limit (bytes)
max_reductions: 100_000 # opcode budget per eval/call
)
# Track per-context memory
{:ok, %{context_malloc_size: 92_000}} = QuickBEAM.Context.memory_usage(ctx)
```
Exceeding `memory_limit` triggers OOM. Exceeding `max_reductions`
interrupts the current eval but keeps the context usable for
subsequent calls.
## API surfaces
QuickBEAM can load browser APIs, Node.js APIs, or both:
```elixir
# Browser APIs only (default)
QuickBEAM.start(apis: [:browser])
# Node.js compatibility
QuickBEAM.start(apis: [:node])
# Both
QuickBEAM.start(apis: [:browser, :node])
# Bare QuickJS engine — no polyfills
QuickBEAM.start(apis: false)
```
## Node.js compatibility
Like Bun, QuickBEAM implements core Node.js APIs. BEAM-specific
extensions live in the `Beam` namespace.
```elixir
{:ok, rt} = QuickBEAM.start(apis: [:node])
QuickBEAM.eval(rt, """
const data = fs.readFileSync('/etc/hosts', 'utf8');
const lines = data.split('\\n').length;
lines
""")
# => {:ok, 12}
```
| Module | Coverage |
|---|---|
| `process` | `env`, `cwd()`, `platform`, `arch`, `pid`, `argv`, `version`, `nextTick`, `hrtime`, `stdout`, `stderr` |
| `path` | `join`, `resolve`, `basename`, `dirname`, `extname`, `parse`, `format`, `relative`, `normalize`, `isAbsolute`, `sep`, `delimiter` |
| `fs` | `readFileSync`, `writeFileSync`, `appendFileSync`, `existsSync`, `mkdirSync`, `readdirSync`, `statSync`, `lstatSync`, `unlinkSync`, `renameSync`, `rmSync`, `copyFileSync`, `realpathSync`, `readFile`, `writeFile` |
| `os` | `platform()`, `arch()`, `type()`, `hostname()`, `homedir()`, `tmpdir()`, `cpus()`, `totalmem()`, `freemem()`, `uptime()`, `EOL`, `endianness()` |
`process.env` is a live Proxy — reads and writes go to `System.get_env` / `System.put_env`.
## Resource limits
```elixir
{:ok, rt} = QuickBEAM.start(
memory_limit: 10 * 1024 * 1024, # 10 MB heap
max_stack_size: 512 * 1024 # 512 KB call stack
)
```
## Introspection
```elixir
# List user-defined globals (excludes builtins)
{:ok, ["myVar", "myFunc"]} = QuickBEAM.globals(rt, user_only: true)
# Get any global's value
{:ok, 42} = QuickBEAM.get_global(rt, "myVar")
# Runtime diagnostics
QuickBEAM.info(rt)
# %{handlers: ["db.query"], memory: %{...}, global_count: 87}
```
### Bytecode disassembly
Disassemble QuickJS bytecode into structured Elixir terms — like
`:beam_disasm` for the BEAM:
```elixir
{:ok, bc} = QuickBEAM.disasm(rt, "function fib(n) { if (n <= 1) return n; return fib(n-1) + fib(n-2) }")
fib = hd(bc.cpool)
fib.name # "fib"
fib.args # ["n"]
fib.stack_size # 4
fib.opcodes
# [
# {0, :get_arg0, 0},
# {1, :push_1, 1},
# {2, :lte},
# {3, :if_false8, 7},
# {5, :get_arg0, 0},
# {6, :return},
# {7, :get_var, "fib"},
# {12, :get_arg0, 0},
# {13, :push_1, 1},
# {14, :sub},
# {15, :call1, 1},
# ...
# ]
```
`disasm/1` works on precompiled bytecode binaries without a runtime:
```elixir
{:ok, bytecode} = QuickBEAM.compile(rt, source)
# later, even on a different node:
{:ok, %QuickBEAM.Bytecode{}} = QuickBEAM.disasm(bytecode)
```
## DOM
Every runtime has a live DOM tree backed by [lexbor](https://github.com/lexbor/lexbor) (the C library
behind PHP 8.4's DOM extension and Elixir's `fast_html`). JS gets a full `document` global
with spec-compliant prototype chains (`instanceof HTMLElement` works), node identity
(`el.parentNode === el.parentNode`), and uppercase `tagName` for HTML elements:
```javascript
document.body.innerHTML = '<ul><li class="item">One</li><li class="item">Two</li></ul>';
const items = document.querySelectorAll("li.item");
items[0].textContent; // "One"
items[0] instanceof HTMLElement; // true
```
Elixir can read the DOM directly — no JS execution, no re-parsing:
```elixir
{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, ~s[document.body.innerHTML = '<h1 class="title">Hello</h1>'])
# Returns Floki-compatible {tag, attrs, children} tuples
{:ok, {"h1", [{"class", "title"}], ["Hello"]}} = QuickBEAM.dom_find(rt, "h1")
# Batch queries
{:ok, items} = QuickBEAM.dom_find_all(rt, "li")
# Extract text and attributes
{:ok, "Hello"} = QuickBEAM.dom_text(rt, "h1")
{:ok, "/about"} = QuickBEAM.dom_attr(rt, "a", "href")
# Serialize back to HTML
{:ok, html} = QuickBEAM.dom_html(rt)
```
## Web APIs
Standard browser APIs backed by BEAM primitives, not JS polyfills:
| JS API | BEAM backend |
|---|---|
| `fetch`, `Request`, `Response`, `Headers` | `:httpc` |
| `document`, `querySelector`, `createElement` | lexbor (native C DOM) |
| `URL`, `URLSearchParams` | `:uri_string` |
| `EventSource` (SSE) | `:httpc` streaming |
| `WebSocket` | `Mint.WebSocket` |
| `Worker` | BEAM process per worker |
| `BroadcastChannel` | `:pg` (distributed) |
| `navigator.locks` | GenServer + monitors |
| `localStorage` | ETS |
| `crypto.subtle` | `:crypto` |
| `crypto.getRandomValues`, `randomUUID` | Zig `std.crypto.random` |
| `ReadableStream`, `WritableStream`, `TransformStream` | Pure TS with `pipeThrough`/`pipeTo` |
| `TextEncoder`, `TextDecoder` | Native Zig (UTF-8) |
| `TextEncoderStream`, `TextDecoderStream` | Stream + Zig encoding |
| `CompressionStream`, `DecompressionStream` | `:zlib` |
| `Buffer` | `Base`, `:unicode` |
| `EventTarget`, `Event`, `CustomEvent` | Pure TS |
| `AbortController`, `AbortSignal` | Pure TS |
| `Blob`, `File` | Pure TS |
| `DOMException` | Pure TS |
| `setTimeout`, `setInterval` | Timer heap in worker thread |
| `console` (log, warn, error, debug, time, group, …) | Erlang Logger |
| `atob`, `btoa` | Native Zig |
| `performance.now` | Nanosecond precision |
| `structuredClone` | QuickJS serialization |
| `queueMicrotask` | `JS_EnqueueJob` |
## Data conversion
No JSON in the data path. JS values map directly to BEAM terms:
| JS | Elixir |
|---|---|
| `number` (integer) | `integer` |
| `number` (float) | `float` |
| `string` | `String.t()` |
| `boolean` | `boolean` |
| `null` | `nil` |
| `undefined` | `nil` |
| `Array` | `list` |
| `Object` | `map` (string keys) |
| `Uint8Array` | `binary` |
| `Symbol("name")` | `:name` (atom) |
| `Infinity` / `NaN` | `:Infinity` / `:NaN` |
| PID / Ref / Port | Opaque JS object (round-trips) |
## TypeScript
Type definitions for the BEAM-specific JS API:
```jsonc
// tsconfig.json
{
"compilerOptions": {
"types": ["./path/to/quickbeam.d.ts"]
}
}
```
The `.d.ts` file covers the `Beam` bridge API, opaque BEAM terms
(`BeamPid`, `BeamRef`, `BeamPort`), and the `compression` helper.
Standard Web APIs are typed by TypeScript's `lib.dom.d.ts`.
## TypeScript toolchain
QuickBEAM includes a built-in TypeScript toolchain via [OXC](https://oxc.rs)
Rust NIFs — no Node.js or Bun required:
```elixir
# Evaluate TypeScript directly
{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval_ts(rt, "const x: number = 40 + 2; x")
# => {:ok, 42}
# Transform, minify, bundle — available as QuickBEAM.JS.*
{:ok, js} = QuickBEAM.JS.transform("const x: number = 1", "file.ts")
{:ok, min} = QuickBEAM.JS.minify("const x = 1 + 2;", "file.js")
# Bundle multiple modules into a single IIFE
files = [
{"utils.ts", "export function add(a: number, b: number) { return a + b }"},
{"main.ts", "import { add } from './utils'\nconsole.log(add(1, 2))"}
]
{:ok, bundle} = QuickBEAM.JS.bundle(files)
```
## npm packages
QuickBEAM can use [`npm_ex`](https://hex.pm/packages/npm) to install npm packages without Node.js. Add it when you want Mix-powered npm installs:
```elixir
def deps do
[
{:duskmoon_quickbeam, "~> 9.0"},
{:duskmoon_npm, "~> 9.0"}
]
end
```
```sh
mix npm.install sanitize-html
```
The `:script` option auto-resolves imports. Point it at a TypeScript file
that imports npm packages, and QuickBEAM bundles everything at startup:
```elixir
# priv/js/app.ts
import sanitize from 'sanitize-html'
Beam.onMessage((html: string) => {
Beam.callSync("done", sanitize(html))
})
```
```elixir
{QuickBEAM, name: :sanitizer, script: "priv/js/app.ts", handlers: %{...}}
```
No build step, no webpack, no esbuild. TypeScript is stripped, imports
are resolved from `node_modules/`, and everything is bundled into a
single script via OXC — all at runtime startup.
You can also bundle from disk programmatically:
```elixir
{:ok, js} = QuickBEAM.JS.bundle_file("src/main.ts")
```
## Performance
vs QuickJSEx 0.3.1 (Rust/Rustler, JSON serialization):
| Benchmark | Speedup |
|---|---|
| Function call — small map | **2.5x faster** |
| Function call — large data | **4.1x faster** |
| Concurrent JS execution | **1.35x faster** |
| `Beam.callSync` (JS→BEAM) | 5 μs overhead (unique to QuickBEAM) |
| Startup | ~600 μs (parity) |
Context pool vs individual runtimes at scale:
| | Runtime (1:1 thread) | Context (pooled) |
|---|---|---|
| JS heap per instance | ~530 KB | ~429 KB (full) / ~58 KB (bare) |
| OS thread stack | ~2.5 MB each | shared (4 threads total) |
| OS threads at 10K | 10,000 | 4 (configurable) |
| Total RAM at 10K | ~30 GB | ~4.2 GB (full) / ~570 MB (bare) |
See [`bench/`](https://github.com/elixir-volt/quickbeam/tree/master/bench) for details.
## When to use what
| Use case | Module | Why |
|---|---|---|
| One-off eval, scripting | `QuickBEAM` (Runtime) | Simple, full isolation |
| SSR request pool | `QuickBEAM.Pool` | Checkout/checkin with reset |
| Per-connection state (LiveView) | `QuickBEAM.Context` | Lightweight, thousands concurrent |
| Sandboxed user code | `QuickBEAM` or `Context` with `apis: false` | Memory limits, reduction limits, timeouts |
## Development
```sh
mix ci
```
Runs the full project quality gate: formatting, Credo, Dialyzer, Zig lint,
TypeScript lint, duplicate-code checks, and tests.
## Examples
- [`examples/chat_room/`](https://github.com/elixir-volt/quickbeam/tree/master/examples/chat_room) — real-time chat
with Phoenix Channels. Each room is a supervised QuickBEAM runtime; JS
broadcasts become PubSub pushes to WebSocket clients.
- [`examples/ai_agent/`](https://github.com/elixir-volt/quickbeam/tree/master/examples/ai_agent) — conversational
AI agent with streaming, tool use, and pluggable LLM backends. JS
orchestrates the conversation loop; Elixir provides the I/O.
- [`examples/counter_live/`](https://github.com/elixir-volt/quickbeam/tree/master/examples/counter_live) — LiveView
counter where each session gets a ~58 KB JS context from a shared pool.
The simplest QuickBEAM + Phoenix integration.
- [`examples/ssr/`](https://github.com/elixir-volt/quickbeam/tree/master/examples/ssr) — Preact SSR with a pool of runtimes
and native DOM. Elixir reads the DOM directly — no `renderToString`.
- [`examples/rule_engine/`](https://github.com/elixir-volt/quickbeam/tree/master/examples/rule_engine) — user-defined business
rules (pricing, validation, transforms) in sandboxed JS runtimes with
`apis: false`, memory limits, timeouts, and hot reload.
- [`examples/live_dashboard/`](https://github.com/elixir-volt/quickbeam/tree/master/examples/live_dashboard) — Workers (BEAM
processes) compute metrics in parallel and broadcast results via
BroadcastChannel (`:pg`). Crash recovery via OTP supervisor.
## License
MIT