# 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 = '