Boam
Boam is an Elixir wrapper around the Boa JavaScript engine, implemented as a Rustler NIF.
The library starts a dedicated JavaScript runtime per Elixir process and lets JavaScript call back into the BEAM through an explicit dispatch bridge.
Features
- Boa-backed JavaScript evaluation from Elixir
- Dedicated runtime thread per engine, matching Boa's thread-safety model
beam.call(name, ...args)bridge from JavaScript into Elixir- JSON-compatible value round-tripping between JS and Elixir
- Configurable dispatcher process for custom routing and supervision setups
Installation
Add boam to your list of dependencies in mix.exs:
def deps do
[
{:boam, "~> 0.1.0"}
]
endQuick Start
{:ok, runtime} =
Boam.start_link(
exports: %{
sum: fn [left, right] -> left + right end,
greet: fn [name] -> "hello #{name}" end
}
)
Boam.eval(runtime, "1 + 2")
#=> {:ok, 3}
Boam.eval(runtime, "beam.call('sum', 2, 3)")
#=> {:ok, 5}
Boam.eval(runtime, "beam.call('greet', 'Ada')")
#=> {:ok, "hello Ada"}Startup Prelude
Run JavaScript during runtime startup with prelude::
{:ok, runtime} =
Boam.start_link(
prelude: [
"globalThis.appName = 'boam';",
"globalThis.version = 1;"
]
)
Those snippets run before your first call to Boam.eval/2.
Automatic Function Exposure
If you want JavaScript functions like console.log(...) without writing wrapper
code by hand, use expose::
{:ok, runtime} =
Boam.start_link(
expose: %{
console: %{
log: fn [message] -> "logged: #{message}" end,
warn: {:dispatch, "logger.warn", fn [message] -> "warn: #{message}" end}
}
}
)
Boam.eval(runtime, "console.log('hello')")
#=> {:ok, "logged: hello"}
Leaf dispatch names default to the dot-joined path, so console.log dispatches
to "console.log" unless you override it with
{:dispatch, "custom.name", handler}.
Manual Shim Generation
If you want to keep dispatch setup separate from runtime startup, generate the shim code yourself:
prelude =
Boam.JS.export_prelude(%{
console: %{
log: true,
error: {:dispatch, "logger.error"}
}
})
{:ok, runtime} =
Boam.start_link(
prelude: prelude,
fallback: fn name, args -> {name, args} end
)Value Model
Boam intentionally restricts the bridge to JSON-compatible values:
-
JavaScript
nullmaps to Elixirnil - booleans, numbers, strings, arrays, and objects round-trip normally
-
top-level JavaScript
undefinedbecomes{:ok, :undefined} undefinedis rejected when passed throughbeam.call(...)- JavaScript object keys come back as strings
For predictable round-tripping, return Elixir maps with string keys from dispatch handlers.
Dispatching Into Elixir
JavaScript code can call:
beam.call("name", arg1, arg2)
That request is delivered to a Boam.Dispatcher process on the BEAM side. A
handler can return:
- any JSON-compatible value
{:ok, value}{:error, reason}
If a handler crashes, the JavaScript caller receives an error instead of hanging forever.
Architecture
Boamis the small public entrypointBoam.Runtimeowns the NIF resource and runtime lifecycleBoam.Dispatcherresolves and executesbeam.call(...)handlersBoam.JSgenerates JavaScript shim preludes from nested export trees- the internal NIF bridge module is intentionally hidden from the generated docs
Generating Docs
Generate the docs locally with:
mix docs
The generated site will be written to doc/.