mini_racer 0.21.4 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2e290a77bd6b38286dc4e4224d71579a1e8e41bbb8fd70029584f518383ba7f
4
- data.tar.gz: 479d9793ade9c46075986dd53c0a7ebacf62e1afb199fc6983e5b7c4fb4e0faa
3
+ metadata.gz: 7a20d79f246e2bd3f9aa86cdf19c83413d2b782b7738f0bfc13017227e2c20f7
4
+ data.tar.gz: b1c6d7b75c8bbc7520f0722ed255d042e70e4e606bb627a59926523135640921
5
5
  SHA512:
6
- metadata.gz: dc69216098ec6076dc90cf731fc5fd229526a4c21d3383fdb94448aace72068f36fc6bf6910cb09d8f42bf57c6926bf1527a2fd0871c11c5b08df820643cc23e
7
- data.tar.gz: b7b4b851b4cbc9ad35c56e39db1d5c7ccc19b23dabd9dd87f084ba37f0a59fb584c833128827d58ecdb0c99f75969383333d3b304d00f816ba544e83e5436639
6
+ metadata.gz: 9c828f6078e1d6f48503bf9870cab6ced783e853decf679c0534735d7c3a9a8d6efb3c06ed7528f88f1ea8f90ddf5e2c8d50c73e89d8e7672d5b03ad7b85e3ef
7
+ data.tar.gz: 62eb803f2a6aea12b7a80cc6385858631a60dadb61c2eb409e1f5ecf79cfa969f3d9350519377c0a169bf0ffd15987c06635da2d3aaa84f3017183d51b5216bb
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ - 0.22.0 - 12-08-2026
2
+ - 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`
3
+ - Fix a `call` or `eval` made from a Ruby callback taking the timeout or `stop` meant for the evaluation around it, which then kept running
4
+ - Fix a timeout that fires just after its own evaluation ends, where it would stop the next evaluation instead
5
+ - 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
6
+
1
7
  - 0.21.4 - 24-06-2026
2
8
  - Fix stale V8 termination state after interrupts/timeouts so contexts remain usable after cancelled evaluations
3
9
  - Let Ruby interrupts wake MiniRacer calls without immediately terminating V8, allowing signal traps and nested callbacks to unwind safely
data/README.md CHANGED
@@ -348,6 +348,40 @@ Performance is slightly better than running `context.eval("hello('George')")` si
348
348
  * compilation of eval'd string is avoided
349
349
  * function arguments don't need to be converted to JSON
350
350
 
351
+ ### Promises: call_await and eval_await
352
+
353
+ `call_await` and `eval_await` work like `call` and `eval`, but when the result is a
354
+ Promise they block until it settles and return the settled value. A rejected
355
+ promise raises `MiniRacer::RuntimeError`, just like a synchronous `throw`:
356
+
357
+ ```ruby
358
+ context = MiniRacer::Context.new
359
+ context.eval("async function f(x) { await Promise.resolve(); return x * 2 }")
360
+ context.call_await("f", 21)
361
+ # => 42
362
+
363
+ context.eval_await("(async () => 6 * 7)()")
364
+ # => 42
365
+
366
+ context.eval("async function boom() { throw new Error('kaboom') }")
367
+ context.call_await("boom")
368
+ # => raises MiniRacer::RuntimeError (Error: kaboom)
369
+ ```
370
+
371
+ Non-Promise results pass through unchanged, so `call_await` is a drop-in
372
+ superset of `call` (same for `eval_await`/`eval`).
373
+
374
+ A promise that never settles blocks forever, just like an infinite loop. The
375
+ `timeout:` option and `Context#stop` both interrupt it, raising
376
+ `MiniRacer::ScriptTerminatedError`.
377
+
378
+ Calling `call_await` or `eval_await` recursively on the same context from an
379
+ attached Ruby callback is not supported and raises `MiniRacer::RuntimeError`.
380
+ V8 cannot run the nested microtask checkpoint needed to settle such a call.
381
+ Synchronous nested `call` and `eval` remain supported.
382
+
383
+ `call_await` and `eval_await` are not currently supported on TruffleRuby.
384
+
351
385
  ### Microtask checkpoints
352
386
 
353
387
  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":
