Boam.Runtime (boam v0.1.4)

Copy Markdown

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:

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:

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:

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:

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}.

Summary

Functions

Returns a specification to start this module under a supervisor.

Returns the dispatcher pid currently attached to the runtime.

Evaluates a JavaScript string inside the runtime.

Installs JavaScript shim functions on globalThis.

Queues JavaScript shim exposure through an asynchronous message.

Starts a runtime process.

Types

dispatch_timeout()

@type dispatch_timeout() :: timeout()

export_name()

@type export_name() :: atom() | String.t()

legacy_expose()

@type legacy_expose() :: map() | keyword()

prelude()

@type prelude() :: String.t() | [String.t()]

start_option()

@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()}

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

dispatcher(runtime)

@spec dispatcher(GenServer.server()) :: pid()

Returns the dispatcher pid currently attached to the runtime.

eval(runtime, source)

@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

{: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(runtime, tree)

@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(runtime, tree)

@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(opts \\ [])

@spec start_link([start_option()]) :: GenServer.on_start()

Starts a runtime process.

See the module documentation for the supported options and dispatch contract.