mini_racer-csim 0.21.1.2 → 0.21.1.3

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: b0e53581dc8063ed19c418dfc9ea450a7fc027adf5ea7732b32fb5dbabd6b0c7
4
+ data.tar.gz: 100100878b0966db7d6b0d8a38723993ac33eff9252bd6c48adccfcec65bf92c
5
5
  SHA512:
6
- metadata.gz: 365acff79cbaca1871707ea32e50d1d91dc7f15c23002e260449c1e1a553cb50c211fdb2406bc54b5bb40aa4e5848e915064891943b68dc8b4ca3ac02589a728
7
- data.tar.gz: 48dec93bf2a431bedc18c6c7d5b79890d833db748b525bc5f19c22a796adcd6a0e045acf43d5ac9226a5094dbacf4bc5a48a0214dab71983fd993f930d1ce465
6
+ metadata.gz: c9a3e14d28a170e7d60201c490e0c695c92684e88f77935c733812206cd7a158d07cd96b76cb0bbfdc9a2f9804c3add1a33d189c47ef2b304873980448ce3ab2
7
+ data.tar.gz: c74e8ae6b9acac62be7a1099e7464564f08ac3e485f4e2f992b879d27d91c636d412c17506e0e972646e69780cc81dfd70e3de3bdbf961b4c1e3bcf19c808e84
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ - 0.21.1.3 - 08-06-2026
2
+ - **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
3
+ - `__mr_realmGlobal(id)` → `<host_namespace>.realmGlobal(id)`
4
+ - `__mr_realmOf(fn)` → `<host_namespace>.realmOf(fn)`
5
+ - the embedder-defined `globalThis.__mr_emitUnhandledRejection` hook → a `<host_namespace>.onUnhandledRejection(fn)` registration method (the engine stores the handler per realm and calls it)
6
+ - **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
7
+
1
8
  - 0.21.1.2 - 07-06-2026
2
9
  - **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
10
  - 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
 
@@ -134,6 +134,11 @@ struct Realm
134
134
  // graph_resolve_callback can resolve imports from the pre-walked graph
135
135
  // with zero Ruby round-trips. Null otherwise.
136
136
  struct GraphLoad *active_graph = nullptr;
137
+ // Embedder-registered handler for promises that reject with no handler in
138
+ // this realm, set via <host_namespace>.onUnhandledRejection(fn). Called by
139
+ // notify_unhandled_rejections with (reason, promise). Global so its dtor
140
+ // releases the function when the realm is disposed; reset on reset_realm.
141
+ v8::Global<v8::Function> unhandled_rejection_handler;
137
142
  };
138
143
 
139
144
  struct State
@@ -768,13 +773,14 @@ void v8_drain_microtasks_callback(const v8::FunctionCallbackInfo<v8::Value>& inf
768
773
  info.GetReturnValue().SetUndefined();
769
774
  }
770
775
 
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.
776
+ // <host_namespace>.realmGlobal(id): returns the globalThis of realm `id` as a
777
+ // LIVE V8 object in the calling realm (same isolate, not a copy), or undefined
778
+ // for an unknown realm. Hung off the host namespace (opt-in via
779
+ // Context.new(host_namespace:)) rather than a bare global, so globalThis stays
780
+ // unpolluted. Because all realms share one security token (see install_realm),
781
+ // the caller can read/write the returned global's properties this is how csim
782
+ // wires frames[i] / iframe.contentWindow to the right realm. Cross-realm object
783
+ // identity holds: mutating a property here is visible in the target realm.
778
784
  void v8_realm_global_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
779
785
  {
780
786
  auto isolate = info.GetIsolate();
@@ -815,13 +821,13 @@ static int32_t realm_id_of_context(v8::Local<v8::Context> ctx)
815
821
  return v.As<v8::Int32>()->Value();
816
822
  }
817
823
 
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.
824
+ // <host_namespace>.realmOf(fn): returns the realm id where `fn` (any
825
+ // object/function) was created — its [[Realm]] / creation context — or
826
+ // undefined if unknown. This is the realm WebIDL's "invoke a callback function"
827
+ // reports errors against (e.g. a setTimeout callback's uncaught throw is
828
+ // reported on the realm that *created* the callback, not the scheduling realm
829
+ // nor the thrown Error's realm). csim uses it to dispatch an ErrorEvent on
830
+ // <ns>.realmGlobal(<ns>.realmOf(cb)) from its own per-callback try/catch.
825
831
  void v8_realm_of_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
