Skip to content

feat: support asynchronous Elixir callbacks from JavaScript - #5

Merged
pepicrft merged 1 commit into
mainfrom
feat/async-host-callbacks
Oct 19, 2026
Merged

pepicrft merged 1 commit into
mainfrom
feat/async-host-callbacks

Conversation

@pepicrft

Copy link
Copy Markdown
Contributor

Behavior

Javex.run/3 accepts per-run named callbacks. JavaScript calls await Javex.Host.call(name, json); top-level await, sequential calls and genuinely overlapping Promise.all work with both in-process and Linux-isolated runtimes, in static and dynamic modes.

A resumable custom Javy provider releases the Wasm/NIF stack between continuations. Monitored Elixir callback processes receive decoded JSON and an absolute monotonic deadline. Grants, call/concurrency budgets and JSON payload limits are independently validated by the host. Execution completion, callback/overall deadlines, caller death and runtime shutdown cancel owned callback work. Isolated workers use bounded bidirectional IPC without relaxing their OS restrictions.

HTTP stays in application callbacks. The README demonstrates a fixed-destination Finch capability with host-only credentials, streaming response-size checks and propagated deadlines. This does not install browser fetch, timers, streaming responses or per-call AbortSignal.

Adversarial review and fixes

  • Guest prototype mutation: reproduced Object.prototype.toJSON suppressing a terminal quota failure and Map/Array prototype mutation corrupting the private bridge. Capture private intrinsics and serialize protocol records from primitives, without invoking guest toJSON hooks. Regression scenarios pass on both backends, including escaped UTF-8 names and payloads.
  • Independent diagnostic limits: reproduced a 9,000-character JS exception becoming a callback-frame trap when callback input/concurrency budgets were small. Bound diagnostic records only by max_stderr; regressions preserve the full JS error on both backends.
  • Also exercise forged Wasm states bypassing guest checks, duplicate IDs, partial-batch cleanup, catch-resistant quotas, reentrant argument serialization, sanitized callback crashes, denied grants, out-of-order replies and cancellation/admission cleanup.
  • The fresh full run exposed a cold-JIT timing assumption in a cancellation test. Give startup room without changing production deadlines or removing the timeout/callback-termination assertions.

Verification

  • JAVEX_BUILD=1 CARGO_BUILD_JOBS=1 mise exec -- mix test --cover: 62 tests, 0 failures; 91.17% coverage.
  • Release Rust contract tests: 5 passed (including isolated OS-boundary enforcement).
  • Strict Credo; Rust formatting and Clippy for both crates; Elixir formatting and compile warnings checks passed.
  • Documentation and Hex package builds passed; package contains provider source/lock/build instructions, not build targets.
  • Provider rebuilt byte-identically, including after relocating sources. CI rebuilds and compares bundled bytes and audits the provider lockfile.
  • Disposable real HTTP validation observed both requests before either response on both backends, with host-only credentials and correctly correlated JSON responses.

Compatibility and limits

Dynamic modules compiled against the previous bundled provider must be recompiled. Custom legacy providers retain synchronous execution. The custom provider is built from pinned Javy 7.0.0 (Javy CLI 8.1.1), not the upstream default binary.

Callbacks are trusted host code outside guest memory/OS limits; applications must bound external I/O and cancel separately owned work. Cancellation cannot undo completed external effects. Snapshot-seeded Math.random may repeat and is not cryptographically secure. Local execution was verified on Linux x86_64; other supported architectures remain covered by release packaging tests, not this orb.

Drive a resumable Javy provider between Promise continuations so host I/O never holds a dirty NIF scheduler. Grant named JSON callbacks per run, own and cancel callback processes with execution, and enforce budgets independently on the host for both backends.

Harden private bridge serialization against guest prototype mutations and keep JavaScript diagnostic budgets independent of callback frames. Include public contract regressions, provider source and reproducible builds, and a bounded HTTP capability example.

Amp-Thread-ID: https://ampcode.com/threads/T-01a1507b-cf78-7207-87c1-60a651b36fa4
Co-authored-by: Pedro Piñera Buendía <pedro@pepicrft.me>
Comment thread .github/workflows/ci.yml
runs-on: tuist-linux
steps:
- uses: actions/checkout@v4
- uses: jdx/mise-action@v2
@pepicrft
pepicrft merged commit 95d0ee3 into main Oct 19, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants