Star 历史趋势
数据来源: GitHub API · 生成自 Stargazers.cn
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

def deps do [{:quickbeam, "~> 0.7.1"}] end

Requires Zig 0.15+ (installed automatically by Zigler, or use system Zig).

Quick start

{: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:

{: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:

// 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

CategoryAPIDescription
BridgeBeam.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
ProcessBeam.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
DistributionBeam.nodes()List connected BEAM nodes
Beam.rpc(node, runtime, fn, ...args)Remote call to another node
UtilitiesBeam.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)
SemverBeam.semver.satisfies(version, range)Check version against Elixir requirement
Beam.semver.order(a, b)Compare two semver strings
PasswordBeam.password.hash(password, opts?)PBKDF2-SHA256 hash
Beam.password.verify(password, hash)Constant-time verification
IntrospectionBeam.versionQuickBEAM 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:

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:

# 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:

# 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:

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

{: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:

# 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.

{: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}
ModuleCoverage
processenv, cwd(), platform, arch, pid, argv, version, nextTick, hrtime, stdout, stderr
pathjoin, resolve, basename, dirname, extname, parse, format, relative, normalize, isAbsolute, sep, delimiter
fsreadFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync, lstatSync, unlinkSync, renameSync, rmSync, copyFileSync, realpathSync, readFile, writeFile
osplatform(), 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

{:ok, rt} = QuickBEAM.start( memory_limit: 10 * 1024 * 1024, # 10 MB heap max_stack_size: 512 * 1024 # 512 KB call stack )

Introspection

# 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:

{: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:

{: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 (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:

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:

{: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 APIBEAM backend
fetch, Request, Response, Headers:httpc
document, querySelector, createElementlexbor (native C DOM)
URL, URLSearchParams:uri_string
EventSource (SSE):httpc streaming
WebSocket:gun
WorkerBEAM process per worker
BroadcastChannel:pg (distributed)
navigator.locksGenServer + monitors
localStorageETS
crypto.subtle:crypto
crypto.getRandomValues, randomUUIDZig std.crypto.random
ReadableStream, WritableStream, TransformStreamPure TS with pipeThrough/pipeTo
TextEncoder, TextDecoderNative Zig (UTF-8)
TextEncoderStream, TextDecoderStreamStream + Zig encoding
CompressionStream, DecompressionStream:zlib
BufferBase, :unicode
EventTarget, Event, CustomEventPure TS
AbortController, AbortSignalPure TS
Blob, FilePure TS
DOMExceptionPure TS
setTimeout, setIntervalTimer heap in worker thread
console (log, warn, error, debug, time, group, …)Erlang Logger
atob, btoaNative Zig
performance.nowNanosecond precision
structuredCloneQuickJS serialization
queueMicrotaskJS_EnqueueJob

Data conversion

No JSON in the data path. JS values map directly to BEAM terms:

JSElixir
number (integer)integer
number (float)float
stringString.t()
booleanboolean
nullnil
undefinednil
Arraylist
Objectmap (string keys)
Uint8Arraybinary
Symbol("name"):name (atom)
Infinity / NaN:Infinity / :NaN
PID / Ref / PortOpaque JS object (round-trips)

TypeScript

Type definitions for the BEAM-specific JS API:

// 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 Rust NIFs — no Node.js or Bun required:

# 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 ships with a built-in npm client — no Node.js required.

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:

# priv/js/app.ts import sanitize from 'sanitize-html' Beam.onMessage((html: string) => { Beam.callSync("done", sanitize(html)) })
{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:

{:ok, js} = QuickBEAM.JS.bundle_file("src/main.ts")

Performance

vs QuickJSEx 0.3.1 (Rust/Rustler, JSON serialization):

BenchmarkSpeedup
Function call — small map2.5x faster
Function call — large data4.1x faster
Concurrent JS execution1.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 eachshared (4 threads total)
OS threads at 10K10,0004 (configurable)
Total RAM at 10K~30 GB~4.2 GB (full) / ~570 MB (bare)

See bench/ for details.

When to use what

Use caseModuleWhy
One-off eval, scriptingQuickBEAM (Runtime)Simple, full isolation
SSR request poolQuickBEAM.PoolCheckout/checkin with reset
Per-connection state (LiveView)QuickBEAM.ContextLightweight, thousands concurrent
Sandboxed user codeQuickBEAM or Context with apis: falseMemory limits, reduction limits, timeouts

Development

mix ci

Runs the full project quality gate: formatting, Credo, Dialyzer, Zig lint, TypeScript lint, duplicate-code checks, and tests.

Examples

  • 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/ — conversational AI agent with streaming, tool use, and pluggable LLM backends. JS orchestrates the conversation loop; Elixir provides the I/O.
  • 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/ — Preact SSR with a pool of runtimes and native DOM. Elixir reads the DOM directly — no renderToString.
  • 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/ — Workers (BEAM processes) compute metrics in parallel and broadcast results via BroadcastChannel (:pg). Crash recovery via OTP supervisor.

License

MIT

关于 About

JavaScript runtime for the BEAM — Web APIs backed by OTP, native DOM, and a built-in TypeScript toolchain.

语言 Languages

C83.0%
Elixir9.2%
Zig5.3%
TypeScript1.7%
JavaScript0.8%
CMake0.0%

提交活跃度 Commit Activity

代码提交热力图
过去 52 周的开发活跃度
209
Total Commits
峰值: 183次/周
Less
More

核心贡献者 Contributors