rusty_racer 0.1.2 → 0.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: b0b742605170382bce639eaa6c8acd1917f6ed4e6f8a5d4f83d40701ffb14b25
4
- data.tar.gz: ce873858c2a28e9903ca9abec1cc72e061f3561f1cf2c8d5b0ff3694d613ec55
3
+ metadata.gz: 329c875cb612dccca54a8c70302d26ad0a38b8a1821733aa6eac9c4795e77d8b
4
+ data.tar.gz: 4d2665db69e108f6ef37fa9d4f0ae4bc3224ab1756e911abec76cfad558f8bd4
5
5
  SHA512:
6
- metadata.gz: 1e3960b00b14c62813d731ee85c8f41cca8f04a0ff7657446f205dda2f24c5e11fd07b00da000b1d7be503bb00f083cf389f3736fac8d6344343a6ec26a3a11e
7
- data.tar.gz: 3ee0d6217b064b70362a7e2475e5db840a0b613922f91a418d3548464066ea1824d1c073870e5d9d6da4cdc351fa481e490d667520123675f2b7d730a926d23e
6
+ metadata.gz: f95ae2f448af2ade88af369bea7c0b41c500e4028aeed04e34574e148c7d5f460ac068804bc49b409ab55ad352d063fcb0269064b27b1cf3a2a0453a8e245e2f
7
+ data.tar.gz: 4b853defb2dbde4c0455b9eefb5b6ba9d40002e94953f5d1540c4035087e8128c6b6df843463429b0e5cbcb404a0d1a1c625d13a24fee301a300eaf50dc9ad84
data/README.md CHANGED
@@ -21,6 +21,11 @@ Embed [V8](https://v8.dev/) in Ruby, built on [rusty_v8](https://crates.io/crate
21
21
  - **Drop-in [ExecJS](#execjs) runtime** — any ExecJS consumer switches with no
22
22
  code change.
23
23
  - **Snapshots, realms (`Context`s), host callbacks, and a bytecode cache.**
24
+ - **Resource limits on both axes** — a `timeout_ms` (time) and a `memory_limit`
25
+ (space), each catchable: a runaway script fails just its own `eval`, leaving
26
+ the isolate usable, instead of aborting the process. A heap runaway is caught
27
+ even with no explicit `memory_limit` — V8's default ceiling raises instead of
28
+ aborting.
24
29
  - **Precompiled gems** bundle V8 for Linux/macOS × Ruby 3.3–4.0 — no V8 build,
25
30
  no Rust toolchain.
26
31
 
@@ -33,9 +38,10 @@ dynamic import** (mini_racer is eval/classic-script oriented); **richer
33
38
  marshalling** (the types above round-trip natively instead of through a
34
39
  JSON-shaped projection); and **in-thread execution** with no per-op thread hop,
35
40
  which is faster for overhead-dominated workloads (lots of tiny `eval`/`call`) and
36
- at parity once the per-op JS work dominates. It is also younger and
37
- **experimental**fewer miles, no Windows yet, no per-isolate memory cap. Parity
38
- with mini_racer is not a goal; the overlap is convergent evolution, not a port.
41
+ at parity once the per-op JS work dominates. Both axes of resource limiting are
42
+ covereda `timeout_ms` (time) and a `memory_limit` (space), each catchable. It
43
+ is also younger and **experimental** fewer miles, no Windows yet. Parity with
44
+ mini_racer is not a goal; the overlap is convergent evolution, not a port.
39
45
 
40
46
  ## What it can do
41
47
 
@@ -83,6 +89,32 @@ app.namespace["r"] # => 42
83
89
 
84
90
  Classic `<script>`s work the same way: `ctx.compile("1 + 1").run` # => 2.
85
91
 
92
+ ### Resource limits
93
+
94
+ An isolate can cap untrusted code on both axes. Each limit terminates the
95
+ offending op and raises a catchable error — the isolate stays usable afterward,
96
+ so one runaway script fails just its own `eval`, not the whole process.
97
+
98
+ ```ruby
99
+ # Time: timeout_ms caps each eval/call (per-call override on Context#eval).
100
+ iso = RustyRacer::Isolate.new(timeout_ms: 1000)
101
+ iso.context.eval("for (;;) {}") # raises RustyRacer::ScriptTerminatedError
102
+
103
+ # Space: memory_limit caps the V8 heap in bytes (a soft limit, enforced at GC
104
+ # granularity). The isolate forces a GC and resets the ceiling on recovery.
105
+ iso = RustyRacer::Isolate.new(memory_limit: 64 * 1024 * 1024)
106
+ iso.context.eval("a = []; for (;;) a.push(new Array(1e6))")
107
+ # raises RustyRacer::V8OutOfMemoryError
108
+ iso.context.eval("1 + 1") # => 2 (still usable)
109
+ ```
110
+
111
+ Even without an explicit `memory_limit`, a heap runaway raises
112
+ `V8OutOfMemoryError` against V8's own default ceiling (~2 GB on 64-bit) rather
113
+ than aborting the process — pass `memory_limit:` for a tighter bound. (One
114
+ caveat: if the process's available memory — e.g. a container cgroup limit — sits
115
+ below the active ceiling, the OS may kill the process before V8's callback
116
+ fires; set an explicit `memory_limit` under that bound to keep it catchable.)
117
+
86
118
  ### Bytecode caching
87
119
 
88
120
  V8 compiles lazily: the top level up front, each function body on first call.
@@ -4,7 +4,7 @@
4
4
  # libv8-rusty needed under the cibuildgem native-per-platform model).
5
5
  [package]
6
6
  name = "rusty_racer"
7
- version = "0.1.2"
7
+ version = "0.1.4"
8
8
  edition = "2021"
9
9
  publish = false
10
10
 
@@ -186,7 +186,63 @@ enum VmError {
186
186
  message: String,
187
187
  backtrace: Vec<String>,
188
188
  },
189
- Terminated, // watchdog/stop -> RustyRacer::ScriptTerminatedError
189
+ Terminated, // watchdog/stop -> RustyRacer::ScriptTerminatedError
190
+ OutOfMemory, // memory_limit hit -> RustyRacer::V8OutOfMemoryError
191
+ }
192
+
193
+ // V8's near-heap-limit callback (registered on EVERY isolate). V8 calls this,
194
+ // synchronously on the owner thread, when a GC still leaves the heap about to
195
+ // exceed the current ceiling — i.e. the script is running away on memory. That
196
+ // ceiling is the configured memory_limit when one was set, otherwise V8's own
197
+ // platform-derived default (the default-protection path), so a runaway raises a
198
+ // catchable error either way instead of aborting the process. `data` is the
199
+ // isolate ptr we registered (Core.iso_ptr). We flag the isolate and terminate the
200
+ // running JS so it unwinds with that catchable error. The return value becomes
201
+ // V8's new ceiling: hand it a DOUBLED limit so the unwind itself (and any pending
202
+ // finalizers) has room to allocate without tripping a hard OOM abort mid-unwind.
203
+ // Core::run, once the op has unwound, forces a GC to reclaim and resets the
204
+ // ceiling — see the OOM recovery there. The bump is a no-op-after-the-fact:
205
+ // doubling here, GC + reset after, so the limit keeps protecting later ops.
206
+ unsafe extern "C" fn near_heap_limit_cb(data: *mut c_void, current_heap_limit: usize, initial: usize) -> usize {
207
+ let isolate = unsafe { &mut *(data as *mut v8::Isolate) };
208
+ // get_slot_mut (not istate!): this runs as an extern "C" callback from V8's
209
+ // C++ allocator, where a panic would unwind across the FFI boundary. The slot
210
+ // is always present once an op can run (set in Isolate::new before any JS), but
211
+ // skip flagging rather than .expect()-panic in the impossible absent case.
212
+ // The closure releases the &mut borrow before terminate_execution (&self).
213
+ // Also record V8's `initial` ceiling so recovery can restore it when no
214
+ // explicit memory_limit was set (see oom_initial_limit).
215
+ let flagged = isolate
216
+ .get_slot_mut::<IsolateState>()
217
+ .map(|s| {
218
+ s.oom_fired = true;
219
+ s.oom_initial_limit = initial;
220
+ })
221
+ .is_some();
222
+ if flagged {
223
+ isolate.terminate_execution();
224
+ }
225
+ current_heap_limit.saturating_mul(2)
226
+ }
227
+
228
+ // After an OOM the running op's outcome is a bare Terminated (the terminate the
229
+ // callback fired). Swap it for OutOfMemory so it surfaces as V8OutOfMemoryError,
230
+ // preserving the reply's variant (the caller dispatches on it). Only the error
231
+ // arm changes; a Terminated from a real timeout/stop never reaches here because
232
+ // this runs only when oom_fired was set.
233
+ fn relabel_oom(reply: VmReply) -> VmReply {
234
+ fn fix<T>(r: Result<T, VmError>) -> Result<T, VmError> {
235
+ match r {
236
+ Err(VmError::Terminated) => Err(VmError::OutOfMemory),
237
+ other => other,
238
+ }
239
+ }
240
+ match reply {
241
+ VmReply::Done(r) => VmReply::Done(fix(r)),
242
+ VmReply::ModuleCompiled(r) => VmReply::ModuleCompiled(fix(r)),
243
+ VmReply::ScriptCompiled(r) => VmReply::ScriptCompiled(fix(r)),
244
+ VmReply::CodeCache(r) => VmReply::CodeCache(fix(r)),
245
+ }
190
246
  }
191
247
 
192
248
  // ---------------------------------------------------------------------------
@@ -522,6 +578,18 @@ struct IsolateState {
522
578
  instantiate_resolve: Option<RootedProc>,
523
579
  instantiate_resolve_err: Option<BoxValue<Exception>>,
524
580
  watchdog: Arc<WatchdogShared>,
581
+ // Set by near_heap_limit_cb when the heap ceiling is hit: it terminates the
582
+ // running JS, and Core::run reads this after the op to relabel the terminate as
583
+ // OutOfMemory and recover the heap (GC + reset the ceiling).
584
+ // Plain bool (no atomic): the callback fires synchronously on the owner thread,
585
+ // never concurrently with the bracket that reads it.
586
+ oom_fired: bool,
587
+ // V8's original heap ceiling, captured from the callback's `initial` argument
588
+ // when it fires. Recovery resets the ceiling to memory_limit when one was set,
589
+ // but with no explicit limit (the default-protection path) the ceiling IS V8's
590
+ // platform-derived default, whose value we don't otherwise know — so we restore
591
+ // to this captured initial instead. 0 until the callback has fired at least once.
592
+ oom_initial_limit: usize,
525
593
  }
526
594
 
527
595
  impl IsolateState {
@@ -543,6 +611,8 @@ impl IsolateState {
543
611
  instantiate_resolve: None,
544
612
  instantiate_resolve_err: None,
545
613
  watchdog: WatchdogShared::new(),
614
+ oom_fired: false,
615
+ oom_initial_limit: 0,
546
616
  }
547
617
  }
548
618
  }
