Skip to content

FEATURE: add Context#call_async and Context#eval_async - #441

Open
davidtaylorhq wants to merge 2 commits into
mainfrom
async-support
Open

FEATURE: add Context#call_async and Context#eval_async#441
davidtaylorhq wants to merge 2 commits into
mainfrom
async-support

Conversation

@davidtaylorhq

Copy link
Copy Markdown
Collaborator

These work like call and eval, except that when the result is a promise they block until it settles and return the settled value. A rejected promise raises MiniRacer::RuntimeError, like a synchronous throw, and non-promise results are returned as-is.

While waiting, the V8 thread alternates between draining the microtask queue and pumping the platform message loop in wait-for-work mode, so there is no polling: microtask chains settle immediately, and delayed or background work (Atomics.waitAsync timers, async wasm compilation) wakes the loop when its tasks are posted.

TerminateExecution doesn't wake a parked message loop, and when called while no JS is running it only queues a termination for the next JS entry. v8_terminate_execution therefore also sets a flag on the State and posts a no-op wakeup task, so the timeout watchdog, Context#stop and Ruby thread interrupts can all end a pending await. A promise that can never settle blocks like an infinite loop until one of those stops it.

The new methods use two new request opcodes ('D' and 'F') sharing the existing v8_call/v8_eval implementations. Ruby callbacks invoked while waiting go through the usual nested-dispatch path, and exceptions they raise propagate out through the promise rejection. TruffleRuby raises MiniRacer::Error.

Base automatically changed from fix-roundtrip-request-drop to main August 11, 2026 07:00
These work like call and eval, except that when the result is a promise they block until it settles and return the settled value. A rejected promise raises MiniRacer::RuntimeError, like a synchronous throw, and non-promise results are returned as-is.

While waiting, the V8 thread alternates between draining the microtask queue and pumping the platform message loop in wait-for-work mode, so there is no polling: microtask chains settle immediately, and delayed or background work (Atomics.waitAsync timers, async wasm compilation) wakes the loop when its tasks are posted.

TerminateExecution doesn't wake a parked message loop, and when called while no JS is running it only queues a termination for the next JS entry. v8_terminate_execution therefore also sets a flag on the State and posts a no-op wakeup task, so the timeout watchdog, Context#stop and Ruby thread interrupts can all end a pending await. A promise that can never settle blocks like an infinite loop until one of those stops it.

The new methods use two new request opcodes ('D' and 'F') sharing the existing v8_call/v8_eval implementations. Ruby callbacks invoked while waiting go through the usual nested-dispatch path, and exceptions they raise propagate out through the promise rejection. TruffleRuby raises MiniRacer::Error.
@SamSaffron

Copy link
Copy Markdown
Collaborator

give me a tiny bit on this one, I want to test carefully

@SamSaffron

Copy link
Copy Markdown
Collaborator

name of api is a bit confusing:

call_await or call_and_await ... async leave the taste that we will be returning a promise of sorts that is not the case here.

Raise MiniRacer::RuntimeError when call_async or eval_async is invoked recursively from an attached Ruby callback. V8 cannot run nested microtask checkpoints, so these calls would otherwise deadlock.

Document the limitation and cover both async entry points while ensuring the context remains usable.
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.

2 participants