Class: Dommy::Js::HostBridge
- Inherits:
-
Object
- Object
- Dommy::Js::HostBridge
- Defined in:
- lib/dommy/js/host_bridge.rb
Overview
Engine-agnostic core of the JS<->Ruby DOM bridge. Given a ‘backend` that can evaluate JS, register Ruby host functions, and call back into JS, HostBridge exposes a Ruby object to the JS side as an ES Proxy whose property/method access routes into the bridge ABI:
__js_get__(name) / __js_set__(name, value) / __js_call__(method, args)
Nothing here is QuickJS-specific; this layer is intended to move into a future ‘dommy-js` gem with QuickJS/wasm backends plugged in underneath.
Two collaborators keep the marshalling core free of DOM specifics:
DomInterfaces — interface name/chain derivation (instanceof support)
ConstructorRegistry — `new Event(...)` style reverse construction
Backend contract:
backend.eval(js) -> evaluate top-level JS
backend.define_host_function(name) { } -> expose a Ruby block as a JS global
backend.call_js(path, *args) -> invoke a JS global function by path
The host object must implement js_get/js_set/js_call, and the bridge needs to know which names are methods (callable via js_call) vs. properties (read via js_get) — see #method_names.
Constant Summary collapse
- HOST_RUNTIME_JS =
JS half of the bridge (globalThis.__rbHost). Read from a companion file so it stays lintable/highlightable rather than buried in a heredoc. ::File — inside module Dommy, bare ‘File` resolves to Dommy::File (the File API class), not Ruby’s file class.
::File.read(::File.join(__dir__, "host_runtime.js")).freeze
- OBSERVABLE_RUNTIME_JS =
The WICG Observable polyfill (Observable/Subscriber + EventTarget.when), evaluated after the DOM interface prototypes are seeded.
::File.read(::File.join(__dir__, "observable_runtime.js")).freeze
Instance Method Summary collapse
-
#decode(tagged) ⇒ Object
Turn a JS-side tagged value (produced by __rbHost.tag) back into Ruby: tagged handles become the original Ruby DOM objects.
-
#define_host_object(name, obj) ⇒ Object
Bind a Ruby object to a JS global of the given name.
-
#expose_constructors_on(window_obj) ⇒ Object
Expose the seeded interface constructors (Element, Node, DOMException, …) on a secondary window object — an iframe’s contentWindow — so cross-window ‘instanceof subWin.Element` and `subDoc.defaultView.DOMException` resolve to the same constructors the top window uses.
-
#initialize(backend) ⇒ HostBridge
constructor
A new instance of HostBridge.
-
#invoke_callback(id, args, this_arg = nil) ⇒ Object
Invoke a retained live JS function by id (used by HostCallback).
-
#invoke_lifecycle(node, callback, args) ⇒ Object
Invoke a JS custom element lifecycle callback (connectedCallback etc.) for a Dommy node.
-
#registered_count ⇒ Object
Number of live handle entries.
-
#schedule_native_microtask(callback) ⇒ Object
Enqueue a Ruby callback as a NATIVE microtask (a resolved-promise job), so it runs in FIFO order with the engine’s other promise jobs.
-
#window=(win) ⇒ Object
Bind the window the bridge draws on for JS constructors (new Event(…)) and custom element registration.
Constructor Details
#initialize(backend) ⇒ HostBridge
Returns a new instance of HostBridge.
38 39 40 41 42 43 44 45 46 47 |
# File 'lib/dommy/js/host_bridge.rb', line 38 def initialize(backend) @backend = backend @handles = HandleTable.new @callback_objects = {} @constructors = ConstructorRegistry.new @custom_elements = CustomElements.new(self) @microtask_procs = {} @microtask_seq = 0 install! end |
Instance Method Details
#decode(tagged) ⇒ Object
Turn a JS-side tagged value (produced by __rbHost.tag) back into Ruby: tagged handles become the original Ruby DOM objects. Used for return values that may contain DOM nodes (e.g. evaluate_script).
124 125 126 |
# File 'lib/dommy/js/host_bridge.rb', line 124 def decode(tagged) unwrap(tagged) end |
#define_host_object(name, obj) ⇒ Object
Bind a Ruby object to a JS global of the given name.
50 51 52 53 54 |
# File 'lib/dommy/js/host_bridge.rb', line 50 def define_host_object(name, obj) handle = @handles.register(obj) @backend.eval("globalThis[#{name.to_s.to_json}] = __rbHost.makeProxy(#{handle}); undefined;") obj end |
#expose_constructors_on(window_obj) ⇒ Object
Expose the seeded interface constructors (Element, Node, DOMException, …) on a secondary window object — an iframe’s contentWindow — so cross-window ‘instanceof subWin.Element` and `subDoc.defaultView.DOMException` resolve to the same constructors the top window uses. Idempotent per window.
92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/dommy/js/host_bridge.rb', line 92 def expose_constructors_on(window_obj) handle = @handles.register(window_obj) # Retain the proxy in a JS-side registry: the constructors are defined as # own properties on the proxy's target, so the proxy must stay alive (and # keep its handle) — otherwise GC releases it and a later # `iframe.contentWindow` rebuilds a fresh, constructor-less proxy. @backend.eval(<<~JS) (globalThis.__rbSubWindows ||= []).push(__rbHost.makeProxy(#{handle})); __rbHost.exposeConstructorsOnWindow(globalThis.__rbSubWindows.at(-1)); JS window_obj end |
#invoke_callback(id, args, this_arg = nil) ⇒ Object
Invoke a retained live JS function by id (used by HostCallback). The JS side returns a ‘dehydrate`d (tagged) value, so unwrap it back to Ruby: a callback that returns e.g. a Promise proxy must come back as the live PromiseValue, otherwise Dommy can’t adopt it (breaking ‘fetch().then(r => r.json()).then(…)` chains).
117 118 119 |
# File 'lib/dommy/js/host_bridge.rb', line 117 def invoke_callback(id, args, this_arg = nil) unwrap(@backend.call_js("__rbHost.invokeCallback", id, wrap(Array(args)), wrap(this_arg))) end |
#invoke_lifecycle(node, callback, args) ⇒ Object
Invoke a JS custom element lifecycle callback (connectedCallback etc.) for a Dommy node. Called by the bridged custom element class (see CustomElements).
107 108 109 110 |
# File 'lib/dommy/js/host_bridge.rb', line 107 def invoke_lifecycle(node, callback, args) handle = @handles.register(node) unwrap(@backend.call_js("__rbHost.invokeLifecycle", handle, callback, wrap(Array(args)))) end |
#registered_count ⇒ Object
Number of live handle entries. Introspection for lifetime tests.
129 130 131 |
# File 'lib/dommy/js/host_bridge.rb', line 129 def registered_count @handles.size end |
#schedule_native_microtask(callback) ⇒ Object
Enqueue a Ruby callback as a NATIVE microtask (a resolved-promise job), so it runs in FIFO order with the engine’s other promise jobs.
81 82 83 84 85 86 |
# File 'lib/dommy/js/host_bridge.rb', line 81 def schedule_native_microtask(callback) id = (@microtask_seq += 1) @microtask_procs[id] = callback @backend.call_js("__rbHost.scheduleMicrotask", id) nil end |
#window=(win) ⇒ Object
Bind the window the bridge draws on for JS constructors (new Event(…)) and custom element registration. Called by Runtime#install_window — kept distinct from define_host_object so the generic binder has no hidden side effects.
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/dommy/js/host_bridge.rb', line 60 def window=(win) @constructors.source = win @custom_elements.window = win # Now that constructors are resolvable, expose their static methods # (URL.createObjectURL, …) on the seeded interface globals, and expose # the constructors themselves on the window proxy (window.Node, # document.defaultView.DOMException, …). @backend.call_js("__rbHost.attachStatics") @backend.call_js("__rbHost.exposeConstructorsOnWindow") # Route Dommy's host-side microtasks (MutationObserver delivery, …) onto # the engine's native promise-job queue, so they interleave FIFO with JS # `await`/Promise reactions instead of draining on a separate pass (which # would deliver e.g. MutationObserver records only after `await # Promise.resolve()`, batching several mutations into one callback). if win.respond_to?(:scheduler) && win.scheduler.respond_to?(:native_microtask_scheduler=) win.scheduler.native_microtask_scheduler = ->(callback) { schedule_native_microtask(callback) } end end |