@@ -1033,6 +1103,13 @@ struct Core {
1033
1103
  // Default per-eval/call timeout (ms); 0 = none. eval(timeout_ms:)'s explicit
1034
1104
  // value overrides it. Guards against an in-V8 infinite loop without a watchdog.
1035
1105
  default_timeout_ms: u64,
1106
+ // Per-isolate heap ceiling (bytes); 0 = V8's default ceiling. When set, the
1107
+ // isolate is created with this as V8's max heap; near_heap_limit_cb is registered
1108
+ // either way (against this ceiling when set, V8's default otherwise), so a runaway
1109
+ // is always catchable rather than a process abort. Core::run's OOM recovery resets
1110
+ // the ceiling after each OOM (to this when set, else V8's captured default — see
1111
+ // oom_initial_limit). Space-axis twin of default_timeout_ms.
1112
+ memory_limit: usize,
1036
1113
  // Set by Context#dynamic_import_resolver=; called for a JS import() to map
1037
1114
  // (specifier, referrer) to an already-loaded Module. GC-rooted like procs.
1038
1115
  dynamic_import_resolver: Mutex<Option<RootedProc>>,
@@ -1457,6 +1534,9 @@ fn build_snapshot(code: &str, base: Option<Vec<u8>>, warmup: bool) -> Result<Vec
1457
1534
  VmError::Parse(m) | VmError::Runtime(m) => m,
1458
1535
  VmError::JsError { message, .. } => message,
1459
1536
  VmError::Terminated => "snapshot code was terminated".to_string(),
1537
+ // Unreachable: the snapshot-creator is a separate isolate
1538
+ // that never registers near_heap_limit_cb.
1539
+ VmError::OutOfMemory => "snapshot code ran out of memory".to_string(),
1460
1540
  });
