In-process executor on ex_safejs,
the QuickJS-NG engine embedded as a Rustler NIF (our hard fork of
lpgauth/quicksand). Like Executor.ZapCode it's a pure dependency with
precompiled binaries — no runtime binary in the image, no subprocess — but
unlike zapcode it enforces a genuine hard memory cap (QuickJS's own
allocator is the sole memory authority, so even typed-array/ArrayBuffer
allocations are bounded, the vector that escapes V8's heap limit) and runs
a mature, correct engine with O(1) container access (no O(n²) spec scans).
Dialect: same async arrow as Executor.Deno
Since ex_safejs 0.3.0 eval is async-aware, so guest code may be a sync or
an async arrow — await, .then chains, and Promise.all all work.
apis.<name>.request(...) blocks the JS thread while the Elixir callback
runs and returns the response as a plain value, which await passes
through unchanged; Promise.all over several requests therefore executes
them serially, not concurrently. A promise that nothing can ever settle
is reported as a deadlock error immediately instead of burning the
timeout.
Timeout is compute-only — wall clock is a separate opt
:timeout is a JS compute budget: host-callback time does not count
against it. For a single slow call that's a feature (a slow host call
never reads as guest misbehavior), but it means a guest looping over
cheap request() calls has unbounded wall-clock time — the engine
can never end it (ele P1). Hosts that meter runs (semaphores, billing,
request deadlines) must pass :wall_clock_ms: the eval then runs in a
throwaway worker killed at that deadline, returning
{:error, {:wall_clock, ms}} (logs captured before the kill are lost).
OapiCodemode.Tools' :max_calls bounds the same class at the tool
layer for every executor. Executor.Deno's timeout, by contrast, is a
wall-clock deadline that includes callback time.
Injection differs from Deno: ex_safejs has no globals API, so
env.globals is handed in through a host callback that returns the map
(direct term→JS conversion — faster than embedding JSON) and
Object.assigned onto globalThis in a preamble. The apis object and a
console.log capture shim are built in the same preamble.
Isolation
ex_safejs guarantees a straggler-free mailbox: eval messages are tagged
per-eval and a timed-out eval's late result is absorbed inside eval/3,
so run/3 executes directly in the calling process — safe for a
trap_exit GenServer caller (gentility's LoopServer). The quicksand-era
throwaway worker per eval is gone.
Divergences from Executor.Deno, inherent to the engine
- Serial
Promise.all(above): requests resolve one at a time. - Raising callbacks are opaque to the guest AND the caller. A
callback that raises surfaces to JS as a generic
"host function failed"exception; if uncaught, this run's{:error, %{message: ..., logs: ...}}carries a fixed redacted message — the real exception text goes toLoggeronly (C1: exception messages can embed paths, query fragments, credentials).
Regex works — global match, named groups, lookaheads (QuickJS-NG is a complete engine). The old "no regex" note was a zapcode/quicksand-era belief, disproven live 2026-08-20.