Packages

Boa-powered JavaScript runtimes for Elixir with BEAM dispatch hooks.

Current section

Files

Jump to
boam doc readme.md
Raw

doc/readme.md

# Boam
Boam is an Elixir wrapper around the [Boa](https://boajs.dev/) 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`:
```elixir
def deps do
[
{:boam, "~> 0.1.0"}
]
end
```
## Quick Start
```elixir
{: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:`:
```elixir
{: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:
```elixir
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`:
```elixir
{: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:
```elixir
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:
```elixir
: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 `null` maps to Elixir `nil`
- booleans, numbers, strings, arrays, and objects round-trip normally
- top-level JavaScript `undefined` becomes `{:ok, :undefined}`
- `undefined` is rejected when passed through `beam.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:
```javascript
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
- `Boam` is the small public entrypoint
- `Boam.Runtime` owns the NIF resource and runtime lifecycle
- `Boam.Dispatcher` resolves and executes `beam.call(...)` handlers
- `Boam.JS` generates 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:
```bash
mix docs
```
The generated site will be written to `doc/`.