dommy 0.8.0 → 0.9.0
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 +4 -4
- data/README.md +3 -3
- data/lib/dommy/animation.rb +4 -0
- data/lib/dommy/attr.rb +11 -5
- data/lib/dommy/backend/makiri_adapter.rb +330 -0
- data/lib/dommy/backend.rb +114 -33
- data/lib/dommy/blob.rb +2 -0
- data/lib/dommy/bridge.rb +11 -0
- data/lib/dommy/browser.rb +217 -0
- data/lib/dommy/compression_streams.rb +4 -0
- data/lib/dommy/crypto.rb +4 -0
- data/lib/dommy/css.rb +487 -50
- data/lib/dommy/custom_elements.rb +2 -2
- data/lib/dommy/data_transfer.rb +2 -0
- data/lib/dommy/data_uri.rb +35 -0
- data/lib/dommy/deferred_response.rb +59 -0
- data/lib/dommy/document.rb +386 -228
- data/lib/dommy/dom_exception.rb +2 -0
- data/lib/dommy/dom_parser.rb +7 -17
- data/lib/dommy/element.rb +502 -155
- data/lib/dommy/event.rb +240 -9
- data/lib/dommy/fetch.rb +152 -34
- data/lib/dommy/form_data.rb +2 -0
- data/lib/dommy/history.rb +2 -0
- data/lib/dommy/html_canvas_element.rb +230 -0
- data/lib/dommy/html_collection.rb +5 -6
- data/lib/dommy/html_elements.rb +304 -27
- data/lib/dommy/interaction/debug.rb +35 -0
- data/lib/dommy/interaction/dom_summary.rb +131 -0
- data/lib/dommy/interaction/driver.rb +244 -0
- data/lib/dommy/interaction/event_synthesis.rb +56 -0
- data/lib/dommy/interaction/field_interactor.rb +117 -0
- data/lib/dommy/interaction/form_submission.rb +268 -0
- data/lib/dommy/interaction/locator.rb +158 -0
- data/lib/dommy/interaction/role_query.rb +58 -0
- data/lib/dommy/interaction.rb +32 -0
- data/lib/dommy/internal/accessibility_tree.rb +215 -0
- data/lib/dommy/internal/accessible_description.rb +38 -0
- data/lib/dommy/internal/accessible_name.rb +301 -0
- data/lib/dommy/internal/aria_role.rb +252 -0
- data/lib/dommy/internal/aria_snapshot.rb +64 -0
- data/lib/dommy/internal/aria_state.rb +151 -0
- data/lib/dommy/internal/css/calc.rb +242 -0
- data/lib/dommy/internal/css/cascade.rb +430 -0
- data/lib/dommy/internal/css/color.rb +381 -0
- data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
- data/lib/dommy/internal/css/counters.rb +227 -0
- data/lib/dommy/internal/css/custom_properties.rb +183 -0
- data/lib/dommy/internal/css/media_query.rb +302 -0
- data/lib/dommy/internal/css/parser.rb +265 -0
- data/lib/dommy/internal/css/property_registry.rb +512 -0
- data/lib/dommy/internal/css/rule_index.rb +494 -0
- data/lib/dommy/internal/css/supports.rb +158 -0
- data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
- data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
- data/lib/dommy/internal/css_rule_text.rb +160 -0
- data/lib/dommy/internal/dom_matching.rb +80 -9
- data/lib/dommy/internal/element_matching.rb +109 -0
- data/lib/dommy/internal/global_functions.rb +33 -0
- data/lib/dommy/internal/mutation_coordinator.rb +95 -4
- data/lib/dommy/internal/namespaces.rb +49 -5
- data/lib/dommy/internal/node_wrapper_cache.rb +163 -26
- data/lib/dommy/internal/parent_node.rb +82 -5
- data/lib/dommy/internal/selector_ast.rb +124 -0
- data/lib/dommy/internal/selector_index.rb +146 -0
- data/lib/dommy/internal/selector_matcher.rb +756 -0
- data/lib/dommy/internal/selector_parser.rb +283 -131
- data/lib/dommy/internal/shadow_root_registry.rb +9 -2
- data/lib/dommy/internal/template_content_registry.rb +26 -18
- data/lib/dommy/internal/xml_serialization.rb +344 -0
- data/lib/dommy/intersection_observer.rb +2 -0
- data/lib/dommy/js/bridge_conformance.rb +80 -0
- data/lib/dommy/js/constructor_resolver.rb +44 -0
- data/lib/dommy/js/custom_element_bridge.rb +90 -0
- data/lib/dommy/js/dom_interfaces.rb +162 -0
- data/lib/dommy/js/handle_table.rb +60 -0
- data/lib/dommy/js/host_bridge.rb +517 -0
- data/lib/dommy/js/host_runtime.js +1495 -0
- data/lib/dommy/js/import_map.rb +58 -0
- data/lib/dommy/js/marshaller.rb +240 -0
- data/lib/dommy/js/module_loader.rb +99 -0
- data/lib/dommy/js/observable_runtime.js +742 -0
- data/lib/dommy/js/runtime.rb +115 -0
- data/lib/dommy/js/script_boot.rb +221 -0
- data/lib/dommy/js/wire_tags.rb +62 -0
- data/lib/dommy/location.rb +2 -0
- data/lib/dommy/media_query_list.rb +50 -14
- data/lib/dommy/message_channel.rb +22 -6
- data/lib/dommy/minitest/assertions.rb +27 -0
- data/lib/dommy/mutation_observer.rb +89 -4
- data/lib/dommy/navigator.rb +34 -2
- data/lib/dommy/node.rb +24 -14
- data/lib/dommy/notification.rb +2 -0
- data/lib/dommy/parser.rb +1 -1
- data/lib/dommy/performance.rb +21 -1
- data/lib/dommy/promise.rb +94 -10
- data/lib/dommy/range.rb +173 -31
- data/lib/dommy/resources.rb +178 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
- data/lib/dommy/scheduler.rb +149 -13
- data/lib/dommy/screen.rb +91 -0
- data/lib/dommy/shadow_root.rb +76 -13
- data/lib/dommy/storage.rb +2 -1
- data/lib/dommy/streams.rb +6 -0
- data/lib/dommy/text_codec.rb +7 -1
- data/lib/dommy/tree_walker.rb +33 -10
- data/lib/dommy/url.rb +13 -1
- data/lib/dommy/version.rb +1 -1
- data/lib/dommy/window.rb +199 -11
- data/lib/dommy/worker.rb +8 -4
- data/lib/dommy/xml_http_request.rb +47 -6
- data/lib/dommy.rb +36 -1
- metadata +96 -10
- data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
- data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Js
|
|
5
|
+
# Bridges JS-defined custom elements to Dommy's custom element pipeline.
|
|
6
|
+
# `customElements.define(name, JSClass)` on the JS side calls in here, which
|
|
7
|
+
# registers a Dommy::HTMLElement subclass for `name` whose lifecycle
|
|
8
|
+
# reactions (connected/disconnected/adopted/attributeChanged) route back to
|
|
9
|
+
# the JS instance through the bridge. The JS class's constructor itself runs
|
|
10
|
+
# on the JS side via the construction-stack upgrade in host_runtime.js.
|
|
11
|
+
#
|
|
12
|
+
# Named distinctly from Dommy::CustomElementRegistry (the DOM
|
|
13
|
+
# window.customElements registry); this is the JS<->Dommy wiring, not the
|
|
14
|
+
# registry itself.
|
|
15
|
+
class CustomElementBridge
|
|
16
|
+
attr_writer :window
|
|
17
|
+
|
|
18
|
+
def initialize(bridge)
|
|
19
|
+
@bridge = bridge
|
|
20
|
+
@window = nil
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def define(name, observed)
|
|
24
|
+
return unless @window.respond_to?(:custom_elements)
|
|
25
|
+
|
|
26
|
+
@window.custom_elements.define(name, build_class(name, observed))
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# customElements.upgrade(root): delegate to Dommy's registry so a subtree
|
|
31
|
+
# attached without firing reactions gets its registered elements upgraded.
|
|
32
|
+
def upgrade(root)
|
|
33
|
+
return unless @window.respond_to?(:custom_elements)
|
|
34
|
+
|
|
35
|
+
@window.custom_elements.upgrade(root)
|
|
36
|
+
nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
# A Dommy custom element class for `name` whose reactions forward to the JS
|
|
42
|
+
# instance. A fresh subclass per tag carries the bridge / name / observed
|
|
43
|
+
# set as class-level attributes; the reaction methods are defined once on
|
|
44
|
+
# the base (BridgedCustomElement), reading them via `self.class` — so no
|
|
45
|
+
# per-registration method definition is needed.
|
|
46
|
+
def build_class(name, observed)
|
|
47
|
+
klass = Class.new(BridgedCustomElement)
|
|
48
|
+
klass.js_bridge = @bridge
|
|
49
|
+
klass.js_name = name
|
|
50
|
+
klass.js_observed = observed
|
|
51
|
+
klass
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Base for a JS-defined custom element. Each registered tag is a subclass
|
|
56
|
+
# carrying its bridge / tag name / observed attributes; the lifecycle
|
|
57
|
+
# reactions (defined once here) forward to the JS instance through the
|
|
58
|
+
# bridge. `__js_custom_element_name__` marks the node so the bridge upgrades
|
|
59
|
+
# it on first crossing (see HostBridge interface info).
|
|
60
|
+
class BridgedCustomElement < Dommy::HTMLElement
|
|
61
|
+
class << self
|
|
62
|
+
attr_accessor :js_bridge, :js_name, :js_observed
|
|
63
|
+
|
|
64
|
+
def observed_attributes
|
|
65
|
+
js_observed
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def __js_custom_element_name__
|
|
70
|
+
self.class.js_name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def connected_callback
|
|
74
|
+
self.class.js_bridge.invoke_lifecycle(self, "connectedCallback", [])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def disconnected_callback
|
|
78
|
+
self.class.js_bridge.invoke_lifecycle(self, "disconnectedCallback", [])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def adopted_callback
|
|
82
|
+
self.class.js_bridge.invoke_lifecycle(self, "adoptedCallback", [])
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def attribute_changed_callback(attr, old_value, new_value)
|
|
86
|
+
self.class.js_bridge.invoke_lifecycle(self, "attributeChangedCallback", [attr, old_value, new_value])
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Js
|
|
5
|
+
# Derives WebIDL interface metadata for a Dommy DOM object: the most-derived
|
|
6
|
+
# interface name and the single-inheritance chain up to the root
|
|
7
|
+
# (EventTarget for nodes). This mirrors the JS prototype chain the bridge
|
|
8
|
+
# builds so `instanceof` / Object.prototype.toString resolve correctly.
|
|
9
|
+
#
|
|
10
|
+
# Engine-agnostic, and the single home for DOM interface hierarchy knowledge:
|
|
11
|
+
# BASE_CHAINS (seeded eagerly on the JS side) must stay consistent with what
|
|
12
|
+
# #chain_for derives from real objects.
|
|
13
|
+
module DomInterfaces
|
|
14
|
+
# Dommy class basename -> WebIDL interface name, where they diverge.
|
|
15
|
+
# Anything not listed uses the class basename verbatim (HTMLDivElement, …).
|
|
16
|
+
NAME_OVERRIDES = {
|
|
17
|
+
"TextNode" => "Text",
|
|
18
|
+
"CommentNode" => "Comment",
|
|
19
|
+
"CharacterDataNode" => "CharacterData",
|
|
20
|
+
"Fragment" => "DocumentFragment",
|
|
21
|
+
"ClassList" => "DOMTokenList",
|
|
22
|
+
"DatasetMap" => "DOMStringMap",
|
|
23
|
+
"StyleDeclaration" => "CSSStyleDeclaration",
|
|
24
|
+
"LiveNodeList" => "NodeList",
|
|
25
|
+
"StandaloneEventTarget" => "EventTarget"
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
# Concrete HTML element interfaces. A browser exposes every one of these as
|
|
29
|
+
# a global constructor whether or not an instance exists, so a framework's
|
|
30
|
+
# bare `instanceof HTMLInputElement` feature check resolves regardless of
|
|
31
|
+
# page content (idiomorph, Turbo's morph engine, probes `instanceof
|
|
32
|
+
# HTMLInputElement`/`HTMLTextAreaElement` during focus restoration even when
|
|
33
|
+
# the page has no such element). Each is a direct HTMLElement subclass
|
|
34
|
+
# except the two media leaves, appended with their chains below. Mirrors the
|
|
35
|
+
# `class HTMLxxxElement < HTMLElement` set in the dommy gem's html_elements.
|
|
36
|
+
HTML_LEAF_INTERFACES = %w[
|
|
37
|
+
HTMLAnchorElement HTMLAreaElement HTMLBaseElement HTMLBodyElement
|
|
38
|
+
HTMLBRElement HTMLButtonElement HTMLCanvasElement HTMLDataElement HTMLDetailsElement
|
|
39
|
+
HTMLDialogElement HTMLDivElement HTMLEmbedElement HTMLFieldsetElement
|
|
40
|
+
HTMLFormElement HTMLHeadElement HTMLHeadingElement HTMLHRElement
|
|
41
|
+
HTMLHtmlElement HTMLIFrameElement HTMLImageElement HTMLInputElement
|
|
42
|
+
HTMLLabelElement HTMLLegendElement HTMLLIElement HTMLLinkElement
|
|
43
|
+
HTMLMapElement HTMLMetaElement HTMLMeterElement HTMLModElement
|
|
44
|
+
HTMLObjectElement HTMLOListElement HTMLOptGroupElement HTMLOptionElement
|
|
45
|
+
HTMLOutputElement HTMLParagraphElement HTMLPictureElement HTMLPreElement
|
|
46
|
+
HTMLProgressElement HTMLQuoteElement HTMLScriptElement HTMLSelectElement
|
|
47
|
+
HTMLSlotElement HTMLSourceElement HTMLSpanElement HTMLStyleElement
|
|
48
|
+
HTMLTableCaptionElement HTMLTableCellElement HTMLTableElement
|
|
49
|
+
HTMLTableRowElement HTMLTableSectionElement HTMLTemplateElement
|
|
50
|
+
HTMLTextAreaElement HTMLTimeElement HTMLTitleElement HTMLTrackElement
|
|
51
|
+
HTMLUListElement
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
# Base interface chains seeded eagerly on the JS side so `instanceof Node`
|
|
55
|
+
# / `typeof HTMLElement` resolve before an instance of that exact type has
|
|
56
|
+
# crossed. Concrete leaves (HTMLButtonElement, …) are built lazily from
|
|
57
|
+
# #chain_for when an instance crosses. Keep consistent with #chain_for.
|
|
58
|
+
BASE_CHAINS = [
|
|
59
|
+
%w[Node EventTarget],
|
|
60
|
+
%w[Element Node EventTarget],
|
|
61
|
+
%w[HTMLElement Element Node EventTarget],
|
|
62
|
+
%w[SVGElement Element Node EventTarget],
|
|
63
|
+
%w[CharacterData Node EventTarget],
|
|
64
|
+
%w[Text CharacterData Node EventTarget],
|
|
65
|
+
%w[Comment CharacterData Node EventTarget],
|
|
66
|
+
%w[Document Node EventTarget],
|
|
67
|
+
%w[DocumentFragment Node EventTarget],
|
|
68
|
+
# ShadowRoot is a DocumentFragment subclass; seeded so bare `node
|
|
69
|
+
# instanceof ShadowRoot` (Alpine.js walks the tree with this) resolves.
|
|
70
|
+
%w[ShadowRoot DocumentFragment Node EventTarget],
|
|
71
|
+
%w[DocumentType Node EventTarget],
|
|
72
|
+
%w[Attr Node EventTarget],
|
|
73
|
+
%w[Event],
|
|
74
|
+
%w[CustomEvent Event],
|
|
75
|
+
%w[MessageEvent Event],
|
|
76
|
+
%w[PopStateEvent Event],
|
|
77
|
+
%w[HashChangeEvent Event],
|
|
78
|
+
%w[CloseEvent Event],
|
|
79
|
+
%w[MouseEvent Event],
|
|
80
|
+
%w[KeyboardEvent Event],
|
|
81
|
+
%w[PromiseRejectionEvent Event],
|
|
82
|
+
%w[ErrorEvent Event],
|
|
83
|
+
%w[DOMException],
|
|
84
|
+
# Window-exposed constructors that frameworks call bare (new X(...)).
|
|
85
|
+
# Seeding them creates the global; construction routes to the window.
|
|
86
|
+
%w[MutationObserver], %w[IntersectionObserver], %w[ResizeObserver],
|
|
87
|
+
%w[PerformanceObserver], %w[AbortController], %w[AbortSignal EventTarget],
|
|
88
|
+
%w[FormData], %w[URL], %w[URLSearchParams], %w[Headers], %w[Request], %w[Response],
|
|
89
|
+
%w[Blob], %w[File], %w[FileList], %w[FileReader], %w[XMLHttpRequest],
|
|
90
|
+
%w[TextEncoder], %w[TextDecoder], %w[DOMParser], %w[XMLSerializer],
|
|
91
|
+
%w[MessageChannel], %w[BroadcastChannel], %w[WebSocket], %w[EventSource],
|
|
92
|
+
%w[Notification], %w[Worker], %w[DataTransfer],
|
|
93
|
+
%w[ReadableStream], %w[WritableStream], %w[TransformStream],
|
|
94
|
+
%w[Range],
|
|
95
|
+
# CSSOM stylesheet interfaces. Seeded so `style instanceof CSSStyleSheet`
|
|
96
|
+
# resolves: Lit's css-tag runs that check while deciding whether to use
|
|
97
|
+
# constructable stylesheets; with the interface present but
|
|
98
|
+
# `adoptedStyleSheets` unsupported it falls back to injecting a <style>
|
|
99
|
+
# element, which Dommy handles.
|
|
100
|
+
%w[CSSStyleSheet StyleSheet], %w[StyleSheet],
|
|
101
|
+
# Collection interfaces, seeded so `result instanceof NodeList` /
|
|
102
|
+
# `instanceof HTMLCollection` resolve (querySelectorAll, children, …).
|
|
103
|
+
%w[NodeList], %w[HTMLCollection],
|
|
104
|
+
# Traversal: NodeFilter exposes only [Constant]s (NodeFilter.SHOW_ELEMENT,
|
|
105
|
+
# .FILTER_ACCEPT, …); TreeWalker/NodeIterator are instances.
|
|
106
|
+
%w[NodeFilter], %w[TreeWalker], %w[NodeIterator],
|
|
107
|
+
# Concrete HTML element interfaces (see HTML_LEAF_INTERFACES) + the media
|
|
108
|
+
# subtree, so bare `instanceof HTMLInputElement` always resolves.
|
|
109
|
+
*HTML_LEAF_INTERFACES.map { |n| [n, "HTMLElement", "Element", "Node", "EventTarget"] },
|
|
110
|
+
# createElementNS with an unrecognized HTML-namespace local name yields an
|
|
111
|
+
# HTMLUnknownElement; seed it so a bare `instanceof HTMLUnknownElement`
|
|
112
|
+
# resolves even before such an element crosses.
|
|
113
|
+
%w[HTMLUnknownElement HTMLElement Element Node EventTarget],
|
|
114
|
+
%w[HTMLMediaElement HTMLElement Element Node EventTarget],
|
|
115
|
+
%w[HTMLAudioElement HTMLMediaElement HTMLElement Element Node EventTarget],
|
|
116
|
+
%w[HTMLVideoElement HTMLMediaElement HTMLElement Element Node EventTarget]
|
|
117
|
+
].freeze
|
|
118
|
+
|
|
119
|
+
module_function
|
|
120
|
+
|
|
121
|
+
# { "name" => most-derived interface, "chain" => [...] } for a host object.
|
|
122
|
+
def info(obj)
|
|
123
|
+
chain = chain_for(obj)
|
|
124
|
+
{"name" => chain.first, "chain" => chain}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Walk the Dommy class superclass chain (HTMLDivElement < HTMLElement <
|
|
128
|
+
# Element), then append the module-provided base interfaces (Node ->
|
|
129
|
+
# EventTarget) for nodes, since Dommy models Node as a mixin rather than a
|
|
130
|
+
# superclass. Stops at the first foreign superclass (Object, or
|
|
131
|
+
# StandardError for DOMException) so non-DOM ancestors stay out.
|
|
132
|
+
def chain_for(obj)
|
|
133
|
+
# WHATWG models DOMException as a single interface distinguished by its
|
|
134
|
+
# `name` property — there are no per-name subclasses. Collapse Dommy's
|
|
135
|
+
# convenience subclasses (AbortError < DOMException) to the one interface
|
|
136
|
+
# so a DOMException crossing as a value (e.g. `signal.reason`) reports
|
|
137
|
+
# `constructor === DOMException`, which assert_throws_dom checks.
|
|
138
|
+
return ["DOMException"] if defined?(Dommy::DOMException) && obj.is_a?(Dommy::DOMException)
|
|
139
|
+
|
|
140
|
+
names = []
|
|
141
|
+
klass = obj.class
|
|
142
|
+
while klass && klass.name&.start_with?("Dommy::")
|
|
143
|
+
name = name_for(klass)
|
|
144
|
+
names << name if name && !names.include?(name)
|
|
145
|
+
klass = klass.superclass
|
|
146
|
+
end
|
|
147
|
+
if defined?(Dommy::Node) && obj.is_a?(Dommy::Node)
|
|
148
|
+
names << "Node" unless names.include?("Node")
|
|
149
|
+
names << "EventTarget" unless names.include?("EventTarget")
|
|
150
|
+
end
|
|
151
|
+
names
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def name_for(klass)
|
|
155
|
+
base = klass.name&.split("::")&.last
|
|
156
|
+
return nil unless base
|
|
157
|
+
|
|
158
|
+
NAME_OVERRIDES.fetch(base, base)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
module Js
|
|
5
|
+
# Maps JS-facing integer handles to live Ruby objects. Engine-agnostic.
|
|
6
|
+
#
|
|
7
|
+
# Handles are monotonic — never reused — so a handle can never refer to two
|
|
8
|
+
# different objects over the table's lifetime. That invariant is what makes
|
|
9
|
+
# GC-driven #release race-free: a finalizer releasing handle N can't clobber
|
|
10
|
+
# a different object that happened to get the same id.
|
|
11
|
+
#
|
|
12
|
+
# The same Ruby object reuses its handle while still registered (keyed by
|
|
13
|
+
# object_id), so it maps to a single JS proxy (stable identity). A registry
|
|
14
|
+
# entry also keeps the object reachable while JS holds a proxy for it.
|
|
15
|
+
class HandleTable
|
|
16
|
+
def initialize
|
|
17
|
+
@by_handle = {} # handle (Integer) -> object
|
|
18
|
+
@handle_by_oid = {} # object_id -> handle (for identity reuse)
|
|
19
|
+
@next_handle = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def register(obj)
|
|
23
|
+
oid = obj.object_id
|
|
24
|
+
existing = @handle_by_oid[oid]
|
|
25
|
+
return existing if existing && @by_handle[existing].equal?(obj)
|
|
26
|
+
|
|
27
|
+
handle = (@next_handle += 1)
|
|
28
|
+
@by_handle[handle] = obj
|
|
29
|
+
@handle_by_oid[oid] = handle
|
|
30
|
+
handle
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fetch(handle)
|
|
34
|
+
@by_handle.fetch(handle.to_i)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Tolerant lookup: nil when the handle is unknown (its object was released
|
|
38
|
+
# by GC, or a framework passed an invalid/never-allocated handle). Used for
|
|
39
|
+
# call ARGUMENTS, where a reference to a vanished object is simply absent —
|
|
40
|
+
# unlike a receiver handle, where a miss is a real error (#fetch).
|
|
41
|
+
def lookup(handle)
|
|
42
|
+
@by_handle[handle.to_i]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Forget a handle (called when its JS proxy is garbage-collected). Only
|
|
46
|
+
# trims the mapping; the object itself lives on via its other references.
|
|
47
|
+
def release(handle)
|
|
48
|
+
obj = @by_handle.delete(handle.to_i)
|
|
49
|
+
return unless obj
|
|
50
|
+
|
|
51
|
+
oid = obj.object_id
|
|
52
|
+
@handle_by_oid.delete(oid) if @handle_by_oid[oid] == handle.to_i
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def size
|
|
56
|
+
@by_handle.size
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|