mini_racer-csim 0.21.1.2 → 0.21.1.4

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: 876af5088db34f57853916faa39e0568268bfcce946d44078a263de4f9e45867
4
- data.tar.gz: 887bf5d80fd136be29e5f6243bc5fd3185b1020eee67002ae5c03176e75f3a85
3
+ metadata.gz: 4a64c6a3cd59b38b736212c4ef9685cbc7c6bc9e56d0aee12734e957e749fb7c
4
+ data.tar.gz: 50e7e8a671b90f3bf2e6563092f31ed00edf7d2c8093eb52eccbe502635773ca
5
5
  SHA512:
6
- metadata.gz: 365acff79cbaca1871707ea32e50d1d91dc7f15c23002e260449c1e1a553cb50c211fdb2406bc54b5bb40aa4e5848e915064891943b68dc8b4ca3ac02589a728
7
- data.tar.gz: 48dec93bf2a431bedc18c6c7d5b79890d833db748b525bc5f19c22a796adcd6a0e045acf43d5ac9226a5094dbacf4bc5a48a0214dab71983fd993f930d1ce465
6
+ metadata.gz: 53cb234c4dca6643756f5f0558080dade542569bf64aef713e0f44477e05d2c7e14bc305f63671519d67131a63f35dcee65829893dd493011b5f2764552677bd
7
+ data.tar.gz: ee5e61767e56179bf2aec29c54ca81d0f3c6b3ad7d1788498bb45b84ee5f7d3b6dcb613cc5f297dbdcb4eae32f4005b594a6a7a562d51c9d1904b646537976cb
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ - 0.21.1.4 - 08-06-2026
2
+ - Memoize the safe-context reply filter (the recursive deep-copy fallback used when a reply value is not cloneable by V8's ValueSerializer — e.g. it contains a function or a host object). A visited-set (original → filtered copy) makes shared subgraphs clone once instead of being re-walked per reference path (O(N) instead of super-linear), terminates cycles (previously a cyclic value containing a non-cloneable member recursed forever), and preserves object identity — matching the ValueSerializer fast path it backs
3
+
4
+ - 0.21.1.3 - 08-06-2026
5
+ - **Breaking (realm JS API):** move the per-frame-realm JS helpers off bare `globalThis` globals onto the opt-in host namespace, so globalThis pollution is decided once (by `host_namespace:`) rather than per feature
6
+ - `__mr_realmGlobal(id)` → `<host_namespace>.realmGlobal(id)`
7
+ - `__mr_realmOf(fn)` → `<host_namespace>.realmOf(fn)`
8
+ - the embedder-defined `globalThis.__mr_emitUnhandledRejection` hook → a `<host_namespace>.onUnhandledRejection(fn)` registration method (the engine stores the handler per realm and calls it)
9
+ - **using these JS helpers now requires `Context.new(host_namespace:)`.** Realms themselves do not: `Context#create_realm` + `Realm#eval`/`call`/`attach` work without it (isolated realms driven from Ruby) — only cross-realm wiring *in JS* needs the namespace, which is why the helpers live there
10
+
1
11
  - 0.21.1.2 - 07-06-2026
2
12
  - **Breaking:** move off the `mini_racer` require path / `MiniRacer` namespace to a fork-specific identity so the gem never collides with — and loads deterministically alongside — upstream `mini_racer` in the same bundle
3
13
  - require path: `require 'mini_racer_csim'` (was `require 'mini_racer'`); the `require 'mini_racer-csim'` autorequire shim now points here
data/README.md CHANGED
@@ -12,7 +12,7 @@ MiniRacer has an adapter for [execjs](https://github.com/rails/execjs) so it can
12
12
 
13
13
  ## This repository is `mini_racer-csim` (a fork)
14
14
 
15
- This is **`mini_racer-csim`**, a private fork of [`mini_racer`](https://github.com/rubyjs/mini_racer) maintained for [capybara-simulated](https://github.com/ursm/capybara-simulated). It adds browser-fidelity extensions (ES modules, realm reset, …) that capybara-simulated needs but most users do not — **if you are not using capybara-simulated, use upstream `mini_racer`.**
15
+ This is **`mini_racer-csim`**, a private fork of [`mini_racer`](https://github.com/rubyjs/mini_racer) maintained for [capybara-simulated](https://github.com/ursm/capybara-simulated). It adds browser-fidelity extensions (ES modules, per-frame realms, realm reset, …) that capybara-simulated needs but most users do not — **if you are not using capybara-simulated, use upstream `mini_racer`.**
16
16
 
17
17
  It has its **own require path and namespace** so it never collides with — and loads deterministically alongside — upstream `mini_racer` in the same bundle: load it with `require 'mini_racer_csim'` and use the `MiniRacerCsim` module (e.g. `MiniRacerCsim::Context`). The native extensions are `mini_racer_csim_extension` / `mini_racer_csim_loader`. (The JS-side host-namespace brand, `globalThis.MiniRacer` by default, is embedder-chosen and unrelated to the Ruby namespace.)
18
18
 
@@ -24,10 +24,11 @@ It has its **own require path and namespace** so it never collides with — and
24
24
  | ES Module API | `Context#compile_module` → `MiniRacerCsim::Module` (`#instantiate` / `#evaluate` / `#namespace` / `#status` / `#cached_data` / `#dispose`); `Context#dynamic_import_resolver=` | V8's ES module pipeline, `import.meta.url`, dynamic `import()` |
25
25
  | Batched module-graph loader | `Context#load_module_graph(resolve:, …)` | Loads an ESM graph in one batched, native (C++) pass; one `Module` per URL shared across every load path |
26
26
  | Realm reset | `Context#reset_realm` | Discards the user realm (`globalThis`) while keeping the warm isolate (browser per-navigation model); re-binds attached host functions and the host namespace |
27
+ | Per-frame realms | `Context#create_realm` → `MiniRacerCsim::Realm` (`#eval` / `#call` / `#attach` / `#dispose` / `#disposed?`) | Multiple V8 realms (Contexts) in one isolate with browser-iframe semantics: realms share one security token for cross-realm access. JS-side helpers live on the [host namespace](#host-namespace) (so using them needs `host_namespace:`): `<ns>.realmGlobal(id)` exposes a realm's live `globalThis`, `<ns>.realmOf(fn)` reports a callback's `[[Realm]]`, and `<ns>.onUnhandledRejection(fn)` registers a per-realm unhandled-rejection handler. Realms themselves work without the namespace (driven from Ruby). |
27
28
  | Host namespace | `Context.new(host_namespace: "MiniRacer")` → `globalThis.MiniRacer.drainMicrotasks()` | Opt-in JS namespace exposing an inline, rendezvous-free microtask checkpoint |
28
29
  | GVL release on boot | (automatic) | Releases the Ruby GVL while the V8 thread boots the isolate |
29
30
 
30
- The fork is periodically rebased on upstream `mini_racer` to pick up V8 / `libv8-node` bumps and bug fixes.
31
+ This is a hard fork: it no longer tracks upstream `mini_racer`, and follows only `libv8-node` for V8 version bumps.
31
32
 
32
33
  ## Supported Ruby Versions & Troubleshooting
33
34
 
@@ -23,44 +23,61 @@ static const char safe_context_script_source[] = R"js(
23
23
  ;(function($globalThis) {
24
24
  const {Map: $Map, Set: $Set} = $globalThis
25
25
  const sentinel = {}
26
- return function filter(v) {
27
- if (typeof v === "function")
28
- return sentinel
29
- if (typeof v !== "object" || v === null)
30
- return v
31
- if (v instanceof $Map) {
32
- const m = new Map()
33
- for (let [k, t] of Map.prototype.entries.call(v)) {
34
- t = filter(t)
35
- if (t !== sentinel)
36
- m.set(k, t)
37
- }
38
- return m
39
- } else if (v instanceof $Set) {
40
- const s = new Set()
41
- for (let t of Set.prototype.values.call(v)) {
42
- t = filter(t)
43
- if (t !== sentinel)
44
- s.add(t)
45
- }
46
- return s
47
- } else {
48
- const o = Array.isArray(v) ? [] : {}
49
- const pds = Object.getOwnPropertyDescriptors(v)
50
- for (const [k, d] of Object.entries(pds)) {
51
- if (!d.enumerable)
52
- continue
53
- let t = d.value
54
- if (d.get) {
55
- // *not* d.get.call(...), may have been tampered with
56
- t = Function.prototype.call.call(d.get, v, k)
26
+ return function filter(root) {
27
+ // Memoize original -> filtered copy. Registered BEFORE recursing into a
28
+ // value's children so that a value reachable by many paths is cloned
29
+ // once (linear, not super-linear: e.g. a DOM node's ownerDocument is
30
+ // reachable from every node), cycles terminate (the in-progress copy is
31
+ // returned), and object identity is preserved. This matches V8's
32
+ // ValueSerializer (the fast path this is the fallback for), whose ref
33
+ // table also dedupes and handles cycles; without it the slow path both
34
+ // diverges (duplicates shared objects) and can recurse forever on a
35
+ // cyclic value that happens to contain a non-cloneable member.
36
+ const seen = new Map()
37
+ return (function rec(v) {
38
+ if (typeof v === "function")
39
+ return sentinel
40
+ if (typeof v !== "object" || v === null)
41
+ return v
42
+ if (seen.has(v))
43
+ return seen.get(v)
44
+ if (v instanceof $Map) {
45
+ const m = new Map()
46
+ seen.set(v, m)
47
+ for (let [k, t] of Map.prototype.entries.call(v)) {
48
+ t = rec(t)
49
+ if (t !== sentinel)
50
+ m.set(k, t)
51
+ }
52
+ return m
53
+ } else if (v instanceof $Set) {
54
+ const s = new Set()
55
+ seen.set(v, s)
56
+ for (let t of Set.prototype.values.call(v)) {
57
+ t = rec(t)
58
+ if (t !== sentinel)
59
+ s.add(t)
60
+ }
61
+ return s
62
+ } else {
63
+ const o = Array.isArray(v) ? [] : {}
64
+ seen.set(v, o)
65
+ const pds = Object.getOwnPropertyDescriptors(v)
66
+ for (const [k, d] of Object.entries(pds)) {
67
+ if (!d.enumerable)
68
+ continue
69
+ let t = d.value
70
+ if (d.get) {
71
+ // *not* d.get.call(...), may have been tampered with
72
+ t = Function.prototype.call.call(d.get, v, k)
73
+ }
74
+ t = rec(t)
75
+ if (t !== sentinel)
76
+ Object.defineProperty(o, k, {value: t, enumerable: true})
57
77
  }
58
- t = filter(t)
59
- if (t !== sentinel)
60
- Object.defineProperty(o, k, {value: t, enumerable: true})
78
+ return o
61
79
  }
62
- return o
63
- }
80
+ })(root)
64
81
  }
65
82
  })
66
83
  )js";
@@ -134,6 +151,11 @@ struct Realm
134
151
  // graph_resolve_callback can resolve imports from the pre-walked graph
135
152
  // with zero Ruby round-trips. Null otherwise.
136
153
  struct GraphLoad *active_graph = nullptr;
154
+ // Embedder-registered handler for promises that reject with no handler in
155
+ // this realm, set via <host_namespace>.onUnhandledRejection(fn). Called by
156
+ // notify_unhandled_rejections with (reason, promise). Global so its dtor
157
+ // releases the function when the realm is disposed; reset on reset_realm.
158
+ v8::Global<v8::Function> unhandled_rejection_handler;
137
159
  };
138
160
 
139
161
  struct State
@@ -768,13 +790,14 @@ void v8_drain_microtasks_callback(const v8::FunctionCallbackInfo<v8::Value>& inf
768
790
  info.GetReturnValue().SetUndefined();
769
791
  }
770
792
 
771
- // __mr_realmGlobal(id): returns the globalThis of realm `id` as a LIVE V8
772
- // object in the calling realm (same isolate, not a copy), or undefined for an
773
- // unknown realm. Installed on every realm's global. Because all realms share
774
- // one security token (see install_realm), the caller can read/write the
775
- // returned global's properties this is how csim wires frames[i] /
776
- // iframe.contentWindow to the right realm. Cross-realm object identity holds:
777
- // mutating a property here is visible in the target realm and vice versa.
793
+ // <host_namespace>.realmGlobal(id): returns the globalThis of realm `id` as a
794
+ // LIVE V8 object in the calling realm (same isolate, not a copy), or undefined
795
+ // for an unknown realm. Hung off the host namespace (opt-in via
796
+ // Context.new(host_namespace:)) rather than a bare global, so globalThis stays
797
+ // unpolluted. Because all realms share one security token (see install_realm),
798
+ // the caller can read/write the returned global's properties this is how csim
799
+ // wires frames[i] / iframe.contentWindow to the right realm. Cross-realm object
800
+ // identity holds: mutating a property here is visible in the target realm.
778
801
  void v8_realm_global_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
779
802
  {
780
803
  auto isolate = info.GetIsolate();
@@ -815,13 +838,13 @@ static int32_t realm_id_of_context(v8::Local<v8::Context> ctx)
815
838
  return v.As<v8::Int32>()->Value();
816
839
  }
817
840
 
818
- // __mr_realmOf(fn): returns the realm id where `fn` (any object/function) was
819
- // created — its [[Realm]] / creation context — or undefined if unknown. This is
820
- // the realm WebIDL's "invoke a callback function" reports errors against (e.g.
821
- // a setTimeout callback's uncaught throw is reported on the realm that *created*
822
- // the callback, not the scheduling realm nor the thrown Error's realm). csim
823
- // uses it to dispatch an ErrorEvent on __mr_realmGlobal(__mr_realmOf(cb)) from
824
- // its own per-callback try/catch.
841
+ // <host_namespace>.realmOf(fn): returns the realm id where `fn` (any
842
+ // object/function) was created — its [[Realm]] / creation context — or
843
+ // undefined if unknown. This is the realm WebIDL's "invoke a callback function"
844
+ // reports errors against (e.g. a setTimeout callback's uncaught throw is
845
+ // reported on the realm that *created* the callback, not the scheduling realm
846
+ // nor the thrown Error's realm). csim uses it to dispatch an ErrorEvent on
847
+ // <ns>.realmGlobal(<ns>.realmOf(cb)) from its own per-callback try/catch.
825
848
  void v8_realm_of_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
826
849
  {
827
850
  auto isolate = info.GetIsolate();
@@ -842,6 +865,27 @@ void v8_realm_of_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
842
865
  info.GetReturnValue().Set(v8::Integer::New(isolate, rid));
843
866
  }
844
867
 
868
+ // <host_namespace>.onUnhandledRejection(fn): register fn as the calling realm's
869
+ // handler for promises that reject with no handler. notify_unhandled_rejections
870
+ // calls it with (reason, promise) at the next microtask checkpoint, entered in
871
+ // the rejecting promise's realm (HTML notify-rejected-promises). The handler is
872
+ // stored per realm (keyed by the calling context's realm id) instead of as a
873
+ // bare globalThis property, so globalThis stays unpolluted. Passing a non-
874
+ // function clears the handler.
875
+ void v8_set_unhandled_rejection_handler(const v8::FunctionCallbackInfo<v8::Value>& info)
876
+ {
877
+ auto isolate = info.GetIsolate();
878
+ State& st = *static_cast<State*>(isolate->GetData(0));
879
+ int32_t rid = realm_id_of_context(isolate->GetCurrentContext());
880
+ auto it = st.realms.find(rid);
881
+ if (it == st.realms.end())
882
+ return; // unknown realm: no-op
883
+ if (info.Length() >= 1 && info[0]->IsFunction())
884
+ it->second->unhandled_rejection_handler.Reset(isolate, info[0].As<v8::Function>());
885
+ else
886
+ it->second->unhandled_rejection_handler.Reset();
887
+ }
888
+
845
889
  // V8 calls this when a promise's rejection state changes. We implement the
846
890
  // HTML "notify rejected promises" bookkeeping: queue promises that reject with
847
891
  // no handler (tagged with the realm they were created in), and drop them again
@@ -1041,12 +1085,13 @@ static void clear_realm_locals(State& st)
1041
1085
  }
1042
1086
 
1043
1087
  // HTML notify-rejected-promises: for each promise that rejected with no handler
1044
- // since the last checkpoint, enter its realm and call the embedder's
1045
- // globalThis.__mr_emitUnhandledRejection(reason, promise) if present (csim
1046
- // turns that into a PromiseRejectionEvent so addEventListener('unhandledrejection')
1047
- // fires natively in the right realm). The list is snapshotted and cleared first
1048
- // so rejections triggered by a handler queue for the next checkpoint instead of
1049
- // looping. A realm disposed in the meantime is skipped.
1088
+ // since the last checkpoint, enter its realm and call that realm's handler
1089
+ // (registered via <host_namespace>.onUnhandledRejection) with (reason, promise)
1090
+ // if one was set (csim turns that into a PromiseRejectionEvent so
1091
+ // addEventListener('unhandledrejection') fires natively in the right realm). The
1092
+ // list is snapshotted and cleared first so rejections triggered by a handler
1093
+ // queue for the next checkpoint instead of looping. A realm disposed in the
1094
+ // meantime is skipped.
1050
1095
  static void notify_unhandled_rejections(State& st)
1051
1096
  {
1052
1097
  if (st.pending_rejections.empty())
@@ -1067,18 +1112,17 @@ static void notify_unhandled_rejections(State& st)
1067
1112
  auto context = v8::Local<v8::Context>::New(st.isolate, it->second->persistent_context);
1068
1113
  if (context.IsEmpty())
1069
1114
  continue;
1115
+ if (it->second->unhandled_rejection_handler.IsEmpty())
1116
+ continue; // realm registered no onUnhandledRejection handler
1070
1117
  st.active_realm_id = pr.second;
1071
1118
  restore_realm_locals(st);
1072
1119
  v8::Context::Scope context_scope(context);
1073
1120
  auto global = context->Global();
1074
- auto name = v8::String::NewFromUtf8Literal(st.isolate, "__mr_emitUnhandledRejection");
1075
- v8::Local<v8::Value> hook;
1076
- if (!global->Get(context, name).ToLocal(&hook) || !hook->IsFunction())
1077
- continue;
1121
+ auto hook = v8::Local<v8::Function>::New(st.isolate, it->second->unhandled_rejection_handler);
1078
1122
  auto promise = v8::Local<v8::Promise>::New(st.isolate, pr.first);
1079
1123
  v8::TryCatch try_catch(st.isolate); // swallow errors from the handler itself
1080
1124
  v8::Local<v8::Value> args[2] = { promise->Result(), promise };
1081
- (void)hook.As<v8::Function>()->Call(context, global, 2, args);
1125
+ (void)hook->Call(context, global, 2, args);
1082
1126
  }
1083
1127
  st.active_realm_id = prev;
1084
1128
  st.context = saved_context;
@@ -1133,47 +1177,41 @@ static bool install_realm(State& st)
1133
1177
  v8::Context::Scope context_scope(context);
1134
1178
  // If the embedder opted in via Context.new(host_namespace:), install a
1135
1179
  // single host-namespace object (in the spirit of Deno's `Deno` / Bun's
1136
- // `Bun`) under that global name and hang native helpers off it. The object
1137
- // closes over native code pointers so it cannot live in the (de)serialized
1138
- // snapshot; it is installed here on every fresh realm. The namespace is
1139
- // non-enumerable on globalThis so it stays out of Object.keys(globalThis)/
1140
- // for-in; its methods are ordinary enumerable properties so they remain
1141
- // discoverable on the object.
1180
+ // `Bun`) under that global name and hang EVERY native JS helper off it:
1181
+ // drainMicrotasks(), realmGlobal(id), realmOf(fn), onUnhandledRejection(fn).
1182
+ // Keeping them on one opt-in object (rather than bare __mr_* globals) means
1183
+ // globalThis pollution is decided once, by the opt-in — not relitigated per
1184
+ // feature. The object closes over native code pointers so it cannot live in
1185
+ // the (de)serialized snapshot; it is installed here on every fresh realm.
1186
+ // The namespace is non-enumerable on globalThis (out of Object.keys/for-in);
1187
+ // its methods are ordinary properties so they remain discoverable.
1188
+ //
1189
+ // Consequence: the JS-side realm-reflection helpers (realmGlobal/realmOf)
1190
+ // and per-realm unhandled-rejection delivery require host_namespace. Realms
1191
+ // themselves do NOT — Context#create_realm + Realm#eval/call/attach work
1192
+ // without it (isolated realms driven from Ruby); only cross-realm wiring *in
1193
+ // JS* needs these helpers, which is why they live on the namespace.
1142
1194
  if (!st.host_namespace.empty()) {
1143
1195
  v8::Local<v8::String> ns_name;
1144
1196
  if (!v8::String::NewFromUtf8(st.isolate, st.host_namespace.c_str()).ToLocal(&ns_name))
1145
1197
  return false;
1146
1198
  auto ns = v8::Object::New(st.isolate);
1147
1199
  auto data = v8::External::New(st.isolate, pst);
1148
- auto drain_name = v8::String::NewFromUtf8Literal(st.isolate, "drainMicrotasks");
1149
- v8::Local<v8::Function> drain;
1150
- if (!v8::Function::New(context, v8_drain_microtasks_callback, data).ToLocal(&drain))
1151
- return false;
1152
- if (!ns->Set(context, drain_name, drain).FromMaybe(false)) return false;
1200
+ v8::Local<v8::Function> drain, rg, ro, onrej;
1201
+ // drainMicrotasks + realmGlobal read State via info.Data() (the External);
1202
+ // realmOf + onUnhandledRejection take none (onUnhandledRejection uses
1203
+ // isolate->GetData(0)), so they are created without data.
1204
+ if (!v8::Function::New(context, v8_drain_microtasks_callback, data).ToLocal(&drain)) return false;
1205
+ if (!v8::Function::New(context, v8_realm_global_callback, data).ToLocal(&rg)) return false;
1206
+ if (!v8::Function::New(context, v8_realm_of_callback).ToLocal(&ro)) return false;
1207
+ if (!v8::Function::New(context, v8_set_unhandled_rejection_handler).ToLocal(&onrej)) return false;
1208
+ if (!ns->Set(context, v8::String::NewFromUtf8Literal(st.isolate, "drainMicrotasks"), drain).FromMaybe(false)) return false;
1209
+ if (!ns->Set(context, v8::String::NewFromUtf8Literal(st.isolate, "realmGlobal"), rg).FromMaybe(false)) return false;
1210
+ if (!ns->Set(context, v8::String::NewFromUtf8Literal(st.isolate, "realmOf"), ro).FromMaybe(false)) return false;
1211
+ if (!ns->Set(context, v8::String::NewFromUtf8Literal(st.isolate, "onUnhandledRejection"), onrej).FromMaybe(false)) return false;
1153
1212
  if (!context->Global()->DefineOwnProperty(context, ns_name, ns, v8::DontEnum).FromMaybe(false))
1154
1213
  return false;
1155
1214
  }
1156
- // Install __mr_realmGlobal(id) on every realm (not gated by host_namespace):
1157
- // it returns realm `id`'s live globalThis so realms can reach each other
1158
- // (per-frame realms / iframes). Non-enumerable so it stays out of
1159
- // Object.keys(globalThis).
1160
- {
1161
- auto rg_name = v8::String::NewFromUtf8Literal(st.isolate, "__mr_realmGlobal");
1162
- auto rg_data = v8::External::New(st.isolate, pst);
1163
- v8::Local<v8::Function> rg;
1164
- if (!v8::Function::New(context, v8_realm_global_callback, rg_data).ToLocal(&rg))
1165
- return false;
1166
- if (!context->Global()->DefineOwnProperty(context, rg_name, rg, v8::DontEnum).FromMaybe(false))
1167
- return false;
1168
- // __mr_realmOf(fn) -> realm id where fn was created (for per-realm error
1169
- // attribution; the embedder dispatches error events on that realm).
1170
- auto ro_name = v8::String::NewFromUtf8Literal(st.isolate, "__mr_realmOf");
1171
- v8::Local<v8::Function> ro;
1172
- if (!v8::Function::New(context, v8_realm_of_callback).ToLocal(&ro))
1173
- return false;
1174
- if (!context->Global()->DefineOwnProperty(context, ro_name, ro, v8::DontEnum).FromMaybe(false))
1175
- return false;
1176
- }
1177
1215
  // Re-attach host functions onto the fresh global. Empty at boot; populated
1178
1216
  // when install_realm runs from v8_reset_realm. bind_callback reads st.context,
1179
1217
  // so point the members at the new realm for the duration of the loop, and
@@ -2999,6 +3037,10 @@ extern "C" void v8_reset_realm(State *pst)
2999
3037
  cur(st).scripts.clear();
3000
3038
  cur(st).modules.clear();
3001
3039
  cur(st).module_id_by_url.clear();
3040
+ // The realm-0 struct is reused across reset, so its onUnhandledRejection
3041
+ // handler points at a function in the now-discarded old realm. Drop it; the
3042
+ // embedder re-registers on the fresh realm (the namespace is reinstalled).
3043
+ cur(st).unhandled_rejection_handler.Reset();
3002
3044
  // Same rationale as the scripts/modules above: a not-yet-delivered rejection
3003
3045
  // recorded against the old realm would, after the swap, fire against the
3004
3046
  // fresh realm's globalThis (reset reuses the realm id). Drop them.
@@ -3067,6 +3109,7 @@ State::~State()
3067
3109
  Realm& r = *kv.second;
3068
3110
  r.modules.clear();
3069
3111
  r.scripts.clear();
3112
+ r.unhandled_rejection_handler.Reset();
3070
3113
  r.persistent_safe_context_function.Reset();
3071
3114
  r.persistent_safe_context.Reset();
3072
3115
  r.persistent_context.Reset();
@@ -4,6 +4,6 @@ module MiniRacerCsim
4
4
  # mini_racer-csim fork: upstream version + a fork revision segment.
5
5
  # 0.21.1.0 = first fork release on upstream 0.21.1; bump the 4th segment for
6
6
  # fork-only changes, reset it when rebasing onto a new upstream version.
7
- VERSION = "0.21.1.2"
7
+ VERSION = "0.21.1.4"
8
8
  LIBV8_NODE_VERSION = "~> 24.12.0.1"
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mini_racer-csim
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.21.1.2
4
+ version: 0.21.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-06-06 00:00:00.000000000 Z
12
+ date: 2026-06-08 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -110,11 +110,12 @@ dependencies:
110
110
  - !ruby/object:Gem::Version
111
111
  version: 24.12.0.1
112
112
  description: 'A private fork of mini_racer (minimal embedded V8 for Ruby) adding browser-level
113
- behavior used by capybara-simulated: the V8 ES Module API, cross-process bytecode
114
- caching, an opt-in host namespace, realm reset, and a batched module-graph loader
115
- with a URL module registry. These are niche browser-fidelity features; general users
116
- should use upstream mini_racer. The library is still required as `mini_racer` and
117
- exposes the `MiniRacer` module, so it stays a drop-in for code targeting mini_racer.'
113
+ behavior used by capybara-simulated: the V8 ES Module API, per-frame realms, realm
114
+ reset, cross-process bytecode caching, an opt-in host namespace, and a batched module-graph
115
+ loader with a URL module registry. These are niche browser-fidelity features; general
116
+ users should use upstream mini_racer. It loads under its own `mini_racer_csim` require
117
+ path and `MiniRacerCsim` namespace, so it never collides with upstream mini_racer
118
+ in the same bundle.'
118
119
  email:
119
120
  - ursm@ursm.jp
120
121
  executables: []