826
832
  {
827
833
  auto isolate = info.GetIsolate();
@@ -842,6 +848,27 @@ void v8_realm_of_callback(const v8::FunctionCallbackInfo<v8::Value>& info)
842
848
  info.GetReturnValue().Set(v8::Integer::New(isolate, rid));
843
849
  }
844
850
 
851
+ // <host_namespace>.onUnhandledRejection(fn): register fn as the calling realm's
852
+ // handler for promises that reject with no handler. notify_unhandled_rejections
853
+ // calls it with (reason, promise) at the next microtask checkpoint, entered in
854
+ // the rejecting promise's realm (HTML notify-rejected-promises). The handler is
855
+ // stored per realm (keyed by the calling context's realm id) instead of as a
856
+ // bare globalThis property, so globalThis stays unpolluted. Passing a non-
857
+ // function clears the handler.
858
+ void v8_set_unhandled_rejection_handler(const v8::FunctionCallbackInfo<v8::Value>& info)
859
+ {
860
+ auto isolate = info.GetIsolate();
861
+ State& st = *static_cast<State*>(isolate->GetData(0));
862
+ int32_t rid = realm_id_of_context(isolate->GetCurrentContext());
863
+ auto it = st.realms.find(rid);
864
+ if (it == st.realms.end())
865
+ return; // unknown realm: no-op
866
+ if (info.Length() >= 1 && info[0]->IsFunction())
867
+ it->second->unhandled_rejection_handler.Reset(isolate, info[0].As<v8::Function>());
868
+ else
869
+ it->second->unhandled_rejection_handler.Reset();
870
+ }
871
+
845
872
  // V8 calls this when a promise's rejection state changes. We implement the
846
873
  // HTML "notify rejected promises" bookkeeping: queue promises that reject with
847
874
  // no handler (tagged with the realm they were created in), and drop them again
@@ -1041,12 +1068,13 @@ static void clear_realm_locals(State& st)
1041
1068
  }
1042
1069
 
1043
1070
  // 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.
1071
+ // since the last checkpoint, enter its realm and call that realm's handler
1072
+ // (registered via <host_namespace>.onUnhandledRejection) with (reason, promise)
1073
+ // if one was set (csim turns that into a PromiseRejectionEvent so
1074
+ // addEventListener('unhandledrejection') fires natively in the right realm). The
1075
+ // list is snapshotted and cleared first so rejections triggered by a handler
1076
+ // queue for the next checkpoint instead of looping. A realm disposed in the
1077
+ // meantime is skipped.
1050
1078
  static void notify_unhandled_rejections(State& st)
1051
1079
  {
1052
1080
  if (st.pending_rejections.empty())
@@ -1067,18 +1095,17 @@ static void notify_unhandled_rejections(State& st)
1067
1095
  auto context = v8::Local<v8::Context>::New(st.isolate, it->second->persistent_context);
1068
1096
  if (context.IsEmpty())
1069
1097
  continue;
1098
+ if (it->second->unhandled_rejection_handler.IsEmpty())
1099
+ continue; // realm registered no onUnhandledRejection handler
1070
1100
  st.active_realm_id = pr.second;
1071
1101
  restore_realm_locals(st);
1072
1102
  v8::Context::Scope context_scope(context);
1073
1103
  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;
1104
+ auto hook = v8::Local<v8::Function>::New(st.isolate, it->second->unhandled_rejection_handler);
1078
1105
  auto promise = v8::Local<v8::Promise>::New(st.isolate, pr.first);
1079
1106
  v8::TryCatch try_catch(st.isolate); // swallow errors from the handler itself
1080
1107
  v8::Local<v8::Value> args[2] = { promise->Result(), promise };
1081
- (void)hook.As<v8::Function>()->Call(context, global, 2, args);
1108
+ (void)hook->Call(context, global, 2, args);
1082
1109
  }
1083
1110
  st.active_realm_id = prev;
1084
1111
  st.context = saved_context;
@@ -1133,47 +1160,41 @@ static bool install_realm(State& st)
1133
1160
  v8::Context::Scope context_scope(context);
1134
1161
  // If the embedder opted in via Context.new(host_namespace:), install a
1135
1162
  // 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.
