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: * expose Elixir handlers directly as JavaScript functions with `:expose` * run startup JavaScript with `:prelude` * generate shim code manually with `Boam.JS.export_prelude/1` ## Quick Start ```elixir {:ok, runtime} = Boam.start_link( exports: %{ sum: fn [left, right] -> left + right end } ) Boam.eval(runtime, "1 + 2") #=> {:ok, 3} Boam.eval(runtime, "beam.call('sum', 2, 3)") #=> {:ok, 5} ``` Or expose a nested JavaScript API directly: ```elixir {:ok, runtime} = Boam.start_link( expose: %{ console: %{ log: fn [message] -> "logged: \#{message}" end } } ) 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, pid()} | {:exports, map() | keyword()} | {:expose, Boam.JS.export_tree()} | {: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 end