Packages

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

Current section

Files

Jump to
boam doc Boam.Runtime.md
Raw

doc/Boam.Runtime.md

# `Boam.Runtime`
`GenServer` wrapper around a single Boa runtime.
Each `Boam.Runtime` process owns one Rust resource, and that
resource owns one dedicated worker thread where the Boa engine actually runs.
This keeps the JavaScript engine isolated from BEAM scheduler threads while
still allowing synchronous `eval/2` calls from Elixir.
## Options
* `:name` - optional `GenServer` name for the runtime process
* `:dispatcher` - existing dispatcher server to receive `beam.call(...)`
messages; accepts any `GenServer.server/0`. This is the primary way to
integrate Boam into a supervised application.
* `:exports` - map or keyword list of dispatch handlers when Boam starts
its own convenience dispatcher
* `:js_expose` - nested tree of JavaScript shims to install on `globalThis`
during startup
* `:expose` - legacy combined tree that configures both `:exports` and
`:js_expose`
* `:prelude` - JavaScript source string or list of strings to evaluate
during startup, after any generated exposure shims
* `:fallback` - optional `(name, args -> result)` function used when a
dispatch name is not present in `:exports`
* `:dispatch_timeout` - timeout for a single `beam.call(...)` round-trip;
defaults to `30_000`, accepts `:infinity`
## Dispatch Contract
JavaScript code may call:
```javascript
beam.call("name", arg1, arg2)
```
The dispatcher receives the function name and arguments as JSON-compatible
Elixir values and may reply with:
* any JSON-compatible value
* `{:ok, value}`
* `{:error, reason}`
Returning `{:error, reason}` raises a JavaScript error with `reason` as the
message.
## Exposing Functions
In most applications you will manage your own dispatch process and pass it in
through `:dispatcher`:
```elixir
children = [
{MyApp.JsDispatcher, name: MyApp.JsDispatcher},
{Boam.Runtime,
dispatcher: MyApp.JsDispatcher,
js_expose: %{console: %{log: true}}}
]
```
The built-in dispatcher exists as convenience sugar. Register Elixir handlers
with `:exports` and install JavaScript shims with `:js_expose`:
```elixir
exports: %{
"console.log" => fn [message] -> "logged: #{message}" end,
"logger.warn" => fn [message] -> "warn: #{message}" end
},
js_expose: %{
console: %{
log: true,
warn: {:dispatch, "logger.warn"}
}
}
```
This creates `console.log(...)` and `console.warn(...)` automatically.
Dispatch handlers can also queue JS exposure at runtime by sending:
```elixir
send(runtime, {:boam_expose_js, %{console: %{debug: true}}})
```
The message is applied after the current runtime operation completes, which
avoids deadlocking a `beam.call(...)` handler.
## Notes
* Avoid synchronously calling back into the same runtime from a dispatch
handler before replying, or the call will deadlock. Use
`notify_expose_js/2` or `send(runtime, {:boam_expose_js, tree})` from
dispatch handlers instead.
* If you pass a named sibling dispatcher, use a supervisor strategy that
restarts the runtime after the dispatcher restarts, because the runtime
resolves the dispatcher to a pid during startup.
* Top-level JavaScript `undefined` is surfaced as `{:ok, :undefined}`.
# `dispatch_timeout`
```elixir
@type dispatch_timeout() :: timeout()
```
# `export_name`
```elixir
@type export_name() :: atom() | String.t()
```
# `legacy_expose`
```elixir
@type legacy_expose() :: map() | keyword()
```
# `prelude`
```elixir
@type prelude() :: String.t() | [String.t()]
```
# `start_option`
```elixir
@type start_option() ::
{:name, GenServer.name()}
| {:dispatcher, GenServer.server()}
| {:exports,
%{optional(export_name()) => Boam.Dispatcher.handler()}
| keyword(Boam.Dispatcher.handler())}
| {:js_expose, Boam.JS.export_tree()}
| {:expose, legacy_expose()}
| {:prelude, prelude()}
| {:fallback, (String.t(), list() -> term())}
| {:dispatch_timeout, dispatch_timeout()}
```
# `child_spec`
Returns a specification to start this module under a supervisor.
See `Supervisor`.
# `dispatcher`
```elixir
@spec dispatcher(GenServer.server()) :: pid()
```
Returns the dispatcher pid currently attached to the runtime.
# `eval`
```elixir
@spec eval(GenServer.server(), String.t()) :: {:ok, term()} | {:error, String.t()}
```
Evaluates a JavaScript string inside the runtime.
The return value is either:
* `{:ok, value}` for a JSON-compatible JavaScript result
* `{:ok, :undefined}` for top-level JavaScript `undefined`
* `{:error, message}` if evaluation or dispatch fails
## Example
```elixir
{:ok, runtime} =
Boam.Runtime.start_link(
exports: %{
greet: fn [name] -> "hello #{name}" end
}
)
Boam.Runtime.eval(runtime, "beam.call('greet', 'Ada')")
#=> {:ok, "hello Ada"}
```
# `expose_js`
```elixir
@spec expose_js(GenServer.server(), Boam.JS.export_tree()) ::
:ok | {:error, String.t()}
```
Installs JavaScript shim functions on `globalThis`.
This call is synchronous and should be used only when the runtime is idle.
Do not call it from a `beam.call(...)` dispatch handler; use
`notify_expose_js/2` instead.
# `notify_expose_js`
```elixir
@spec notify_expose_js(GenServer.server(), Boam.JS.export_tree()) :: :ok
```
Queues JavaScript shim exposure through an asynchronous message.
This is safe to use from dispatch handlers because it does not wait for the
runtime process to finish its current evaluation.
# `start_link`
```elixir
@spec start_link([start_option()]) :: GenServer.on_start()
```
Starts a runtime process.
See the module documentation for the supported options and dispatch contract.
---
*Consult [api-reference.md](api-reference.md) for complete listing*