Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
- Unreleased
- Add `Context#call_await` and `Context#eval_await`: like `call`/`eval` but block until a returned Promise settles and return the settled value; rejections raise `MiniRacer::RuntimeError`
- Fix a race introduced in 0.21.4 where a request sent right after a nested dispatch (e.g. `perform_microtask_checkpoint` or a nested `call` from an attached callback) could be dropped, deadlocking the context

- 0.21.4 - 24-06-2026
Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,38 @@ Performance is slightly better than running `context.eval("hello('George')")` si
* compilation of eval'd string is avoided
* function arguments don't need to be converted to JSON

### Promises: call_await and eval_await

`call_await` and `eval_await` work like `call` and `eval`, but when the result is a
Promise they block until it settles and return the settled value. A rejected
promise raises `MiniRacer::RuntimeError`, just like a synchronous `throw`:

```ruby
context = MiniRacer::Context.new
context.eval("async function f(x) { await Promise.resolve(); return x * 2 }")
context.call_await("f", 21)
# => 42

context.eval_await("(async () => 6 * 7)()")
# => 42

context.eval("async function boom() { throw new Error('kaboom') }")
context.call_await("boom")
# => raises MiniRacer::RuntimeError (Error: kaboom)
```

Non-Promise results pass through unchanged, so `call_await` is a drop-in
superset of `call` (same for `eval_await`/`eval`).

A promise that never settles blocks forever, just like an infinite loop. The
`timeout:` option and `Context#stop` both interrupt it, raising
`MiniRacer::ScriptTerminatedError`.

Calling `call_await` or `eval_await` recursively on the same context from an
attached Ruby callback is not supported and raises `MiniRacer::RuntimeError`.
V8 cannot run the nested microtask checkpoint needed to settle such a call.
Synchronous nested `call` and `eval` remain supported.

### Microtask checkpoints