@@ -152,7 +152,8 @@ typedef struct Context
152
152
  struct {
153
153
  pthread_mutex_t mtx;
154
154
  pthread_cond_t cv;
155
- int cancel;
155
+ int active; // dispatch thread only
156
+ int cancel; // protected by |mtx|
156
157
  } wd; // watchdog
157
158
  Barrier early_init, late_init;
158
159
  } Context;
@@ -772,7 +773,7 @@ static void *v8_watchdog(void *arg)
772
773
  if (c->wd.cancel)
773
774
  break;
774
775
  if (deadline_exceeded(deadline)) {
775
- v8_terminate_execution(c->pst);
776
+ v8_terminate_watchdog(c->pst);
776
777
  break;
777
778
  }
778
779
  }
@@ -786,8 +787,14 @@ static void v8_timedwait(Context *c, const uint8_t *p, size_t n,
786
787
  pthread_t thr;
787
788
  int r;
788
789
 
789
- r = -1;
790
- if (c->timeout > 0 && (r = pthread_create(&thr, NULL, v8_watchdog, c))) {
790
+ if (c->timeout <= 0 || c->wd.active) {
791
+ func(c->pst, p, n);
792
+ return;
793
+ }
794
+ c->wd.active = 1;
795
+ r = pthread_create(&thr, NULL, v8_watchdog, c);
796
+ if (r) {
797
+ c->wd.active = 0;
791
798
  fprintf(stderr, "mini_racer: watchdog: pthread_create: %s\n", strerror(r));
792
799
  fflush(stderr);
793
800
  }
@@ -799,7 +806,9 @@ static void v8_timedwait(Context *c, const uint8_t *p, size_t n,
799
806
  pthread_cond_signal(&c->wd.cv);
800
807
  pthread_mutex_unlock(&c->wd.mtx);
801
808
  pthread_join(thr, NULL);
809
+ v8_cancel_watchdog_termination(c->pst);
802
810
  c->wd.cancel = 0;
811
+ c->wd.active = 0;
803
812
  }
804
813
 
805
814
  static void dispatch1(Context *c, const uint8_t *p, size_t n)
@@ -810,7 +819,9 @@ static void dispatch1(Context *c, const uint8_t *p, size_t n)
810
819
  switch (*p) {
811
820
  case 'A': return v8_attach(c->pst, p+1, n-1);
812
821
  case 'C': return v8_timedwait(c, p+1, n-1, v8_call);
822
+ case 'D': return v8_timedwait(c, p+1, n-1, v8_call_await);
813
823
  case 'E': return v8_timedwait(c, p+1, n-1, v8_eval);
824
+ case 'F': return v8_timedwait(c, p+1, n-1, v8_eval_await);
814
825
  case 'H': return v8_heap_snapshot(c->pst);
815
826
  case 'M': return v8_perform_microtask_checkpoint(c->pst);
816
827
  case 'P': return v8_pump_message_loop(c->pst);
@@ -888,12 +899,12 @@ void v8_dispatch(Context *c)
888
899
  pthread_mutex_unlock(&c->mtx);
889
900
  }
890
901
 
891
- // only called when inside v8_call, v8_eval, or v8_pump_message_loop
902
+ // only called when inside v8_call, v8_eval (and their await variants),
903
+ // or v8_pump_message_loop
892
904
  void v8_roundtrip(Context *c, const uint8_t **p, size_t *n)
893
905
  {
894
906
  pthread_mutex_lock(&c->mtx);
895
907
  buf_reset(&c->v8_req);
896
- buf_reset(&c->req);
897
908
  if (c->res.len)
898
909
  c->res_ready = 1;
899
910
  pthread_cond_signal(&c->cv);
@@ -1655,7 +1666,7 @@ static VALUE context_stop(VALUE self)
1655
1666
  return Qnil;
1656
1667
  }
1657
1668
 
1658
- static VALUE context_call(int argc, VALUE *argv, VALUE self)
1669
+ static VALUE context_call_common(int argc, VALUE *argv, VALUE self, char op)
1659
1670
  {
1660
1671
  VALUE name, args;
1661
1672
  VALUE a, e;
@@ -1666,8 +1677,8 @@ static VALUE context_call(int argc, VALUE *argv, VALUE self)
1666
1677
  rb_scan_args(argc, argv, "1*", &name, &args);
1667
1678
  Check_Type(name, T_STRING);
1668
1679
  rb_ary_unshift(args, name);
1669
- // request is (C)all, [name, args...] array
1670
- ser_init1(&s, 'C');
1680
+ // request is (C)all or (D) call_await, [name, args...] array
1681
+ ser_init1(&s, op);
1671
1682
  if (serialize(&s, args)) {
1672
1683
  ser_reset(&s);
1673
1684
  rb_raise(runtime_error, "Context.call: %s", s.err);
@@ -1679,7 +1690,17 @@ static VALUE context_call(int argc, VALUE *argv, VALUE self)
1679
1690
  return rb_ary_pop(a);
1680
1691
  }
1681
1692
 
1682
- static VALUE context_eval(int argc, VALUE *argv, VALUE self)
1693
+ static VALUE context_call(int argc, VALUE *argv, VALUE self)
1694
+ {
1695
+ return context_call_common(argc, argv, self, 'C');
1696
+ }
1697
+
1698
+ static VALUE context_call_await(int argc, VALUE *argv, VALUE self)
1699
+ {
1700
+ return context_call_common(argc, argv, self, 'D');
1701
+ }
1702
+
1703
+ static VALUE context_eval_common(int argc, VALUE *argv, VALUE self, char op)
1683
1704
  {
1684
1705
  VALUE a, e, source, filename, kwargs;
1685
1706
  Context *c;
@@ -1694,8 +1715,8 @@ static VALUE context_eval(int argc, VALUE *argv, VALUE self)
1694
1715
  if (NIL_P(filename))
1695
1716
  filename = rb_str_new_cstr("<eval>");
1696
1717
  Check_Type(filename, T_STRING);
1697
- // request is (E)val, [filename, source] array
1698
- ser_init1(&s, 'E');
1718
+ // request is (E)val or (F) eval_await, [filename, source] array
1719
+ ser_init1(&s, op);
1699
1720
  ser_array_begin(&s, 2);
1700
1721
  add_string(&s, filename);
1701
1722
  add_string(&s, source);
@@ -1707,6 +1728,16 @@ static VALUE context_eval(int argc, VALUE *argv, VALUE self)
1707
1728
  return rb_ary_pop(a);
1708
1729
  }
1709
1730
 
1731
+ static VALUE context_eval(int argc, VALUE *argv, VALUE self)
1732
+ {
1733
+ return context_eval_common(argc, argv, self, 'E');
1734
+ }
1735
+
1736
+ static VALUE context_eval_await(int argc, VALUE *argv, VALUE self)
1737
+ {
1738
+ return context_eval_common(argc, argv, self, 'F');
1739
+ }
1740
+
1710
1741
  static VALUE context_heap_stats(VALUE self)
1711
1742
  {
1712
1743
  VALUE a, h, k, v;
@@ -2147,7 +2178,9 @@ void Init_mini_racer_extension(void)
2147
2178
  rb_define_method(c, "dispose", context_dispose, 0);
2148
2179
  rb_define_method(c, "stop", context_stop, 0);
2149
2180
  rb_define_method(c, "call", context_call, -1);
2181
+ rb_define_method(c, "call_await", context_call_await, -1);
2150
2182
  rb_define_method(c, "eval", context_eval, -1);
2183
+ rb_define_method(c, "eval_await", context_eval_await, -1);
2151
2184
  rb_define_method(c, "heap_stats", context_heap_stats, 0);
2152
2185
  rb_define_method(c, "heap_snapshot", context_heap_snapshot, 0);
2153
2186
  rb_define_method(c, "perform_microtask_checkpoint", context_perform_microtask_checkpoint, 0);
@@ -2,6 +2,7 @@
2
2
  #include "v8-profiler.h"
3
3
  #include "libplatform/libplatform.h"
4
4
  #include "mini_racer_v8.h"
5
+ #include <atomic>
5
6
  #include <memory>
6
7
  #include <vector>
7
8
  #include <cassert>
@@ -68,6 +69,12 @@ struct Callback
68
69
  int32_t id;
69
70
  };
70
71
 
72
+ enum : unsigned
73
+ {
74
+ NON_WATCHDOG_TERMINATION = 1,
75
+ WATCHDOG_TERMINATION = 2,
76
+ };
77
+
71
78
  // NOTE: do *not* use thread_locals to store state. In single-threaded
72
79
  // mode, V8 runs on the same thread as Ruby and the Ruby runtime clobbers
73
80
  // thread-locals when it context-switches threads. Ruby 3.4.0 has a new
@@ -91,6 +98,14 @@ struct State
91
98
  Context *ruby_context;
92
99
  int64_t max_memory;
93
100
  int err_reason;
101
+ // TerminateExecution() while idle doesn't make IsExecutionTerminating() true
102
+ std::atomic<unsigned> terminate_requested;
103
+ // Armed before checking terminate_requested so a concurrent terminator
104
+ // either gets observed or posts a task. Uses sequential consistency.
105
+ std::atomic<bool> waiting_for_task;
106
+ // Tracks reentrant call/eval dispatches so nested await calls can fail
107
+ // instead of deadlocking and nested pumps preserve outer termination.
108
+ int javascript_call_depth;
94
109
  bool verbose_exceptions;
95
110
  std::vector<Callback*> callbacks;
96
111
  std::unique_ptr<v8::ArrayBuffer::Allocator> allocator;
@@ -416,6 +431,7 @@ void v8_gc_callback(v8::Isolate*, v8::GCType, v8::GCCallbackFlags, void *data)
416
431
  int64_t used_heap_size = static_cast<int64_t>(s.used_heap_size());
417
432
  if (used_heap_size > st.max_memory) {
418
433
  st.err_reason = MEMORY_ERROR;
434
+ st.terminate_requested.fetch_or(NON_WATCHDOG_TERMINATION);
419
435
  st.isolate->TerminateExecution();
420
436
  }
421
437
  }
@@ -586,8 +602,76 @@ fail:
586
602
  reply_retry(st, err);
587
603
  }
588
604
 
605
+ struct JavascriptCallScope
606
+ {
607
+ int& depth;
608
+
609
+ explicit JavascriptCallScope(int& depth) : depth(depth) { depth++; }
610
+ ~JavascriptCallScope() { depth--; }
611
+ };
612
+
613
+ void throw_nested_await_call(State& st)
614
+ {
615
+ // V8 does not run microtask checkpoints recursively. A nested await call
616
+ // from an attached Ruby callback can therefore deadlock, so reject it
617
+ // before entering JavaScript.
618
+ auto message = v8::String::NewFromUtf8Literal(
619
+ st.isolate, "nested call_await/eval_await is not supported");
620
+ st.isolate->ThrowException(v8::Exception::Error(message));
621
+ }
622
+
623
+ // awaits |*result| if it's a promise; false means an exception is pending
624
+ bool await_promise(State& st, v8::Local<v8::Value> *result)
625
+ {
626
+ if (!(*result)->IsPromise()) return true;
627
+ auto promise = result->As<v8::Promise>();
628
+ for (;;) {
629
+ v8::MicrotasksScope::PerformCheckpoint(st.isolate);
630
+ switch (promise->State()) {
631
+ case v8::Promise::kFulfilled:
632
+ *result = promise->Result();
633
+ return true;
634
+ case v8::Promise::kRejected:
635
+ st.isolate->ThrowException(promise->Result());
636
+ return false;
637
+ case v8::Promise::kPending:
638
+ break;
639
+ }
640
+ st.waiting_for_task.store(true);
641
+ if (st.terminate_requested.load() || st.isolate->IsExecutionTerminating()) {
642
+ st.waiting_for_task.store(false);
643
+ return false;
644
+ }
645
+ // blocks until the next task; a concurrent v8_terminate_execution
646
+ // observes waiting_for_task and posts one to end the wait
647
+ v8::platform::PumpMessageLoop(
648
+ platform, st.isolate,
649
+ v8::platform::MessageLoopBehavior::kWaitForWork);
650
+ st.waiting_for_task.store(false);
651
+ }
652
+ }
653
+
654
+ // Nested calls report termination but leave it pending for the enclosing call.
655
+ // Outermost calls consume it as before.
656
+ bool suspend_termination(State& st, bool nested, int& cause)
657
+ {
658
+ unsigned requests = nested ? st.terminate_requested.load()
659
+ : st.terminate_requested.exchange(0);
660
+ if (!requests && !st.isolate->IsExecutionTerminating()) return false;
661
+ st.isolate->CancelTerminateExecution();
662
+ cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
663
+ if (!nested) st.err_reason = NO_ERROR;
664
+ return nested;
665
+ }
666
+
667
+ void restore_termination(State& st, bool suspended)
668
+ {
669
+ if (suspended)
670
+ st.isolate->TerminateExecution();
671
+ }
672
+
589
673
  // response is errback [result, err] array
590
- extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
674
+ void v8_call_impl(State *pst, const uint8_t *p, size_t n, bool await)
591
675
  {
592
676
  State& st = *pst;
593
677
  v8::TryCatch try_catch(st.isolate);
@@ -598,6 +682,14 @@ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
598
682
  des.ReadHeader(st.context).Check();
599
683
  v8::Local<v8::Value> result;
600
684
  int cause = INTERNAL_ERROR;
685
+ bool preserve_termination = false;
686
+ bool nested = st.javascript_call_depth > 0;
687
+ JavascriptCallScope call_scope(st.javascript_call_depth);
688
+ if (await && nested) {
689
+ throw_nested_await_call(st);
690
+ cause = RUNTIME_ERROR;
691
+ goto fail;
692
+ }
601
693
  {
602
694
  v8::Local<v8::Value> request_v;
603
695
  if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
@@ -645,16 +737,16 @@ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
645
737
  auto maybe_result_v = function->Call(st.context, obj, args.size(), args.data());
646
738
  v8::Local<v8::Value> result_v;
647
739
  if (!maybe_result_v.ToLocal(&result_v)) goto fail;
740
+ if (await && !await_promise(st, &result_v)) goto fail;
648
741
  result = sanitize(st, result_v);
649
742
  }
650
743
  cause = NO_ERROR;
651
744
  fail:
652
- if (st.isolate->IsExecutionTerminating()) {
653
- st.isolate->CancelTerminateExecution();
654
- cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
655
- st.err_reason = NO_ERROR;
745
+ preserve_termination = suspend_termination(st, nested, cause);
746
+ if (bubble_up_ruby_exception(st, &try_catch)) {
747
+ restore_termination(st, preserve_termination);
748
+ return;
656
749
  }
657
- if (bubble_up_ruby_exception(st, &try_catch)) return;
658
750
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
659
751
  if (cause) result = v8::Undefined(st.isolate);
660
752
  auto err = to_error(st, &try_catch, cause);
@@ -662,10 +754,21 @@ fail:
662
754
  assert(try_catch.HasCaught());
663
755
  goto fail; // retry; can be termination exception
664
756
  }
757
+ restore_termination(st, preserve_termination);
758
+ }
759
+
760
+ extern "C" void v8_call(State *pst, const uint8_t *p, size_t n)
761
+ {
762
+ v8_call_impl(pst, p, n, false);
763
+ }
764
+
765
+ extern "C" void v8_call_await(State *pst, const uint8_t *p, size_t n)
766
+ {
767
+ v8_call_impl(pst, p, n, true);
665
768
  }
666
769
 
667
770
  // response is errback [result, err] array
668
- extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
771
+ void v8_eval_impl(State *pst, const uint8_t *p, size_t n, bool await)
669
772
  {
670
773
  State& st = *pst;
671
774
  v8::TryCatch try_catch(st.isolate);
@@ -675,6 +778,14 @@ extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
675
778
  des.ReadHeader(st.context).Check();
676
779
  v8::Local<v8::Value> result;
677
780
  int cause = INTERNAL_ERROR;
781
+ bool preserve_termination = false;
782
+ bool nested = st.javascript_call_depth > 0;
783
+ JavascriptCallScope call_scope(st.javascript_call_depth);
784
+ if (await && nested) {
785
+ throw_nested_await_call(st);
786
+ cause = RUNTIME_ERROR;
787
+ goto fail;
788
+ }
678
789
  {
679
790
  v8::Local<v8::Value> request_v;
680
791
  if (!des.ReadValue(st.context).ToLocal(&request_v)) goto fail;
@@ -694,16 +805,16 @@ extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
694
805
  cause = RUNTIME_ERROR;
695
806
  auto maybe_result_v = script->Run(st.context);
696
807
  if (!maybe_result_v.ToLocal(&result_v)) goto fail;
808
+ if (await && !await_promise(st, &result_v)) goto fail;
697
809
  result = sanitize(st, result_v);
698
810
  }
699
811
  cause = NO_ERROR;
700
812
  fail:
701
- if (st.isolate->IsExecutionTerminating()) {
702
- st.isolate->CancelTerminateExecution();
703
- cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
704
- st.err_reason = NO_ERROR;
813
+ preserve_termination = suspend_termination(st, nested, cause);
814
+ if (bubble_up_ruby_exception(st, &try_catch)) {
815
+ restore_termination(st, preserve_termination);
816
+ return;
705
817
  }
706
- if (bubble_up_ruby_exception(st, &try_catch)) return;
707
818
  if (!cause && try_catch.HasCaught()) cause = RUNTIME_ERROR;
708
819
  if (cause) result = v8::Undefined(st.isolate);
709
820
  auto err = to_error(st, &try_catch, cause);
@@ -711,6 +822,17 @@ fail:
711
822
  assert(try_catch.HasCaught());
712
823
  goto fail; // retry; can be termination exception
713
824
  }
825
+ restore_termination(st, preserve_termination);
826
+ }
827
+
828
+ extern "C" void v8_eval(State *pst, const uint8_t *p, size_t n)
829
+ {
830
+ v8_eval_impl(pst, p, n, false);
831
+ }
832
+
833
+ extern "C" void v8_eval_await(State *pst, const uint8_t *p, size_t n)
834
+ {
835
+ v8_eval_impl(pst, p, n, true);
714
836
  }
715
837
 
716
838
  extern "C" void v8_heap_stats(State *pst)
@@ -775,6 +897,7 @@ extern "C" void v8_perform_microtask_checkpoint(State *pst)
775
897
  // Leave any termination active so the enclosing v8_call/v8_eval frame
776
898
  // surfaces OOM (set by v8_gc_callback) or watchdog termination to Ruby.
777
899
  State& st = *pst;
900
+ JavascriptCallScope call_scope(st.javascript_call_depth);
778
901
  v8::TryCatch try_catch(st.isolate);
779
902
  try_catch.SetVerbose(st.verbose_exceptions);
780
903
  v8::HandleScope handle_scope(st.isolate);
@@ -785,6 +908,8 @@ extern "C" void v8_perform_microtask_checkpoint(State *pst)
785
908
  extern "C" void v8_pump_message_loop(State *pst)
786
909
  {
787
910
  State& st = *pst;
911
+ bool nested = st.javascript_call_depth > 0;
912
+ JavascriptCallScope call_scope(st.javascript_call_depth);
788
913
  v8::TryCatch try_catch(st.isolate);
789
914
  try_catch.SetVerbose(st.verbose_exceptions);
790
915
  v8::HandleScope handle_scope(st.isolate);
@@ -800,7 +925,10 @@ extern "C" void v8_pump_message_loop(State *pst)
800
925
  if (try_catch.HasCaught()) goto fail;
801
926
  }
802
927
  fail:
803
- if (st.isolate->IsExecutionTerminating()) {
928
+ // A nested pump must leave termination active for the enclosing call/eval.
929
+ if (!nested &&
930
+ (st.terminate_requested.exchange(0) ||
931
+ st.isolate->IsExecutionTerminating())) {
804
932
  st.isolate->CancelTerminateExecution();
805
933
  st.err_reason = NO_ERROR;
806
934
  }
@@ -914,7 +1042,8 @@ extern "C" void v8_snapshot(State *pst, const uint8_t *p, size_t n)
914
1042
  }
915
1043
  cause = NO_ERROR;
916
1044
  fail:
917
- if (st.isolate->IsExecutionTerminating()) {
1045
+ if (st.terminate_requested.exchange(0) ||
1046
+ st.isolate->IsExecutionTerminating()) {
918
1047
  st.isolate->CancelTerminateExecution();
919
1048
  cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
920
1049
  st.err_reason = NO_ERROR;
@@ -984,7 +1113,8 @@ extern "C" void v8_warmup(State *pst, const uint8_t *p, size_t n)
984
1113
  }
985
1114
  cause = NO_ERROR;
986
1115
  fail:
987
- if (st.isolate->IsExecutionTerminating()) {
1116
+ if (st.terminate_requested.exchange(0) ||
1117
+ st.isolate->IsExecutionTerminating()) {
988
1118
  st.isolate->CancelTerminateExecution();
989
1119
  cause = st.err_reason ? st.err_reason : TERMINATED_ERROR;
990
1120
  st.err_reason = NO_ERROR;
@@ -1008,10 +1138,42 @@ extern "C" void v8_low_memory_notification(State *pst)
1008
1138
  pst->isolate->LowMemoryNotification();
1009
1139
  }
1010
1140
 
1011
- // called from ruby thread
1012
- extern "C" void v8_terminate_execution(State *pst)
1141
+ struct WakeupTask : public v8::Task
1142
+ {
1143
+ void Run() final {}
1144
+ };
1145
+
1146
+ // called from ruby or watchdog thread
1147
+ void request_termination(State *pst, unsigned request)
1013
1148
  {
1149
+ pst->terminate_requested.fetch_or(request);
1014
1150
  pst->isolate->TerminateExecution();
1151
+ // Wake await_promise if it may be blocked. A racing early return can leave
1152
+ // one harmless no-op task queued.
1153
+ if (pst->waiting_for_task.exchange(false)) {
1154
+ platform->GetForegroundTaskRunner(pst->isolate)
1155
+ ->PostTask(std::make_unique<WakeupTask>());
1156
+ }
1157
+ }
1158
+
1159
+ extern "C" void v8_terminate_execution(State *pst)
1160
+ {
1161
+ request_termination(pst, NON_WATCHDOG_TERMINATION);
1162
+ }
1163
+
1164
+ extern "C" void v8_terminate_watchdog(State *pst)
1165
+ {
1166
+ request_termination(pst, WATCHDOG_TERMINATION);
1167
+ }
1168
+
1169
+ extern "C" void v8_cancel_watchdog_termination(State *pst)
1170
+ {
1171
+ unsigned requests = pst->terminate_requested.fetch_and(~WATCHDOG_TERMINATION);
1172
+ if (requests == WATCHDOG_TERMINATION) {
1173
+ pst->isolate->CancelTerminateExecution();
1174
+ if (pst->terminate_requested.load())
1175
+ pst->isolate->TerminateExecution();
1176
+ }
1015
1177
  }
1016
1178
 
1017
1179
  // called from ruby thread
@@ -1019,6 +1181,7 @@ extern "C" void v8_cancel_terminate_execution(State *pst)
1019
1181
  {
1020
1182
  // TerminateExecution can race with V8 completing and queue a termination
1021
1183
  // for the next entry without IsExecutionTerminating() becoming true.
1184
+ pst->terminate_requested.store(0);
1022
1185
  pst->isolate->CancelTerminateExecution();
1023
1186
  }
1024
1187
 
@@ -39,7 +39,9 @@ struct State *v8_thread_init(struct Context *c, const uint8_t *snapshot_buf,
39
39
  int verbose_exceptions); // calls v8_thread_main
40
40
  void v8_attach(struct State *pst, const uint8_t *p, size_t n);
41
41
  void v8_call(struct State *pst, const uint8_t *p, size_t n);
42
+ void v8_call_await(struct State *pst, const uint8_t *p, size_t n);
42
43
  void v8_eval(struct State *pst, const uint8_t *p, size_t n);
44
+ void v8_eval_await(struct State *pst, const uint8_t *p, size_t n);
43
45
  void v8_heap_stats(struct State *pst);
44
46
  void v8_heap_snapshot(struct State *pst);
45
47
  void v8_perform_microtask_checkpoint(struct State *pst);
@@ -47,7 +49,9 @@ void v8_pump_message_loop(struct State *pst);
47
49
  void v8_snapshot(struct State *pst, const uint8_t *p, size_t n);
48
50
  void v8_warmup(struct State *pst, const uint8_t *p, size_t n);
49
51
  void v8_low_memory_notification(struct State *pst);
50
- void v8_terminate_execution(struct State *pst); // called from ruby or watchdog thread
52
+ void v8_terminate_execution(struct State *pst); // called from ruby thread
53
+ void v8_terminate_watchdog(struct State *pst); // called from watchdog thread
54
+ void v8_cancel_watchdog_termination(struct State *pst); // called from v8 thread
51
55
  void v8_cancel_terminate_execution(struct State *pst); // called from ruby thread
52
56
  void v8_single_threaded_enter(struct State *pst, struct Context *c, void (*f)(struct Context *c));
53
57
  void v8_single_threaded_dispose(struct State *pst);