# quickbeam **Repository Path**: iuact/quickbeam ## Basic Information - **Project Name**: quickbeam - **Description**: https://github.com/elixir-volt/quickbeam JavaScript runtime for the BEAM — Web APIs backed by OTP, native DOM, and a built-in TypeScript toolchain. - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-04-16 - **Last Updated**: 2026-04-16 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 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 [{:quickbeam, "~> 0.7.1"}] 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 = ''; 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 = '

Hello

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