Current section
Files
Jump to
Current section
Files
lib/boam.ex
defmodule Boam do
@moduledoc """
High-level entrypoint for running JavaScript through Boa from Elixir.
`Boam` wraps a dedicated Boa runtime behind a `GenServer`. Each runtime owns a
single Rust worker thread because Boa `Context` values are not thread-safe.
JavaScript code can call back into Elixir with `beam.call(name, ...args)`. The
dispatch itself happens through a separate BEAM process, which keeps the
runtime process focused on driving the JavaScript engine.
You can either:
* pass your own dispatcher with `:dispatcher`
* install JavaScript shims with `:js_expose`
* register Elixir handlers with `:exports` when Boam manages the dispatcher
* use legacy combined config with `:expose`
* run startup JavaScript with `:prelude`
* generate shim code manually with `Boam.JS.export_prelude/1`
## 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}}
)
Boam.eval(runtime, "1 + 2")
#=> {:ok, 3}
Boam.eval(runtime, "math.sum(2, 3)")
#=> {:ok, 5}
```
The built-in dispatcher is available for simpler cases:
```elixir
{:ok, runtime} =
Boam.start_link(
exports: %{
"console.log" => fn [message] -> "logged: \#{message}" end
},
js_expose: %{
console: %{
log: true
}
}
)
Boam.eval(runtime, "console.log('hello')")
#=> {:ok, "logged: hello"}
```
## Value Bridge
Values crossing between JavaScript and Elixir are intentionally restricted to
JSON-compatible data:
* `null` maps to `nil`
* booleans, numbers, strings, arrays, and objects round-trip normally
* top-level JavaScript `undefined` is returned as `:undefined`
* `undefined` cannot be passed through `beam.call/1`
Returned JavaScript objects are serialized through Boa's JSON conversion, so
object keys come back to Elixir as strings.
## Architecture
* [`Boam.Runtime`](`Boam.Runtime`) owns the Rust runtime resource
* [`Boam.Dispatcher`](`Boam.Dispatcher`) receives `beam.call(...)` requests
* [`Boam.JS`](`Boam.JS`) generates JavaScript shim preludes
* `Boam` provides the small public entrypoint for the common case
"""
alias Boam.Runtime
@type start_option ::
{:name, GenServer.name()}
| {:dispatcher, GenServer.server()}
| {:exports, map() | keyword()}
| {:js_expose, Boam.JS.export_tree()}
| {:expose, map() | keyword()}
| {:prelude, String.t() | [String.t()]}
| {:fallback, (String.t(), list() -> term())}
| {:dispatch_timeout, timeout()}
@doc """
Starts a Boa runtime process.
This is a convenience wrapper around `#{inspect(Runtime)}.start_link/1`.
See `#{inspect(Runtime)}` for the full option contract.
"""
@spec start_link([start_option()]) :: GenServer.on_start()
def start_link(opts \\ []) do
Runtime.start_link(opts)
end
@doc """
Evaluates JavaScript source code inside a running runtime.
This is a convenience wrapper around `#{inspect(Runtime)}.eval/2`.
"""
@spec eval(GenServer.server(), String.t()) :: {:ok, term()} | {:error, String.t()}
def eval(runtime, source) do
Runtime.eval(runtime, source)
end
@doc """
Returns the dispatcher pid attached to a runtime.
"""
@spec dispatcher(GenServer.server()) :: pid()
def dispatcher(runtime) do
Runtime.dispatcher(runtime)
end
@doc """
Installs JavaScript shim functions on `globalThis`.
"""
@spec expose_js(GenServer.server(), Boam.JS.export_tree()) :: :ok | {:error, String.t()}
def expose_js(runtime, tree) do
Runtime.expose_js(runtime, tree)
end
@doc """
Queues JavaScript shim installation through an asynchronous message.
"""
@spec notify_expose_js(GenServer.server(), Boam.JS.export_tree()) :: :ok
def notify_expose_js(runtime, tree) do
Runtime.notify_expose_js(runtime, tree)
end
end