QuickBEAM (QuickBEAM v0.11.0)

View Source

QuickJS-NG JavaScript engine embedded in the BEAM.

Each runtime is a GenServer holding a persistent JS context. State, functions, and variables survive across eval/2 and call/3 calls.

iex> {:ok, rt} = QuickBEAM.start()
iex> {:ok, 3} = QuickBEAM.eval(rt, "1 + 2")
iex> QuickBEAM.stop(rt)
:ok

Handlers

JS code can call Elixir functions via Beam.call and Beam.callSync:

iex> {:ok, rt} = QuickBEAM.start(handlers: %{
...>   "greet" => fn [name] -> "Hello, #{name}!" end
...> })
iex> QuickBEAM.eval(rt, ~s[Beam.callSync("greet", "world")])
{:ok, "Hello, world!"}
iex> QuickBEAM.stop(rt)
:ok

Supervision

Runtimes work as OTP children:

children = [
  {QuickBEAM, name: :app, script: "priv/js/app.js", handlers: %{...}},
]
Supervisor.start_link(children, strategy: :one_for_one)

Options

  • :name — GenServer name registration
  • :id — child spec ID (defaults to :name, then module)
  • :handlers — map of handler name → function for Beam.call/Beam.callSync
  • :script — path to a JS/TS file evaluated on startup. TypeScript files are automatically transformed. Files with import statements are automatically bundled — imports are resolved from the filesystem and node_modules/, then compiled into a single script via OXC.
  • :memory_limit — maximum JS heap in bytes (default: 256 MB)
  • :max_stack_size — maximum JS call stack in bytes (default: 8 MB)
  • :wasm_stack_size — WASM operand stack in bytes for guests started via the JS WebAssembly.instantiate path (default: 65536). Distinct from :max_stack_size (the JS call stack); raise it for guests whose deep init overflows the 64 KB default.
  • :wasm_heap_size — WASM auxiliary heap in bytes for the same path (default: 65536)
  • :max_convert_depth — maximum nesting depth for JS→BEAM value conversion (default: 32)
  • :max_convert_nodes — maximum total nodes for JS→BEAM value conversion (default: 10,000)

DOM

Each runtime has a live DOM tree backed by lexbor. JS gets document, querySelector, createElement, etc. Elixir can read the DOM directly via dom_find/2, dom_find_all/2, dom_text/2, dom_attr/3, and dom_html/1 — returning Floki-compatible {tag, attrs, children} tuples.

Summary

Functions

Call a global JavaScript function by name.

Compile JavaScript source to bytecode without executing it.

Get current JS coverage data for a runtime.

Disassemble precompiled bytecode into a %QuickBEAM.Bytecode{} struct.

Compile JavaScript source and disassemble the resulting bytecode.

Get an attribute value from the first element matching a CSS selector.

Find the first element matching a CSS selector in the runtime's DOM.

Find all elements matching a CSS selector in the runtime's DOM.

Serialize the entire DOM tree to an HTML string.

Extract text content from the first element matching a CSS selector.

Evaluate JavaScript code and return the result.

Evaluate TypeScript code by transforming it to JavaScript first.

Get the value of a JS global. Works like eval(rt, "name") but safer — the name is accessed as a property, not evaluated as code.

List global names defined in the JS context.

Return runtime diagnostics: registered handlers, memory stats, and JS global count.

Load a native addon (.node file) via N-API.

Execute precompiled bytecode from compile/2.

Load an ES module into the runtime.

Return QuickJS memory usage statistics.

Reset the runtime to a fresh JS context. Clears all state and functions.

Send a message to the runtime's JS handler.

Set a JS global variable from Elixir.

Start a new JavaScript runtime.

Stop a runtime and free its resources.

Types

js_result()

@type js_result() :: {:ok, term()} | {:error, QuickBEAM.JSError.t()}

runtime()

@type runtime() :: GenServer.server()

Functions

call(runtime, fn_name, args \\ [], opts \\ [])

@spec call(runtime(), String.t(), list(), keyword()) :: js_result()

Call a global JavaScript function by name.

Arguments are converted to JS values; the return value is converted back. Promise-returning functions are automatically awaited.

