ExSafejs (ex_safejs v0.3.1)

Copy Markdown View Source

Sandboxed JavaScript execution via QuickJS-NG.

eval/2 and eval/3 are async-aware: if the completion value of the guest code is a promise (an async arrow, a .then chain, ...), the runtime drives the job queue until it settles and returns the settled value — a rejection comes back as a structured error. Synchronous code returns directly, as before.

Each runtime runs on a dedicated OS thread with a hard memory cap, a stack limit, and a JS compute deadline. The deadline covers JS execution only: time spent inside Elixir callbacks is excluded, so a slow host call never reads as guest misbehavior.

Failures are {:error, %ExSafejs.Error{}} with a :kind callers can branch on — see ExSafejs.Error.

Elixir callbacks are installed as real JS functions whose dispatch path is captured host-side; guest code may shadow or delete the global binding but that only loses its own access. There are no reserved __* globals.

Summary

Functions

Check if a runtime is alive.

Evaluate JavaScript code and return the result.

Evaluate JavaScript code with pre-registered Elixir callbacks.

Start a new JavaScript runtime on a dedicated OS thread.

Stop a runtime. Idempotent — safe to call multiple times.

Types

js_result()

@type js_result() :: {:ok, term()} | {:error, ExSafejs.Error.t()}

Functions

alive?(runtime)

@spec alive?(runtime()) :: boolean()

Check if a runtime is alive.

eval(runtime, code)

@spec eval(runtime(), String.t()) :: js_result()

Evaluate JavaScript code and return the result.

{:ok, 3} = ExSafejs.eval(rt, "1 + 2")
{:ok, 3} = ExSafejs.eval(rt, "(async () => 1 + 2)()")

eval(runtime, code, callbacks)

@spec eval(runtime(), String.t(), map()) :: js_result()

Evaluate JavaScript code with pre-registered Elixir callbacks.

Each callback receives its arguments as a list and must return {:ok, value} or {:error, reason} with a binary reason. A returned {:error, reason} becomes a catchable JS exception carrying reason verbatim — never put a secret in it. A raised exception instead surfaces to the guest as a generic "host function failed" exception; if the guest doesn't catch it, the eval returns a :host_error carrying the real exception message to the Elixir caller.

callbacks = %{"add" => fn [a, b] -> {:ok, a + b} end}
{:ok, 5} = ExSafejs.eval(rt, "add(2, 3)", callbacks)

Callbacks work under await too — a blocking host call is simply a value by the time the guest sees it:

{:ok, 5} = ExSafejs.eval(rt, "(async () => await add(2, 3))()", callbacks)

start(opts \\ [])

@spec start(keyword()) :: {:ok, runtime()} | {:error, term()}

Start a new JavaScript runtime on a dedicated OS thread.

Options

  • :timeout — max JS compute time per eval in milliseconds, excluding time spent in Elixir callbacks (default 30_000)
  • :memory_limit — max JS heap in bytes (default 268_435_456)
  • :max_stack_size — max JS stack in bytes (default 1_048_576)
  • :gc_threshold — GC trigger threshold in bytes (default 4_194_304)

stop(runtime)

@spec stop(runtime()) :: :ok

Stop a runtime. Idempotent — safe to call multiple times.