1163
+ // `Bun`) under that global name and hang EVERY native JS helper off it:
1164
+ // drainMicrotasks(), realmGlobal(id), realmOf(fn), onUnhandledRejection(fn).
1165
+ // Keeping them on one opt-in object (rather than bare __mr_* globals) means
1166
+ // globalThis pollution is decided once, by the opt-in — not relitigated per
1167
+ // feature. The object closes over native code pointers so it cannot live in
1168
+ // the (de)serialized snapshot; it is installed here on every fresh realm.
1169
+ // The namespace is non-enumerable on globalThis (out of Object.keys/for-in);
1170
+ // its methods are ordinary properties so they remain discoverable.
1171
+ //
1172
+ // Consequence: the JS-side realm-reflection helpers (realmGlobal/realmOf)
1173
+ // and per-realm unhandled-rejection delivery require host_namespace. Realms
1174
+ // themselves do NOT — Context#create_realm + Realm#eval/call/attach work
1175
+ // without it (isolated realms driven from Ruby); only cross-realm wiring *in
1176
+ // JS* needs these helpers, which is why they live on the namespace.
1142
1177
  if (!st.host_namespace.empty()) {
1143
1178
  v8::Local<v8::String> ns_name;
1144
1179
  if (!v8::String::NewFromUtf8(st.isolate, st.host_namespace.c_str()).ToLocal(&ns_name))
1145
1180
  return false;
1146
1181
  auto ns = v8::Object::New(st.isolate);
1147
1182
  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;
1183
+ v8::Local<v8::Function> drain, rg, ro, onrej;
1184
+ // drainMicrotasks + realmGlobal read State via info.Data() (the External);
1185
+ // realmOf + onUnhandledRejection take none (onUnhandledRejection uses
1186
+ // isolate->GetData(0)), so they are created without data.
1187
+ if (!v8::Function::New(context, v8_drain_microtasks_callback, data).ToLocal(&drain)) return false;
1188
+ if (!v8::Function::New(context, v8_realm_global_callback, data).ToLocal(&rg)) return false;
1189
+ if (!v8::Function::New(context, v8_realm_of_callback).ToLocal(&ro)) return false;
1190
+ if (!v8::Function::New(context, v8_set_unhandled_rejection_handler).ToLocal(&onrej)) return false;
1191
+ if (!ns->Set(context, v8::String::NewFromUtf8Literal(st.isolate, "drainMicrotasks"), drain).FromMaybe(false)) return false;
1192
+ if (!ns->Set(context, v8::String::NewFromUtf8Literal(st.isolate, "realmGlobal"), rg).FromMaybe(false)) return false;
1193
+ if (!ns->Set(context, v8::String::NewFromUtf8Literal(st.isolate, "realmOf"), ro).FromMaybe(false)) return false;
1194
+ if (!ns->Set(context, v8::String::NewFromUtf8Literal(st.isolate, "onUnhandledRejection"), onrej).FromMaybe(false)) return false;
1153
1195
  if (!context->Global()->DefineOwnProperty(context, ns_name, ns, v8::DontEnum).FromMaybe(false))
1154
1196
  return false;
1155
1197
  }
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
1198
  // Re-attach host functions onto the fresh global. Empty at boot; populated
1178
1199
  // when install_realm runs from v8_reset_realm. bind_callback reads st.context,
1179
1200
  // so point the members at the new realm for the duration of the loop, and
@@ -2999,6 +3020,10 @@ extern "C" void v8_reset_realm(State *pst)
2999
3020
  cur(st).scripts.clear();
3000
3021
  cur(st).modules.clear();
3001
3022
  cur(st).module_id_by_url.clear();
3023
+ // The realm-0 struct is reused across reset, so its onUnhandledRejection
3024
+ // handler points at a function in the now-discarded old realm. Drop it; the
3025
+ // embedder re-registers on the fresh realm (the namespace is reinstalled).
3026
+ cur(st).unhandled_rejection_handler.Reset();
3002
3027
  // Same rationale as the scripts/modules above: a not-yet-delivered rejection
3003
3028
  // recorded against the old realm would, after the swap, fire against the
3004
3029
  // fresh realm's globalThis (reset reuses the realm id). Drop them.
@@ -3067,6 +3092,7 @@ State::~State()
3067
3092
  Realm& r = *kv.second;
3068
3093
  r.modules.clear();
3069
3094
  r.scripts.clear();
3095
+ r.unhandled_rejection_handler.Reset();
3070
3096
  r.persistent_safe_context_function.Reset();
3071
3097
  r.persistent_safe_context.Reset();
3072
3098
  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.3"
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.3
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: []