Boam
Copy MarkdownBoam 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, _dispatcher} =
MyApp.JsDispatcher.start_link(name: MyApp.JsDispatcher)
{:ok, runtime} =
Boam.start_link(
dispatcher: MyApp.JsDispatcher,
js_expose: %{math: %{sum: true, greet: {:dispatch, "app.greet"}}}
)
Boam.eval(runtime, "1 + 2")
#=> {:ok, 3}
Boam.eval(runtime, "math.sum(2, 3)")
#=> {:ok, 5}
Boam.eval(runtime, "math.greet('Ada')")
#=> {:ok, "hello Ada"}In most applications, managing your own dispatcher is the intended setup.
dispatcher: accepts any GenServer.server/0, so a named sibling child works
cleanly under supervision.
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.
Managed Dispatcher
If you want to own the dispatch process yourself, pass it directly:
children = [
{MyApp.JsDispatcher, name: MyApp.JsDispatcher},
{Boam.Runtime,
dispatcher: MyApp.JsDispatcher,
js_expose: %{console: %{log: true}}}
]If the dispatcher is a sibling child, prefer a supervisor strategy like
:rest_for_one so the runtime restarts after the dispatcher restarts. Boam
resolves the dispatcher to a pid during startup.
Convenience Dispatcher
If you want JavaScript functions like console.log(...) without writing a
dispatcher process, Boam can start a small convenience dispatcher from
:exports and :js_expose:
{:ok, runtime} =
Boam.start_link(
exports: %{
"console.log" => fn [message] -> "logged: #{message}" end,
"logger.warn" => fn [message] -> "warn: #{message}" end
},
js_expose: %{
console: %{
log: true,
warn: {:dispatch, "logger.warn"}
}
}
)
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"}.
expose: still exists as legacy sugar when you want to configure both the JS
shim and the Elixir handler in one tree.
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
)Runtime JS Exposure
You can add JS functions after startup with either a synchronous call or an asynchronous message:
:ok = Boam.expose_js(runtime, %{console: %{log: true}})
send(runtime, {:boam_expose_js, %{console: %{debug: true}}})The message form is the one to use from a beam.call(...) handler, because the
runtime only applies it after the current evaluation unwinds.
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/.