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.
Instance Method Summary collapse
-
#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
-
#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).
-
#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_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.
-
#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).
-
#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.
-
#wasm_bridge ⇒ Object
Handle-oriented JS access for a wasm guest (see WasmBridge).
Constructor Details
Instance Method Details
#collect_garbage ⇒ Object
Run JS GC then drain, so FinalizationRegistry cleanup callbacks fire and release handles for proxies that are no longer referenced.
188 189 190 191 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 188 def collect_garbage @backend.run_gc @backend.drain_microtasks end |
#define_host_object(name, obj) ⇒ Object
20 21 22 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 20 def define_host_object(name, obj) @bridge.define_host_object(name, obj) end |
#dispose ⇒ Object
198 199 200 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 198 def dispose @backend.dispose end |
#drain_microtasks ⇒ Object
84 85 86 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 84 def drain_microtasks @backend.drain_microtasks 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.
78 79 80 81 82 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 78 def evaluate(js) @bridge.decode(eval_tagged("await (#{js.strip.sub(/;\s*\z/, "")})")) rescue ::Quickjs::SyntaxError @bridge.decode(eval_tagged("await (async () => {\n#{js}\n})()")) 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.
64 65 66 67 68 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 64 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).
56 57 58 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 56 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.
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 134 def install_browser_globals @backend.eval(<<~JS) globalThis.self = globalThis; // Top-level window: parent/top are the window itself (spec), so // frame-walking loops terminate instead of dereferencing undefined. globalThis.parent = globalThis; globalThis.top = globalThis; globalThis.location = window.location; globalThis.history = window.history; globalThis.navigator = window.navigator; globalThis.sessionStorage = window.sessionStorage; globalThis.localStorage = window.localStorage; globalThis.CSS = window.CSS; globalThis.fetch = (...args) => window.fetch(...args); globalThis.addEventListener = (...args) => window.addEventListener(...args); globalThis.removeEventListener = (...args) => window.removeEventListener(...args); globalThis.dispatchEvent = (event) => window.dispatchEvent(event); // The window IS the global object, so JS built-in constructors and // namespaces are also `window` properties (`window.String`, // `window.Number`, …). Mirror them as own props on the window proxy // so code that reads constructors off `window` (e.g. the WPT // reflection harness's `window[type]` casts) resolves them. for (const __n of [ "String", "Boolean", "Number", "BigInt", "Symbol", "Object", "Array", "Function", "Date", "RegExp", "Promise", "Map", "Set", "WeakMap", "WeakSet", "Math", "JSON", "Reflect", "Proxy", "Error", "TypeError", "RangeError", "SyntaxError", "Infinity", "NaN", "undefined", "parseInt", "parseFloat", "isNaN", "isFinite", "globalThis", ]) { try { window[__n] = globalThis[__n]; } catch (__e) {} } // Minimal WebAssembly.Memory: the engine provides a real // SharedArrayBuffer but no WebAssembly, and WPT's `common/sab.js` // derives the SAB constructor from // `new WebAssembly.Memory({shared:true}).buffer.constructor`. A // Memory whose `.buffer` is a SharedArrayBuffer is enough to let // those tests (encodeInto, TextDecoder copy, …) exercise shared // buffers with the real codec logic. 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_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.
29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 29 def install_window(win) @window = win define_host_object("window", win) @bridge.window = win @backend.eval(<<~JS) globalThis.setTimeout = (fn, delay) => window.setTimeout(fn, delay); globalThis.clearTimeout = (id) => window.clearTimeout(id); globalThis.setInterval = (fn, delay) => window.setInterval(fn, delay); globalThis.clearInterval = (id) => window.clearInterval(id); globalThis.requestAnimationFrame = (fn) => window.requestAnimationFrame(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 |
#on_log(&block) ⇒ Object
Observe console.* output (see Backend).
125 126 127 128 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 125 def on_log(&block) @backend.on_log(&block) self end |
#on_unhandled_rejection(&block) ⇒ Object
Surface otherwise-swallowed JS promise rejections (see Backend).
119 120 121 122 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 119 def on_unhandled_rejection(&block) @backend.on_unhandled_rejection(&block) self end |
#registered_count ⇒ Object
Live handle count (introspection for lifetime tests).
194 195 196 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 194 def registered_count @bridge.registered_count 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).
103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 103 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 |
#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.
91 92 93 |
# File 'lib/dommy/js/quickjs/runtime.rb', line 91 def wasm_bridge @wasm_bridge ||= WasmBridge.new(@backend) end |