iex> {:ok, rt} = QuickBEAM.start()
iex> QuickBEAM.eval(rt, "function add(a, b) { return a + b }")
iex> QuickBEAM.call(rt, "add", [2, 3])
{:ok, 5}
iex> QuickBEAM.stop(rt)
:ok

Options

  • :timeout — maximum execution time in milliseconds (default: no limit)

compile(runtime, code)

@spec compile(runtime(), String.t()) ::
  {:ok, binary()} | {:error, QuickBEAM.JSError.t()}

Compile JavaScript source to bytecode without executing it.

Returns {:ok, bytecode} where bytecode is a binary that can be loaded into any runtime with load_bytecode/2. Useful for precompilation, caching, and transferring compiled code between runtimes or nodes.

coverage(runtime)

@spec coverage(runtime()) :: {:ok, map()} | {:error, term()}

Get current JS coverage data for a runtime.

Returns {:ok, %{filename => %{line => hit_count}}}. Coverage must be enabled via QuickBEAM.Cover.

disasm(bytecode)

@spec disasm(binary()) :: {:ok, QuickBEAM.Bytecode.t()} | {:error, String.t()}

Disassemble precompiled bytecode into a %QuickBEAM.Bytecode{} struct.

Does not require a running runtime — creates a temporary QuickJS context internally to parse the binary format.

{:ok, bytecode} = QuickBEAM.compile(rt, "function add(a, b) { return a + b }")
{:ok, %QuickBEAM.Bytecode{}} = QuickBEAM.disasm(bytecode)

Also accepts JavaScript source code and a runtime, compiling it first:

{:ok, %QuickBEAM.Bytecode{}} = QuickBEAM.disasm(rt, "function add(a, b) { return a + b }")

disasm(runtime, code)

@spec disasm(runtime(), String.t()) ::
  {:ok, QuickBEAM.Bytecode.t()} | {:error, term()}

Compile JavaScript source and disassemble the resulting bytecode.

{:ok, %QuickBEAM.Bytecode{cpool: [%QuickBEAM.Bytecode{name: "add"}]}} =
  QuickBEAM.disasm(rt, "function add(a, b) { return a + b }")

dom_attr(runtime, selector, attr_name)

@spec dom_attr(runtime(), String.t(), String.t()) :: {:ok, String.t() | nil}

Get an attribute value from the first element matching a CSS selector.

Returns nil if the element or attribute is not found.

{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, ~s[document.body.innerHTML = '<a href="/page">link</a>'])
{:ok, "/page"} = QuickBEAM.dom_attr(rt, "a", "href")

dom_find(runtime, selector)

@spec dom_find(runtime(), String.t()) :: {:ok, tuple() | nil}

Find the first element matching a CSS selector in the runtime's DOM.

Returns the element as a Floki-compatible {tag, attrs, children} tuple, or nil if no match is found. This reads the live DOM tree directly from the native layer — no JS execution or HTML re-parsing.

{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, "document.body.innerHTML = '<p class="intro">Hello</p>'")
{:ok, {"p", [{"class", "intro"}], ["Hello"]}} = QuickBEAM.dom_find(rt, "p.intro")

dom_find_all(runtime, selector)

@spec dom_find_all(runtime(), String.t()) :: {:ok, list()}

Find all elements matching a CSS selector in the runtime's DOM.

Returns a list of Floki-compatible {tag, attrs, children} tuples.

{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, ~s[document.body.innerHTML = '<ul><li>a</li><li>b</li></ul>'])
{:ok, items} = QuickBEAM.dom_find_all(rt, "li")
length(items) # => 2

dom_html(runtime)

@spec dom_html(runtime()) :: {:ok, String.t()}

Serialize the entire DOM tree to an HTML string.

{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, "document.body.innerHTML = '<p>Hello</p>'")
{:ok, html} = QuickBEAM.dom_html(rt)

dom_text(runtime, selector)

@spec dom_text(runtime(), String.t()) :: {:ok, String.t()}

Extract text content from the first element matching a CSS selector.

{:ok, rt} = QuickBEAM.start()
QuickBEAM.eval(rt, "document.body.innerHTML = '<h1>Title</h1>'")
{:ok, "Title"} = QuickBEAM.dom_text(rt, "h1")