V8 drains its microtask queue (e.g. callbacks queued via `Promise.resolve().then(...)`) automatically when script execution returns to the embedder, so most code "just works":
Expand Down
56 changes: 45 additions & 11 deletions ext/mini_racer_extension/mini_racer_extension.c
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ typedef struct Context
struct {
pthread_mutex_t mtx;
pthread_cond_t cv;
int cancel;
int active; // dispatch thread only
int cancel; // protected by |mtx|
} wd; // watchdog
Barrier early_init, late_init;
} Context;
Expand Down Expand Up @@ -772,7 +773,7 @@ static void *v8_watchdog(void *arg)
if (c->wd.cancel)
break;
if (deadline_exceeded(deadline)) {
v8_terminate_execution(c->pst);
v8_terminate_watchdog(c->pst);
break;
}
}
Expand All @@ -786,8 +787,14 @@ static void v8_timedwait(Context *c, const uint8_t *p, size_t n,
pthread_t thr;
int r;

r = -1;
if (c->timeout > 0 && (r = pthread_create(&thr, NULL, v8_watchdog, c))) {
if (c->timeout <= 0 || c->wd.active) {
func(c->pst, p, n);
return;
}
c->wd.active = 1;
r = pthread_create(&thr, NULL, v8_watchdog, c);
if (r) {
c->wd.active = 0;
fprintf(stderr, "mini_racer: watchdog: pthread_create: %s\n", strerror(r));
fflush(stderr);
}
Expand All @@ -799,7 +806,9 @@ static void v8_timedwait(Context *c, const uint8_t *p, size_t n,
pthread_cond_signal(&c->wd.cv);
pthread_mutex_unlock(&c->wd.mtx);
pthread_join(thr, NULL);
v8_cancel_watchdog_termination(c->pst);
c->wd.cancel = 0;
c->wd.active = 0;
}

static void dispatch1(Context *c, const uint8_t *p, size_t n)
Expand All @@ -810,7 +819,9 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
switch (*p) {
case 'A': return v8_attach(c->pst, p+1, n-1);
case 'C': return v8_timedwait(c, p+1, n-1, v8_call);
case 'D': return v8_timedwait(c, p+1, n-1, v8_call_await);
case 'E': return v8_timedwait(c, p+1, n-1, v8_eval);
case 'F': return v8_timedwait(c, p+1, n-1, v8_eval_await);
case 'H': return v8_heap_snapshot(c->pst);
case 'M': return v8_perform_microtask_checkpoint(c->pst);
case 'P': return v8_pump_message_loop(c->pst);
Expand Down Expand Up @@ -888,7 +899,8 @@ void v8_dispatch(Context *c)
pthread_mutex_unlock(&c->mtx);
}

// only called when inside v8_call, v8_eval, or v8_pump_message_loop
// only called when inside v8_call, v8_eval (and their async variants),
// or v8_pump_message_loop
void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
{
pthread_mutex_lock(&c->mtx);
Expand Down Expand Up @@ -1654,7 +1666,7 @@ static VALUE context_stop(VALUE self)
return Qnil;
}

static VALUE context_call(int argc, VALUE *argv, VALUE self)
static VALUE context_call_common(int argc, VALUE *argv, VALUE self, char op)
{
VALUE name, args;
VALUE a, e;
Expand All @@ -1665,8 +1677,8 @@ static VALUE context_call(int argc, VALUE *argv, VALUE self)
rb_scan_args(argc, argv, "1*", &name, &args);
Check_Type(name, T_STRING);
rb_ary_unshift(args, name);
// request is (C)all, [name, args...] array
ser_init1(&s, 'C');
// request is (C)all or async (D) call, [name, args...] array
ser_init1(&s, op);
if (serialize(&s, args)) {
ser_reset(&s);
rb_raise(runtime_error, "Context.call: %s", s.err);
Expand All @@ -1678,7 +1690,17 @@ static VALUE context_call(int argc, VALUE *argv, VALUE self)
return rb_ary_pop(a);
}

static VALUE context_eval(int argc, VALUE *argv, VALUE self)
static VALUE context_call(int argc, VALUE *argv, VALUE self)
{
return context_call_common(argc, argv, self, 'C');
}

static VALUE context_call_await(int argc, VALUE *argv, VALUE self)
{
return context_call_common(argc, argv, self, 'D');
}

static VALUE context_eval_common(int argc, VALUE *argv, VALUE self, char op)
{
VALUE a, e, source, filename, kwargs;
Context *c;
Expand All @@ -1693,8 +1715,8 @@ static VALUE context_eval(int argc, VALUE *argv, VALUE self)
if (NIL_P(filename))
filename = rb_str_new_cstr("<eval>");
Check_Type(filename, T_STRING);
// request is (E)val, [filename, source] array
ser_init1(&s, 'E');
// request is (E)val or async (F) eval, [filename, source] array
ser_init1(&s, op);
ser_array_begin(&s, 2);
add_string(&s, filename);
add_string(&s, source);
Expand All @@ -1706,6 +1728,16 @@ static VALUE context_eval(int argc, VALUE *argv, VALUE self)
return rb_ary_pop(a);
}

static VALUE context_eval(int argc, VALUE *argv, VALUE self)
{
return context_eval_common(argc, argv, self, 'E');
}

static VALUE context_eval_await(int argc, VALUE *argv, VALUE self)
{
return context_eval_common(argc, argv, self, 'F');
}

static VALUE context_heap_stats(VALUE self)
{
VALUE a, h, k, v;
Expand Down Expand Up @@ -2146,7 +2178,9 @@ void Init_mini_racer_extension(void)
rb_define_method(c, "dispose", context_dispose, 0);
rb_define_method(c, "stop", context_stop, 0);
rb_define_method(c, "call", context_call, -1);
rb_define_method(c, "call_await", context_call_await, -1);
rb_define_method(c, "eval", context_eval, -1);
rb_define_method(c, "eval_await", context_eval_await, -1);
rb_define_method(c, "heap_stats", context_heap_stats, 0);
rb_define_method(c, "heap_snapshot", context_heap_snapshot, 0);
rb_define_method(c, "perform_microtask_checkpoint", context_perform_microtask_checkpoint, 0);
Expand Down
Loading