1461
1541
  }
1462
1542
  }
@@ -1492,16 +1572,24 @@ impl Isolate {
1492
1572
  host_namespace: Option<String>,
1493
1573
  snapshot: Option<magnus::typed_data::Obj<Snapshot>>,
1494
1574
  timeout_ms: u64,
1575
+ memory_limit: usize,
1495
1576
  explicit_microtasks: bool,
1496
1577
  ) -> Result<Self, Error> {
1497
1578
  init_v8();
1498
1579
  // A snapshot blob bakes globalThis state in: the first Context::new (in
1499
1580
  // new_realm below) deserializes that default context for free.
1500
1581
  let snapshot_bytes = snapshot.map(|s| s.blob.borrow().clone());
1501
- let create_params = match snapshot_bytes {
1582
+ let mut create_params = match snapshot_bytes {
1502
1583
  Some(bytes) => v8::CreateParams::default().snapshot_blob(v8::StartupData::from(bytes)),
1503
1584
  None => Default::default(),
1504
1585
  };
1586
+ // Cap V8's heap at the configured limit so its near-heap-limit callback
1587
+ // fires as the script approaches it (initial 0 = V8's default initial heap).
1588
+ // With no explicit limit the callback is still registered below, against
1589
+ // V8's own default ceiling, so a runaway raises instead of aborting.
1590
+ if memory_limit > 0 {
1591
+ create_params = create_params.heap_limits(0, memory_limit);
1592
+ }
1505
1593
  let mut isolate = v8::Isolate::new(create_params);
1506
1594
  // Always Explicit at the V8 level; the binding performs the kAuto
1507
1595
  // end-of-script drain itself (auto_drain) so it stays inside the
@@ -1541,6 +1629,12 @@ impl Isolate {
1541
1629
  // registry moves only the 8-byte pointer; the boxed OwnedIsolate stays put.
1542
1630
  let mut boxed = Box::new(isolate);
1543
1631
  let iso_ptr = IsoPtr(&mut **boxed as *mut v8::Isolate);
1632
+ // Arm the heap-limit callback now that iso_ptr is stable: the callback's
1633
+ // data IS this ptr (it reads the slot's oom_fired and terminates through
1634
+ // it), and Core::run resets the ceiling through the same ptr on recovery.
1635
+ // Registered unconditionally — with memory_limit it guards that ceiling,
1636
+ // without one it guards V8's default ceiling (catchable, not a process abort).
1637
+ boxed.add_near_heap_limit_callback(near_heap_limit_cb, iso_ptr.0 as *mut c_void);
1544
1638
  let iso_id = NEXT_ISOLATE_ID.fetch_add(1, Ordering::SeqCst);
1545
1639
  isolates().lock().unwrap().insert(iso_id, SendIso(boxed));
1546
1640
  // Root the owner Thread VALUE so its address can't be reused while this
@@ -1559,6 +1653,7 @@ impl Isolate {
1559
1653
  depth: std::sync::atomic::AtomicU32::new(0),
1560
1654
  procs: Mutex::new(ProcTable::default()),
1561
1655
  default_timeout_ms: timeout_ms,
1656
+ memory_limit,
1562
1657
  dynamic_import_resolver: Mutex::new(None),
1563
1658
  watchdog,
1564
1659
  watchdog_join: Mutex::new(Some(watchdog_join)),
@@ -1654,11 +1749,44 @@ impl Core {
1654
1749
  self.scan_start_field.load(Ordering::Relaxed),
1655
1750
  stack_top,
1656
1751
  );
1657
- let reply = std::panic::catch_unwind(AssertUnwindSafe(|| {
1752
+ let mut reply = std::panic::catch_unwind(AssertUnwindSafe(|| {
1658
1753
  v8::scope!(let scope, unsafe { &mut *iso });
1659
1754
  service_request(scope, request, true)
1660
1755
  }))
1661
1756
  .ok();
1757
+ // OOM recovery. The near-heap-limit callback bumped the ceiling and
1758
+ // terminated the op so it could unwind; the scope is closed and JS has
1759
+ // stopped now. Reclaim the runaway allocation (a forced GC) and reset
1760
+ // the ceiling so the limit keeps protecting later ops, then relabel the
1761
+ // terminate as OutOfMemory. watchdog_fired stays false for an OOM, so
1762
+ // the request's end-sweep left the terminate flag set — cancel it here.
1763
+ if std::mem::take(&mut istate!(unsafe { &mut *iso }).oom_fired) {
1764
+ let iso_ref = unsafe { &mut *iso };
1765
+ // The ceiling to restore: the configured memory_limit when one was
1766
+ // set, otherwise V8's default ceiling, which the callback captured
1767
+ // in oom_initial_limit (we don't otherwise know its value).
1768
+ let restore_to = if self.memory_limit > 0 {
1769
+ self.memory_limit
1770
+ } else {
1771
+ istate!(iso_ref).oom_initial_limit
1772
+ };
1773
+ // Reclaim the runaway allocation, then reset the ceiling from the
1774
+ // doubled bump back to restore_to (V8 clamps it no lower than the
1775
+ // live heap — a genuinely-retained set above the limit necessarily
1776
+ // loosens it, inherent to recovering the isolate rather than
1777
+ // discarding it), and re-arm the callback for the next op.
1778
+ iso_ref.low_memory_notification();
1779
+ iso_ref.remove_near_heap_limit_callback(near_heap_limit_cb, restore_to);
1780
+ iso_ref.add_near_heap_limit_callback(near_heap_limit_cb, iso as *mut c_void);
1781
+ // Clear the terminate the OOM set (the request end-sweep skips it —
1782
+ // that only sweeps watchdog_fired). Do this AFTER the GC: the forced
1783
+ // GC above runs with the callback still armed, so a still-huge live
1784
+ // set can re-fire it mid-GC, re-setting both terminate and oom_fired;
1785
+ // clearing both here keeps either from leaking into the next op.
1786
+ iso_ref.cancel_terminate_execution();
1787
+ istate!(iso_ref).oom_fired = false;
1788
+ reply = reply.map(relabel_oom);
1789
+ }
1662
1790
  unsafe { (*iso).exit() };
1663
1791
  reply
1664
1792
  } else {
@@ -2070,6 +2198,12 @@ impl Core {
2070
2198
  st.scripts = ScriptReg::default();
2071
2199
  st.instantiate_resolve = None;
2072
2200
  }
2201
+ // Unregister the near-heap-limit callback before disposal: dropping the box
2202
+ // runs V8's teardown GC, which could otherwise re-invoke near_heap_limit_cb
2203
+ // (touching the just-reset slot of an isolate being destroyed). The watchdog
2204
+ // is already stopped above; this closes the matching space-axis hole.
2205
+ // Registered unconditionally (default protection), so always remove it.
2206
+ unsafe { &mut *self.iso_ptr.0 }.remove_near_heap_limit_callback(near_heap_limit_cb, 0);
2073
2207
  // Remove (and drop) the OwnedIsolate — V8 disposal runs here — AFTER the
2074
2208
  // watchdog joined and the Globals were cleared, while the isolate is
2075
2209
  // entered (above). Drop outside the lock so V8 teardown can't deadlock on
@@ -2524,6 +2658,10 @@ fn vm_err(ruby: &Ruby, e: VmError) -> Error {
2524
2658
  err_class(ruby, "ScriptTerminatedError"),
2525
2659
  "JavaScript was terminated (timeout or stop)",
2526
2660
  ),
2661
+ VmError::OutOfMemory => Error::new(
2662
+ err_class(ruby, "V8OutOfMemoryError"),
2663
+ "JavaScript exceeded the isolate memory_limit",
2664
+ ),
2527
2665
  }
2528
2666
  }
2529
2667
 
@@ -2601,7 +2739,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
2601
2739
  // The isolate (VM) + its isolate-level ops; hands out Contexts.
2602
2740
  let isolate = module.define_class("Isolate", ruby.class_object())?;
2603
2741
  // keyword-arg wrapper Isolate.new(snapshot:, ...) lives in lib/rusty_racer.rb
2604
- isolate.define_singleton_method("_new", function!(Isolate::new, 4))?;
2742
+ isolate.define_singleton_method("_new", function!(Isolate::new, 5))?;
2605
2743
  isolate.define_method("context", method!(Isolate::context, 0))?;
2606
2744
  isolate.define_method("create_context", method!(Isolate::create_context, 0))?;
2607
2745
  isolate.define_method("terminate", method!(Isolate::terminate, 0))?;
@@ -185,6 +185,7 @@ pub(crate) fn run_js_bracketed(
185
185
  && ran_js
186
186
  && !fired
187
187
  && matches!(outcome, Err(VmError::Terminated))
188
+ && !istate!(scope).oom_fired
188
189
  {
189
190
  report_watchdog_anomaly(scope, label, watchdog, timeout_ms, started.elapsed());
190
191
  }
@@ -193,6 +194,15 @@ pub(crate) fn run_js_bracketed(
193
194
  if ran_js {
194
195
  outcome = Err(VmError::Terminated);
195
196
  }
197
+ } else if ran_js && istate!(scope).oom_fired {
198
+ // The memory_limit callback fired TerminateExecution during this op. body
199
+ // may not have noticed it — a microtask/TLA drain that was interrupted
200
+ // leaves a pending promise and returns Ok (auto_drain just stops at the
201
+ // terminate) — so force the terminated outcome rather than let an OOM
202
+ // surface as a bogus success. Core::run relabels it to OutOfMemory and
203
+ // recovers the heap. (No watchdog_fired here: the OOM terminate is swept by
204
+ // Core::run's recovery, not the request end-sweep.)
205
+ outcome = Err(VmError::Terminated);
196
206
  }
197
207
  outcome
198
208
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RustyRacer
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
data/lib/rusty_racer.rb CHANGED
@@ -27,6 +27,11 @@ module RustyRacer
27
27
  class ParseError < EvalError; end
28
28
  class RuntimeError < EvalError; end
29
29
  class ScriptTerminatedError < EvalError; end
30
+ # Raised when JS allocation exceeds the isolate's heap ceiling (the configured
31
+ # memory_limit, or V8's default ceiling when none was set). Catchable like any
32
+ # eval error — a runaway script fails its own eval instead of aborting the
33
+ # process. The space-axis twin of ScriptTerminatedError (the time axis).
34
+ class V8OutOfMemoryError < EvalError; end
30
35
  class SnapshotError < Error; end
31
36
  class PlatformAlreadyInitialized < Error; end
32
37
 
@@ -41,16 +46,29 @@ module RustyRacer
41
46
  # Keyword-arg constructor over the positional Rust primitive. A snapshot
42
47
  # (RustyRacer::Snapshot) boots the isolate with its baked-in state;
43
48
  # timeout_ms caps each eval/call (0 = no limit) against in-V8 infinite
44
- # loops. microtasks mirrors V8's kAuto/kExplicit: :auto (default) drains
49
+ # loops. memory_limit caps the V8 heap in bytes: a script that exceeds the
50
+ # ceiling is terminated and raises V8OutOfMemoryError rather than aborting
51
+ # the process, and the isolate stays usable afterward. 0 (the default) does
52
+ # NOT disable this — it leaves V8's own platform-derived default ceiling
53
+ # (typically ~2 GB on 64-bit) in place, so a runaway is still catchable
54
+ # instead of a fatal abort; pass a smaller value for a tighter bound. It is
55
+ # a soft limit — V8 enforces it at GC granularity, so usage may briefly
56
+ # overshoot, and an explicit limit must comfortably exceed the isolate's
57
+ # baseline (and any snapshot's baked-in heap), since it is only armed once
58
+ # the isolate has booted. Caveat: if the process's available memory (e.g. a
59
+ # container cgroup limit) is below the active ceiling, the OS may kill the
60
+ # process before V8's callback fires — set an explicit memory_limit under
61
+ # that bound to keep the error catchable.
62
+ # microtasks mirrors V8's kAuto/kExplicit: :auto (default) drains
45
63
  # the microtask queue when the outermost eval/call/run/evaluate completes
46
64
  # (the standard embedder contract); :explicit drains only on
47
65
  # #perform_microtask_checkpoint.
48
- def self.new(host_namespace: nil, snapshot: nil, timeout_ms: 0, microtasks: :auto)
66
+ def self.new(host_namespace: nil, snapshot: nil, timeout_ms: 0, memory_limit: 0, microtasks: :auto)
49
67
  unless %i[auto explicit].include?(microtasks)
50
68
  raise ArgumentError, "microtasks must be :auto or :explicit, got #{microtasks.inspect}"
51
69
  end
52
70
 
53
- _new(host_namespace, snapshot, timeout_ms, microtasks == :explicit)
71
+ _new(host_namespace, snapshot, timeout_ms, memory_limit, microtasks == :explicit)
54
72
  end
55
73
 
56
74
  # ->(specifier, referrer_url, context) { Module } for JS import(). |context|
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rusty_racer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keita Urashima
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-13 00:00:00.000000000 Z
11
+ date: 2026-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rb_sys
@@ -54,7 +54,7 @@ metadata:
54
54
  source_code_uri: https://github.com/ursm/rusty_racer
55
55
  bug_tracker_uri: https://github.com/ursm/rusty_racer/issues
56
56
  rubygems_mfa_required: 'true'
57
- post_install_message:
57
+ post_install_message:
58
58
  rdoc_options: []
59
59
  require_paths:
60
60
  - lib
@@ -70,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
70
70
  version: '0'
71
71
  requirements: []
72
72
  rubygems_version: 3.5.22
73
- signing_key:
73
+ signing_key:
74
74
  specification_version: 4
75
75
  summary: Embed V8 in Ruby via rusty_v8 + Magnus (rb-sys)
76
76
  test_files: []