eval(runtime, code, opts \\ [])

@spec eval(runtime(), String.t(), keyword()) :: js_result()

Evaluate JavaScript code and return the result.

Top-level await is supported.

iex> {:ok, rt} = QuickBEAM.start()
iex> QuickBEAM.eval(rt, "40 + 2")
{:ok, 42}
iex> QuickBEAM.eval(rt, "await Promise.all([1, 2].map(x => Promise.resolve(x)))")
{:ok, [1, 2]}
iex> QuickBEAM.stop(rt)
:ok

Options

  • :timeout — maximum execution time in milliseconds (default: no limit). If exceeded, the JS execution is interrupted and an error is returned. The runtime remains usable after a timeout.

    QuickBEAM.eval(rt, "while(true) {}", timeout: 1000)
    # => {:error, %QuickBEAM.JSError{message: "interrupted", ...}}
  • :vars — a map of variable names to values, available in the code as globals. Values are converted using the standard BEAM→JS conversion. Variables are automatically cleaned up after evaluation, even if the code throws an error.

    QuickBEAM.eval(rt, "name.toUpperCase()", vars: %{"name" => "quickbeam"})
    # => {:ok, "QUICKBEAM"}
    
    QuickBEAM.eval(rt, "items.map(i => i.price * i.qty).reduce((a, b) => a + b, 0)",
      vars: %{"items" => [%{"price" => 10, "qty" => 3}, %{"price" => 5, "qty" => 2}]})
    # => {:ok, 40}

eval_ts(runtime, ts_code, opts \\ [])

@spec eval_ts(runtime(), String.t(), keyword()) :: js_result()

Evaluate TypeScript code by transforming it to JavaScript first.

Equivalent to OXC.transform!/2 followed by eval/3, but in a single call.

iex> {:ok, rt} = QuickBEAM.start()
iex> QuickBEAM.eval_ts(rt, "const x: number = 40 + 2; x")
{:ok, 42}
iex> QuickBEAM.stop(rt)
:ok

Options

Accepts the same options as eval/3 (e.g., :timeout).

get_global(runtime, name)

@spec get_global(runtime(), String.t()) :: js_result()

Get the value of a JS global. Works like eval(rt, "name") but safer — the name is accessed as a property, not evaluated as code.

Returns the value converted to Elixir terms. For objects, returns a map of enumerable own properties. For functions, returns a map with metadata.

Examples

QuickBEAM.get_global(rt, "myVar")
{:ok, 42}

QuickBEAM.get_global(rt, "myObj")
{:ok, %{"x" => 1, "y" => 2}}

QuickBEAM.get_global(rt, "nonexistent")
{:ok, nil}

globals(runtime, opts \\ [])

@spec globals(
  runtime(),
  keyword()
) :: {:ok, [String.t()]} | {:error, QuickBEAM.JSError.t()}

List global names defined in the JS context.

By default returns all globalThis property names. Pass user_only: true to exclude JS builtins and QuickBEAM internals — only names defined by your scripts.

Examples

{:ok, all} = QuickBEAM.globals(rt)
# ["Array", "Boolean", "Buffer", "Object", "console", "myVar", ...]

{:ok, mine} = QuickBEAM.globals(rt, user_only: true)
# ["myVar", "myFunc"]

info(runtime)

@spec info(runtime()) :: map()

Return runtime diagnostics: registered handlers, memory stats, and JS global count.

load_addon(runtime, path, opts \\ [])

@spec load_addon(runtime(), String.t(), keyword()) :: {:ok, term()} | {:error, term()}

Load a native addon (.node file) via N-API.

The addon is loaded with dlopen and its napi_register_module_v1 (or napi_module_register) entry point is called. Returns the addon's exports as an Elixir term.

Addon initialization is process-global native state. QuickBEAM initializes a canonical addon path once per BEAM instance. Loading that path again in the same runtime reuses its cached exports, so multiple global aliases are safe. Loading it in another runtime, or after reset/1, returns {:error, {:addon_already_initialized, canonical_path}} by default.

