Class: Dommy::Js::Quickjs::Runtime
- Inherits:
-
Object
- Object
- Dommy::Js::Quickjs::Runtime
- Defined in:
- lib/dommy/js/quickjs/runtime.rb
Overview
Public entry point: a JS runtime that can drive a Dommy DOM.
rt = Dommy::Js::Quickjs::Runtime.new
rt.define_host_object("document", win.document)
rt.evaluate('document.querySelector("h1").textContent') #=> "..."
Wires the QuickJS Backend to the engine-agnostic HostBridge, seeded with the Dommy method manifest.
Constant Summary collapse
- WASM_STUB_JS =
<<~'JS' if (typeof globalThis.WebAssembly === "undefined") { var unsupported = function () { return Promise.reject(new Error("WebAssembly is not supported")); }; var throwUnsupported = function () { throw new Error("WebAssembly is not supported"); }; globalThis.WebAssembly = { instantiate: unsupported, instantiateStreaming: unsupported, compile: unsupported, compileStreaming: unsupported, validate: function () { return false; }, Module: throwUnsupported, Instance: throwUnsupported, Memory: function (opts) { var bytes = ((opts && opts.initial) || 0) * 65536; this.buffer = (opts && opts.shared && typeof SharedArrayBuffer === "function") ? new SharedArrayBuffer(bytes) : new ArrayBuffer(bytes); }, Table: function () {}, Global: function () {}, CompileError: Error, LinkError: Error, RuntimeError: Error, }; } JS
- INTL_POLYFILL_JS =
<<~'JS' if (typeof globalThis.Intl === "undefined") { var I = {}; var group = function (s) { var p = String(s).split("."); p[0] = p[0].replace(/\B(?=(\d{3})+(?!\d))/g, ","); return p.join("."); }; function NumberFormat(l, o) { this.o = o || {}; } NumberFormat.prototype.format = function (n) { n = Number(n); var o = this.o; if (o.style === "percent") n *= 100; var max = o.maximumFractionDigits; if (max == null && o.style === "currency") max = 2; var s = group(max == null ? String(n) : n.toFixed(max)); if (o.style === "percent") s += "%"; if (o.style === "currency" && o.currency) s = o.currency + " " + s; return s; }; NumberFormat.prototype.formatToParts = function (n) { return [{ type: "literal", value: this.format(n) }]; }; NumberFormat.prototype.resolvedOptions = function () { return Object.assign({ locale: "en", numberingSystem: "latn", style: "decimal" }, this.o); }; function DateTimeFormat(l, o) { this.o = o || {}; } DateTimeFormat.prototype.format = function (d) { d = d == null ? new Date() : new Date(d); if (isNaN(d.getTime())) return ""; try { return d.toLocaleString(); } catch (e) { return d.toString(); } }; DateTimeFormat.prototype.formatToParts = function (d) { return [{ type: "literal", value: this.format(d) }]; }; DateTimeFormat.prototype.formatRange = function (a, b) { return this.format(a) + " – " + this.format(b); }; DateTimeFormat.prototype.resolvedOptions = function () { return Object.assign({ locale: "en", calendar: "gregory", numberingSystem: "latn", timeZone: "UTC" }, this.o); }; function Collator(l, o) { this.o = o || {}; } Collator.prototype.compare = function (a, b) { a = String(a); b = String(b); return a < b ? -1 : a > b ? 1 : 0; }; Collator.prototype.resolvedOptions = function () { return Object.assign({ locale: "en" }, this.o); }; function PluralRules(l, o) { this.o = o || {}; } PluralRules.prototype.select = function (n) { return Number(n) === 1 ? "one" : "other"; }; PluralRules.prototype.resolvedOptions = function () { return Object.assign({ locale: "en", type: "cardinal" }, this.o); }; function RelativeTimeFormat(l, o) { this.o = o || {}; } RelativeTimeFormat.prototype.format = function (v, u) { return v + " " + u + (Math.abs(v) === 1 ? "" : "s"); }; RelativeTimeFormat.prototype.formatToParts = function (v, u) { return [{ type: "literal", value: this.format(v, u) }]; }; RelativeTimeFormat.prototype.resolvedOptions = function () { return Object.assign({ locale: "en", numeric: "always", style: "long" }, this.o); }; function ListFormat(l, o) { this.o = o || {}; } ListFormat.prototype.format = function (a) { return Array.from(a || []).join(", "); }; ListFormat.prototype.formatToParts = function (a) { return [{ type: "element", value: this.format(a) }]; }; ListFormat.prototype.resolvedOptions = function () { return Object.assign({ locale: "en", type: "conjunction", style: "long" }, this.o); }; I.NumberFormat = NumberFormat; I.DateTimeFormat = DateTimeFormat; I.Collator = Collator; I.PluralRules = PluralRules; I.RelativeTimeFormat = RelativeTimeFormat; I.ListFormat = ListFormat; ["NumberFormat", "DateTimeFormat", "Collator", "PluralRules", "RelativeTimeFormat", "ListFormat"].forEach(function (k) { I[k].supportedLocalesOf = function (locs) { return Array.isArray(locs) ? locs.slice() : locs ? [locs] : []; }; }); I.getCanonicalLocales = function (locs) { return Array.isArray(locs) ? locs.slice() : locs ? [String(locs)] : []; }; globalThis.Intl = I; } JS
Instance Method Summary collapse
-
#bridge_crossing_counts(limit: nil) ⇒ Object
Snapshot bridge crossing counts when DOMMY_JS_BRIDGE_PROFILE=1.
-
#collect_garbage ⇒ Object
Run JS GC then drain, so FinalizationRegistry cleanup callbacks fire and release handles for proxies that are no longer referenced.
- #define_host_object(name, obj) ⇒ Object
- #dispose ⇒ Object
- #drain_microtasks ⇒ Object
-
#drive_due_now ⇒ Object
Run the event loop over the work ready at the current virtual time — the microtask checkpoint plus every due task and anything it queues at the same instant — WITHOUT advancing the clock to a future timer (a result waiting on a real delay is left for the await to surface).
-
#enrich_rejection(err) ⇒ Object
In rejection-debug mode, replace the engine’s detail-less “[object Object]” message (a non-Error reason the engine could only toString) with the rich detail recorded JS-side at rejection time, paired by recency.
-
#evaluate(js) ⇒ Object
Evaluate JS and return its value, with DOM nodes decoded to Dommy objects (rather than the empty Hash a raw proxy becomes crossing to Ruby).
-
#evaluate_settled(expr) ⇒ Object
Evaluate ‘expr` to its awaited value, driving the event loop so a result that depends on a TASK (a fetch’s setTimeout(0) delivery, a setTimeout) settles first.
-
#execute(js) ⇒ Object
Run a script for side effects (no return value).
-
#expose_constructors_on(window_obj) ⇒ Object
Expose the seeded interface constructors on a secondary window (an iframe’s contentWindow), so cross-window instanceof / defaultView work.
-
#initialize(**vm_opts) ⇒ Runtime
constructor
A new instance of Runtime.
-
#install_browser_globals ⇒ Object
Wire the bare browser globals frameworks reach for, aliased onto the installed window: self / location / history / navigator / storages / CSS / fetch / addEventListener / .…
-
#install_intl_polyfill ⇒ Object
This QuickJS build ships without ICU, so ‘Intl` is undefined and any page touching `Intl.NumberFormat` / `DateTimeFormat` / … throws `’Intl’ is not defined` (nuxt.com, i18n libraries, …).
-
#install_wasm_memory_shim ⇒ Object
WPT-only scaffolding: a minimal ‘WebAssembly.Memory` whose `.buffer` is a SharedArrayBuffer.
-
#install_wasm_stub ⇒ Object
This QuickJS build has no WebAssembly, so a bare ‘WebAssembly.foo` reference throws `’WebAssembly’ is not defined` (nuxt.com via Shiki, many bundlers’ feature probes).
-
#install_window(win) ⇒ Object
Inject the Dommy window and alias the bare browser timer globals to it, so ‘setTimeout(fn, ms)` routes into Dommy’s deterministic scheduler.
-
#load_module(source) ⇒ Object
Evaluate an inline ‘<script type=“module”>` body as an ES module (run for side effects).
-
#load_module_url(url) ⇒ Object
Evaluate an external module by URL (the loader fetches it); its relative imports resolve against that URL.
-
#load_script(js) ⇒ Object
Load a script the way a browser <script> does: in GLOBAL scope, so its top-level ‘var` / `function` / `let` declarations become globals.
-
#load_script_cached(js, cache_key:) ⇒ Object
Like #load_script, but compiles the source to bytecode once per ‘cache_key` (an external script’s URL) and reuses it across VMs — avoiding a re-parse of large vendored bundles on every page load.
-
#module_loader=(callable) ⇒ Object
Install the ESM module resolver (see Backend#module_loader=).
-
#on_callback_error(&block) ⇒ Object
Observe a timer/rAF callback that was force-killed by the execution timeout (a runaway busy loop).
-
#on_log(&block) ⇒ Object
Observe console.* output (see Backend).
-
#on_unhandled_rejection(&block) ⇒ Object
Surface otherwise-swallowed JS promise rejections (see Backend).
-
#registered_count ⇒ Object
Live handle count (introspection for lifetime tests).
- #reset_bridge_crossing_counts ⇒ Object
-
#run_until_idle(max_iterations: 1000) ⇒ Object
Drive the event loop to quiescence: drain the native microtask queue, then advance the deterministic scheduler to its next due timer and drain again, repeating until no timer is pending.
-
#set_document_ready_state(state) ⇒ Object
Drive the document lifecycle: set ‘document.readyState` and fire the milestone events (`readystatechange`, then `DOMContentLoaded` on “interactive” / `load` on “complete”), then drain microtasks so the listeners settle.
-
#settle(max_iterations: 1000) ⇒ Object
Settle the work that is READY at the current virtual time: drain microtasks, run timers already due now (‘setTimeout(0)` chains), and flush pending `requestAnimationFrame` callbacks by advancing to their frame boundary — but do NOT jump the clock to a not-yet-due `setTimeout(300)` (that needs an explicit `advance_time(300)`).
-
#wasm_bridge ⇒ Object
Handle-oriented JS access for a wasm guest (see WasmBridge).
Constructor Details
#initialize(**vm_opts) ⇒ Runtime
Returns a new instance of Runtime.
15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 15 def initialize(**vm_opts) @backend = Backend.new(**vm_opts) @bridge = Dommy::Js::HostBridge.new(@backend) @callback_error_listener = nil @js_halted = false # Opt-in diagnostics: the engine stringifies a non-Error rejection reason # to "[object Object]". Install a JS-side recorder so on_unhandled_rejection # can surface the real cause (DOMMY_JS_DEBUG_REJECTIONS=1). @track_rejections = !ENV["DOMMY_JS_DEBUG_REJECTIONS"].to_s.empty? # Install the JS-side Promise rejection recorder (HostBridge registers # the __rb_record_rejection_detail sink it pushes to). The detail then # backfills the engine's detail-less report in #enrich_rejection. @backend.call_js("__rbHost.installRejectionTracker") if @track_rejections end |
Instance Method Details
#bridge_crossing_counts(limit: nil) ⇒ Object
Snapshot bridge crossing counts when DOMMY_JS_BRIDGE_PROFILE=1.
480 481 482 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 480 def bridge_crossing_counts(limit: nil) @bridge.crossing_counts(limit: limit) end |
#collect_garbage ⇒ Object
Run JS GC then drain, so FinalizationRegistry cleanup callbacks fire and release handles for proxies that are no longer referenced.
469 470 471 472 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 469 def collect_garbage @backend.run_gc @backend.drain_microtasks end |
#define_host_object(name, obj) ⇒ Object
30 31 32 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 30 def define_host_object(name, obj) @bridge.define_host_object(name, obj) end |
#dispose ⇒ Object
489 490 491 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 489 def dispose @backend.dispose end |
#drain_microtasks ⇒ Object
211 212 213 214 215 216 217 218 219 220 221 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 211 def drain_microtasks @backend.drain_microtasks rescue ::Quickjs::RuntimeError => e # The microtask checkpoint hit out-of-memory and poisoned the VM. Per # the "browsing never crashes" contract, don't let it escape the event # loop: record it once (so it shows in js_errors / the activity log) and # then no-op — the page's JS is dead, but the browser stays alive. raise unless @backend.poisoned? note_js_halted(e) end |
#drive_due_now ⇒ Object
Run the event loop over the work ready at the current virtual time —the microtask checkpoint plus every due task and anything it queues at the same instant — WITHOUT advancing the clock to a future timer (a result waiting on a real delay is left for the await to surface). The nested-timer 4ms clamp guarantees due-now work drains in finite turns.
200 201 202 203 204 205 206 207 208 209 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 200 def drive_due_now sched = @window&.scheduler return drain_microtasks unless sched 64.times do sched.advance_time(0) break unless sched.next_due_timer_at == sched.now_ms end nil end |
#enrich_rejection(err) ⇒ Object
In rejection-debug mode, replace the engine’s detail-less “[object Object]” message (a non-Error reason the engine could only toString) with the rich detail recorded JS-side at rejection time, paired by recency. A no-op for errors that already carry a real message.
310 311 312 313 314 315 316 317 318 319 320 321 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 310 def enrich_rejection(err) return err unless err.respond_to?(:message) && err..to_s.strip == "[object Object]" detail = @bridge.take_rejection_detail return err if detail.nil? || detail.to_s.empty? enriched = ::Quickjs::RuntimeError.new(detail.to_s, "UnhandledRejection") enriched.set_backtrace(err.backtrace) if err.backtrace enriched rescue StandardError err end |
#evaluate(js) ⇒ Object
Evaluate JS and return its value, with DOM nodes decoded to Dommy objects (rather than the empty Hash a raw proxy becomes crossing to Ruby). Accepts either an expression (‘document.title`) or a statement body that uses `return` (`const x = …; return x;`): the expression form is tried first and, on a syntax error, retried as an async function body. Syntax errors are compile-time so the failed first attempt runs nothing. The result is awaited, so a Promise resolves before returning.
173 174 175 176 177 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 173 def evaluate(js) evaluate_settled("(#{js.strip.sub(/;\s*\z/, "")})") rescue ::Quickjs::SyntaxError evaluate_settled("(async () => {\n#{js}\n})()") end |
#evaluate_settled(expr) ⇒ Object
Evaluate ‘expr` to its awaited value, driving the event loop so a result that depends on a TASK (a fetch’s setTimeout(0) delivery, a setTimeout) settles first. The gem’s top-level await (js_std_await) only drains the engine’s job queue, never Dommy’s scheduler — so awaiting a task-resolved promise directly deadlocks in C. Instead: store the result, run the event loop over everything due NOW (microtask checkpoint + due tasks and the tasks they chain), then await the now-settled promise (which returns immediately and still surfaces a rejection as a Ruby raise, as before).
187 188 189 190 191 192 193 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 187 def evaluate_settled(expr) # `void 0` keeps the completion value off the promise, so the eval # doesn't trip the gem's "unawaited Promise at top-level" guard. @backend.eval("globalThis.__rbEvalP = Promise.resolve(#{expr}); void 0;") drive_due_now @bridge.decode(eval_tagged("await globalThis.__rbEvalP")) end |
#execute(js) ⇒ Object
Run a script for side effects (no return value). Wrapped in an IIFE so statements are allowed and the completion value is voided — otherwise a trailing Promise expression would trip the gem’s “unawaited Promise” guard. Drains microtasks so queued .then work lands before returning.
116 117 118 119 120 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 116 def execute(js) @backend.eval("(function () {\n#{js}\n})();") drain_microtasks nil end |
#expose_constructors_on(window_obj) ⇒ Object
Expose the seeded interface constructors on a secondary window (an iframe’s contentWindow), so cross-window instanceof / defaultView work. Call after install_window (the constructors must already be seeded).
108 109 110 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 108 def expose_constructors_on(window_obj) @bridge.expose_constructors_on(window_obj) end |
#install_browser_globals ⇒ Object
Wire the bare browser globals frameworks reach for, aliased onto the installed window: self / location / history / navigator / storages / CSS / fetch / addEventListener / .… Call after install_window. This is what lets real frontend bundles (Turbo, …) run unmodified.
342 343 344 345 346 347 348 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 342 def install_browser_globals alias_browser_globals install_intl_polyfill install_wasm_stub mirror_builtins_on_window self end |
#install_intl_polyfill ⇒ Object
This QuickJS build ships without ICU, so ‘Intl` is undefined and any page touching `Intl.NumberFormat` / `DateTimeFormat` / … throws `’Intl’ is not defined` (nuxt.com, i18n libraries, …). Install a small locale-naive polyfill: it formats reasonably (grouped numbers, ISO-ish dates) so pages run instead of crashing, without pulling in full ICU.
386 387 388 389 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 386 def install_intl_polyfill @backend.eval(INTL_POLYFILL_JS) self end |
#install_wasm_memory_shim ⇒ Object
WPT-only scaffolding: a minimal ‘WebAssembly.Memory` whose `.buffer` is a SharedArrayBuffer. The engine ships a real SharedArrayBuffer but no WebAssembly, and WPT’s ‘common/sab.js` derives the SAB constructor from `new WebAssembly.Memory(shared:true).buffer.constructor`. This is test-harness-only (real pages never need it), so it is opt-in and NOT part of #install_browser_globals.
451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 451 def install_wasm_memory_shim @backend.eval(<<~JS) if (typeof globalThis.WebAssembly === "undefined" && typeof globalThis.SharedArrayBuffer === "function") { globalThis.WebAssembly = { Memory: function (opts) { const bytes = ((opts && opts.initial) || 0) * 65536; this.buffer = (opts && opts.shared) ? new SharedArrayBuffer(bytes) : new ArrayBuffer(bytes); }, }; } JS self end |
#install_wasm_stub ⇒ Object
This QuickJS build has no WebAssembly, so a bare ‘WebAssembly.foo` reference throws `’WebAssembly’ is not defined` (nuxt.com via Shiki, many bundlers’ feature probes). Define a stub: compile/instantiate reject and validate() returns false, so WASM-loading code takes its JS fallback instead of crashing. ‘Memory` honors `shared:true` (a SharedArrayBuffer) so WPT’s common/sab.js keeps working.
356 357 358 359 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 356 def install_wasm_stub @backend.eval(WASM_STUB_JS) self end |
#install_window(win) ⇒ Object
Inject the Dommy window and alias the bare browser timer globals to it, so ‘setTimeout(fn, ms)` routes into Dommy’s deterministic scheduler. Drive callbacks with ‘win.scheduler.advance_time(ms)`. `window.setTimeout` already works via the Window manifest; this also wires the unqualified globals browsers expose.
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 39 def install_window(win) @window = win define_host_object("window", win) @bridge.window = win # A runaway timer/rAF callback (busy loop) is force-killed by the gem's # eval timeout, surfacing as a Quickjs::InterruptedError out of the host # call. Route it through the scheduler's error hook so it is recorded as # a js_error and dropped, not propagated as a fatal crash (browsing must # never crash). Genuine host bugs (any other error) still propagate. if win.respond_to?(:scheduler) && win.scheduler win.scheduler.timer_error_handler = method(:handle_timer_error) # The other half of a WHATWG microtask checkpoint: the engine's # promise-job queue. Wiring this lets the scheduler drain microtasks # after EACH task (not once per batch of due timers), as the event # loop processing model requires. win.scheduler.microtask_checkpoint = method(:drain_microtasks) end @backend.eval(<<~JS) // Remember where each timer was scheduled, so a throwing callback can // be traced back to the code that set it up. This matters most for // minified SPA bundles, where the thrown value is often a bare `null` // with no stack of its own — the only locatable stack is the // scheduling site. The origin Error is kept JS-side and only // stringified if the callback actually throws (see // __rbFetchTimerOrigin + handle_timer_error); a successful callback // forgets its origin and the map is size-capped, so this stays cheap. globalThis.__rbTimerOrigins = new Map(); const __rbDefer = (schedule, fn, delay) => { if (typeof fn !== "function") return schedule(fn, delay); const origin = new Error(); let id; const wrapped = function () { const result = fn.apply(this, arguments); __rbTimerOrigins.delete(id); // ran cleanly — no need to keep it return result; }; id = schedule(wrapped, delay); __rbTimerOrigins.set(id, origin); if (__rbTimerOrigins.size > 4096) __rbTimerOrigins.delete(__rbTimerOrigins.keys().next().value); return id; }; globalThis.__rbFetchTimerOrigin = (id) => { const origin = __rbTimerOrigins.get(id); if (!origin) return ""; __rbTimerOrigins.delete(id); return origin.stack || ""; }; globalThis.setTimeout = (fn, delay) => __rbDefer((f, d) => window.setTimeout(f, d), fn, delay); globalThis.clearTimeout = (id) => window.clearTimeout(id); globalThis.setInterval = (fn, delay) => __rbDefer((f, d) => window.setInterval(f, d), fn, delay); globalThis.clearInterval = (id) => window.clearInterval(id); globalThis.requestAnimationFrame = (fn) => __rbDefer((f) => window.requestAnimationFrame(f), fn); globalThis.cancelAnimationFrame = (id) => window.cancelAnimationFrame(id); // queueMicrotask must share the engine's promise-job (microtask) // queue so its callbacks are FIFO-ordered with Promise reactions // (the WHATWG single-microtask-queue model). Routing through the // Ruby scheduler instead would drain on a separate pass, reordering // it after all native promise jobs. globalThis.queueMicrotask = (fn) => { if (typeof fn !== "function") throw new TypeError("queueMicrotask requires a function"); Promise.resolve().then(() => { fn(); }); }; JS win end |
#load_module(source) ⇒ Object
Evaluate an inline ‘<script type=“module”>` body as an ES module (run for side effects). Bare specifiers / absolute paths in its imports resolve through the module loader. Drains microtasks afterward.
151 152 153 154 155 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 151 def load_module(source) @backend.import_module(source) drain_microtasks nil end |
#load_module_url(url) ⇒ Object
Evaluate an external module by URL (the loader fetches it); its relative imports resolve against that URL. Drains microtasks.
159 160 161 162 163 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 159 def load_module_url(url) @backend.import_module_url(url) drain_microtasks nil end |
#load_script(js) ⇒ Object
Load a script the way a browser <script> does: in GLOBAL scope, so its top-level ‘var` / `function` / `let` declarations become globals. UMD / “global” bundles rely on this — e.g. Vue’s global build is literally ‘var Vue = (function()…)({})`, which an IIFE wrapper (execute) would trap in function scope. Drains microtasks afterward.
127 128 129 130 131 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 127 def load_script(js) @backend.eval(js) drain_microtasks nil end |
#load_script_cached(js, cache_key:) ⇒ Object
Like #load_script, but compiles the source to bytecode once per ‘cache_key` (an external script’s URL) and reuses it across VMs —avoiding a re-parse of large vendored bundles on every page load.
136 137 138 139 140 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 136 def load_script_cached(js, cache_key:) @backend.run_compiled(ScriptCache.compiled(cache_key, js)) drain_microtasks nil end |
#module_loader=(callable) ⇒ Object
Install the ESM module resolver (see Backend#module_loader=). A callable ‘(specifier, importer) -> source | as: | nil`.
144 145 146 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 144 def module_loader=(callable) @backend.module_loader = callable end |
#on_callback_error(&block) ⇒ Object
Observe a timer/rAF callback that was force-killed by the execution timeout (a runaway busy loop). The host records it as a js_error; the offending timer is already dropped by the scheduler so it cannot re-stall. Optional in the Runtime contract (guard with respond_to?).
327 328 329 330 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 327 def on_callback_error(&block) @callback_error_listener = block self end |
#on_log(&block) ⇒ Object
Observe console.* output (see Backend).
333 334 335 336 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 333 def on_log(&block) @backend.on_log(&block) self end |
#on_unhandled_rejection(&block) ⇒ Object
Surface otherwise-swallowed JS promise rejections (see Backend).
297 298 299 300 301 302 303 304 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 297 def on_unhandled_rejection(&block) if @track_rejections @backend.on_unhandled_rejection { |err| block.call(enrich_rejection(err)) } else @backend.on_unhandled_rejection(&block) end self end |
#registered_count ⇒ Object
Live handle count (introspection for lifetime tests).
475 476 477 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 475 def registered_count @bridge.registered_count end |
#reset_bridge_crossing_counts ⇒ Object
484 485 486 487 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 484 def reset_bridge_crossing_counts @bridge.reset_crossing_counts self end |
#run_until_idle(max_iterations: 1000) ⇒ Object
Drive the event loop to quiescence: drain the native microtask queue, then advance the deterministic scheduler to its next due timer and drain again, repeating until no timer is pending. This is the single deterministic “settle everything” entry point a host uses after an eval (mirroring a ‘drain_async!`): every queued microtask runs and every scheduled timer fires, in WHATWG order (microtasks before each timer). `max_iterations` bounds runaway timer loops (e.g. a self-rescheduling setInterval).
252 253 254 255 256 257 258 259 260 261 262 263 264 265 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 252 def run_until_idle(max_iterations: 1000) sched = @window&.scheduler max_iterations.times do drain_microtasks break unless sched next_at = sched.next_due_timer_at break unless next_at sched.advance_time(next_at - sched.now_ms) drain_microtasks end self end |
#set_document_ready_state(state) ⇒ Object
Drive the document lifecycle: set ‘document.readyState` and fire the milestone events (`readystatechange`, then `DOMContentLoaded` on “interactive” / `load` on “complete”), then drain microtasks so the listeners settle. Lets a host replay the real load sequence so code that waits on document readiness (framework startup, `ready` handlers) runs the deferred path. The document defaults to “complete”, so call `set_document_ready_state(“loading”)` BEFORE loading such code to exercise the waiting path.
231 232 233 234 235 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 231 def set_document_ready_state(state) @window&.document&.__internal_set_ready_state__(state) drain_microtasks self end |
#settle(max_iterations: 1000) ⇒ Object
Settle the work that is READY at the current virtual time: drain microtasks, run timers already due now (‘setTimeout(0)` chains), and flush pending `requestAnimationFrame` callbacks by advancing to their frame boundary — but do NOT jump the clock to a not-yet-due `setTimeout(300)` (that needs an explicit `advance_time(300)`). This is the “let promises and animation frames resolve” entry point; `bound` caps a self-rescheduling rAF loop.
274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 274 def settle(max_iterations: 1000) sched = @window&.scheduler max_iterations.times do drain_microtasks break unless sched before = sched.now_ms sched.advance_time(0) # run due-now timers + microtasks, no clock jump drain_microtasks raf_at = sched.next_animation_frame_at if raf_at && raf_at > sched.now_ms sched.advance_time(raf_at - sched.now_ms) # advance to the frame, run rAF drain_microtasks next end break if sched.now_ms == before end self end |
#wasm_bridge ⇒ Object
Handle-oriented JS access for a wasm guest (see WasmBridge). Memoized so the guest’s ‘__rbWasmInvoke` dispatcher (installed via #on_invoke) stays registered for the VM’s lifetime.
240 241 242 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 240 def wasm_bridge @wasm_bridge ||= WasmBridge.new(@backend) end |