Options

  • :as - set the addon's exports as a global JS variable with this name, making the functions callable from eval/3 and call/3
  • :allow_reinitialization - opt into calling the native initializer again in a different JavaScript context. Defaults to false. Only enable this when the addon explicitly documents multi-environment initialization as safe; incompatible addons can terminate the BEAM VM.

Examples

QuickBEAM.load_addon(rt, "/path/to/addon.node")
QuickBEAM.load_addon(rt, "/path/to/crc32.node", as: "crc32")
QuickBEAM.eval(rt, "crc32.crc32('hello')")

load_bytecode(runtime, bytecode)

@spec load_bytecode(runtime(), binary()) :: js_result()

Execute precompiled bytecode from compile/2.

The bytecode runs in the current runtime's context, with access to all globals, handlers, and builtins.

load_module(runtime, name, code)

@spec load_module(runtime(), String.t(), String.t()) :: :ok | {:error, String.t()}

Load an ES module into the runtime.

iex> {:ok, rt} = QuickBEAM.start()
iex> code = "export function add(a, b) { return a + b; }"
iex> QuickBEAM.load_module(rt, "math", code)
:ok
iex> QuickBEAM.stop(rt)
:ok

memory_usage(runtime)

@spec memory_usage(runtime()) :: map()

Return QuickJS memory usage statistics.

reset(runtime)

@spec reset(runtime()) :: :ok | {:error, String.t()}

Reset the runtime to a fresh JS context. Clears all state and functions.

iex> {:ok, rt} = QuickBEAM.start()
iex> QuickBEAM.eval(rt, "globalThis.x = 42")
iex> QuickBEAM.reset(rt)
:ok
iex> QuickBEAM.eval(rt, "typeof x")
{:ok, "undefined"}
iex> QuickBEAM.stop(rt)
:ok

send_message(runtime, message)

@spec send_message(runtime(), term()) :: :ok

Send a message to the runtime's JS handler.

The message is delivered to the callback registered via Beam.onMessage in JS. If no handler is registered, the message is silently discarded.

set_global(runtime, name, value)

@spec set_global(runtime(), String.t(), term()) :: :ok

Set a JS global variable from Elixir.

The value is converted using the standard BEAM→JS conversion (no JSON).

Examples

QuickBEAM.set_global(rt, "config", %{"theme" => "dark", "limit" => 100})
{:ok, "dark"} = QuickBEAM.eval(rt, "config.theme")

QuickBEAM.set_global(rt, "items", [1, 2, 3])
{:ok, 3} = QuickBEAM.eval(rt, "items.length")

start(opts \\ [])

@spec start(keyword()) :: GenServer.on_start()

Start a new JavaScript runtime.

Returns {:ok, pid} on success.

Options

  • :name — register the GenServer under this name

  • :handlers%{String.t() => function} map for Beam.call/Beam.callSync

  • :script — path to a JS/TS file to evaluate on startup (auto-bundles imports)

  • :apis — which API surfaces to load (default: [:browser])

    • :browser — Web APIs (fetch, DOM, WebSocket, crypto, streams, …)
    • :node — Node.js compat (process, path, fs, os)
    • [:browser, :node] — both
    • false — bare QuickJS engine, no polyfills
  • :define%{String.t() => term()} of globals to inject before the script runs. Values are JSON-encoded. Useful for passing config without Beam.callSync.

    QuickBEAM.start(script: "build.ts", define: %{"outputDir" => "/tmp/site"})
  • :memory_limit — maximum JS heap in bytes (default: 256 MB)

  • :max_stack_size — maximum JS call stack in bytes (default: 8 MB)

  • :wasm_stack_size — WASM operand stack in bytes for guests started via the JS WebAssembly.instantiate path (default: 65536). Distinct from :max_stack_size (the JS call stack); raise it for guests whose deep init overflows the 64 KB default.

  • :wasm_heap_size — WASM auxiliary heap in bytes for the same path (default: 65536)

  • :max_convert_depth — maximum nesting depth for JS→BEAM value conversion (default: 32)

  • :max_convert_nodes — maximum total nodes for JS→BEAM value conversion (default: 10,000)

stop(runtime)

@spec stop(runtime()) :: :ok

Stop a runtime and free its resources.