dommy 0.8.1 → 0.10.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.
Files changed (121) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/lib/dommy/animation.rb +4 -0
  4. data/lib/dommy/attr.rb +57 -13
  5. data/lib/dommy/backend/makiri_adapter.rb +364 -0
  6. data/lib/dommy/backend.rb +143 -33
  7. data/lib/dommy/blob.rb +50 -6
  8. data/lib/dommy/bridge.rb +11 -0
  9. data/lib/dommy/browser.rb +420 -0
  10. data/lib/dommy/callable_invoker.rb +6 -2
  11. data/lib/dommy/compression_streams.rb +4 -0
  12. data/lib/dommy/crypto.rb +4 -0
  13. data/lib/dommy/css.rb +487 -50
  14. data/lib/dommy/custom_elements.rb +2 -2
  15. data/lib/dommy/data_transfer.rb +2 -0
  16. data/lib/dommy/data_uri.rb +35 -0
  17. data/lib/dommy/deferred_response.rb +59 -0
  18. data/lib/dommy/document.rb +901 -275
  19. data/lib/dommy/dom_exception.rb +2 -0
  20. data/lib/dommy/dom_parser.rb +7 -17
  21. data/lib/dommy/element.rb +940 -353
  22. data/lib/dommy/event.rb +529 -17
  23. data/lib/dommy/fetch.rb +577 -53
  24. data/lib/dommy/form_data.rb +2 -0
  25. data/lib/dommy/history.rb +26 -3
  26. data/lib/dommy/html_canvas_element.rb +230 -0
  27. data/lib/dommy/html_collection.rb +150 -27
  28. data/lib/dommy/html_elements.rb +1968 -282
  29. data/lib/dommy/interaction/debug.rb +35 -0
  30. data/lib/dommy/interaction/dom_summary.rb +131 -0
  31. data/lib/dommy/interaction/driver.rb +385 -0
  32. data/lib/dommy/interaction/event_synthesis.rb +164 -0
  33. data/lib/dommy/interaction/field_interactor.rb +177 -0
  34. data/lib/dommy/interaction/form_submission.rb +268 -0
  35. data/lib/dommy/interaction/locator.rb +158 -0
  36. data/lib/dommy/interaction/role_query.rb +58 -0
  37. data/lib/dommy/interaction.rb +32 -0
  38. data/lib/dommy/internal/accessibility_tree.rb +215 -0
  39. data/lib/dommy/internal/accessible_description.rb +38 -0
  40. data/lib/dommy/internal/accessible_name.rb +301 -0
  41. data/lib/dommy/internal/aria_role.rb +252 -0
  42. data/lib/dommy/internal/aria_snapshot.rb +64 -0
  43. data/lib/dommy/internal/aria_state.rb +151 -0
  44. data/lib/dommy/internal/child_node.rb +199 -0
  45. data/lib/dommy/internal/css/calc.rb +242 -0
  46. data/lib/dommy/internal/css/cascade.rb +472 -0
  47. data/lib/dommy/internal/css/color.rb +381 -0
  48. data/lib/dommy/internal/css/computed_style_declaration.rb +130 -0
  49. data/lib/dommy/internal/css/counters.rb +227 -0
  50. data/lib/dommy/internal/css/custom_properties.rb +183 -0
  51. data/lib/dommy/internal/css/media_query.rb +302 -0
  52. data/lib/dommy/internal/css/parser.rb +265 -0
  53. data/lib/dommy/internal/css/property_registry.rb +512 -0
  54. data/lib/dommy/internal/css/rule_index.rb +494 -0
  55. data/lib/dommy/internal/css/supports.rb +158 -0
  56. data/lib/dommy/internal/css/ua_stylesheet.rb +53 -0
  57. data/lib/dommy/internal/css_pseudo_handlers.rb +283 -42
  58. data/lib/dommy/internal/css_rule_text.rb +160 -0
  59. data/lib/dommy/internal/dom_matching.rb +80 -9
  60. data/lib/dommy/internal/element_matching.rb +109 -0
  61. data/lib/dommy/internal/global_functions.rb +55 -9
  62. data/lib/dommy/internal/mutation_coordinator.rb +122 -11
  63. data/lib/dommy/internal/namespaces.rb +49 -5
  64. data/lib/dommy/internal/node_wrapper_cache.rb +217 -38
  65. data/lib/dommy/internal/observer_manager.rb +6 -0
  66. data/lib/dommy/internal/observer_matcher.rb +6 -8
  67. data/lib/dommy/internal/parent_node.rb +98 -57
  68. data/lib/dommy/internal/selector_ast.rb +124 -0
  69. data/lib/dommy/internal/selector_index.rb +146 -0
  70. data/lib/dommy/internal/selector_matcher.rb +845 -0
  71. data/lib/dommy/internal/selector_parser.rb +321 -132
  72. data/lib/dommy/internal/shadow_root_registry.rb +9 -2
  73. data/lib/dommy/internal/template_content_registry.rb +26 -18
  74. data/lib/dommy/internal/xml_serialization.rb +344 -0
  75. data/lib/dommy/intersection_observer.rb +2 -0
  76. data/lib/dommy/js/bridge_conformance.rb +80 -0
  77. data/lib/dommy/js/constructor_resolver.rb +44 -0
  78. data/lib/dommy/js/custom_element_bridge.rb +101 -0
  79. data/lib/dommy/js/dom_interfaces.rb +177 -0
  80. data/lib/dommy/js/handle_table.rb +60 -0
  81. data/lib/dommy/js/host_bridge.rb +538 -0
  82. data/lib/dommy/js/host_runtime.js +2357 -0
  83. data/lib/dommy/js/import_map.rb +58 -0
  84. data/lib/dommy/js/marshaller.rb +240 -0
  85. data/lib/dommy/js/module_loader.rb +99 -0
  86. data/lib/dommy/js/observable_runtime.js +742 -0
  87. data/lib/dommy/js/runtime.rb +115 -0
  88. data/lib/dommy/js/script_boot.rb +301 -0
  89. data/lib/dommy/js/wire_tags.rb +62 -0
  90. data/lib/dommy/location.rb +78 -13
  91. data/lib/dommy/media_query_list.rb +50 -14
  92. data/lib/dommy/message_channel.rb +22 -6
  93. data/lib/dommy/minitest/assertions.rb +27 -0
  94. data/lib/dommy/mutation_observer.rb +89 -5
  95. data/lib/dommy/navigation.rb +263 -0
  96. data/lib/dommy/navigator.rb +34 -2
  97. data/lib/dommy/node.rb +198 -35
  98. data/lib/dommy/notification.rb +2 -0
  99. data/lib/dommy/parser.rb +1 -1
  100. data/lib/dommy/performance.rb +21 -1
  101. data/lib/dommy/promise.rb +94 -10
  102. data/lib/dommy/range.rb +187 -33
  103. data/lib/dommy/resources.rb +178 -0
  104. data/lib/dommy/rspec/capy_style_matchers.rb +126 -0
  105. data/lib/dommy/scheduler.rb +165 -13
  106. data/lib/dommy/screen.rb +91 -0
  107. data/lib/dommy/shadow_root.rb +109 -13
  108. data/lib/dommy/storage.rb +63 -11
  109. data/lib/dommy/streams.rb +6 -0
  110. data/lib/dommy/text_codec.rb +7 -1
  111. data/lib/dommy/tree_walker.rb +51 -46
  112. data/lib/dommy/url.rb +17 -3
  113. data/lib/dommy/version.rb +1 -1
  114. data/lib/dommy/web_socket.rb +45 -2
  115. data/lib/dommy/window.rb +322 -15
  116. data/lib/dommy/worker.rb +8 -4
  117. data/lib/dommy/xml_http_request.rb +76 -9
  118. data/lib/dommy.rb +38 -1
  119. metadata +82 -14
  120. data/lib/dommy/backend/nokogiri_adapter.rb +0 -127
  121. data/lib/dommy/backend/nokolexbor_adapter.rb +0 -117
data/lib/dommy/backend.rb CHANGED
@@ -2,24 +2,23 @@
2
2
 
3
3
  module Dommy
4
4
  # `Dommy::Backend` — pluggable HTML parser abstraction. Lets Dommy
5
- # work with either Nokogiri (mature, full namespace support) or
6
- # Nokolexbor (faster, HTML5-only). Internally, all DOM library
5
+ # work with Makiri (Lexbor-based, HTML5-only). Internally, all DOM library
7
6
  # code goes through this facade rather than referencing the parser
8
7
  # directly.
9
8
  #
10
- # Defaults to Nokogiri if available, else Nokolexbor.
9
+ # Defaults to Makiri.
11
10
  #
12
11
  # Switching backends:
13
12
  #
14
13
  # require "dommy"
15
- # Dommy::Backend.use(:nokolexbor)
14
+ # Dommy::Backend.use(:makiri)
16
15
  #
17
16
  # Or set directly:
18
17
  #
19
- # Dommy::Backend.current = Dommy::Backend::Nokolexbor
18
+ # Dommy::Backend.current = Dommy::Backend::Makiri
20
19
  #
21
20
  # All adapters must implement the same interface — see
22
- # `Backend::Nokogiri` for the canonical reference.
21
+ # `Backend::Makiri` for the canonical reference.
23
22
  module Backend
24
23
  class BackendNotAvailable < StandardError
25
24
  end
@@ -33,36 +32,133 @@ module Dommy
33
32
 
34
33
  def use(name)
35
34
  @current = case name.to_sym
36
- when :nokogiri
37
- require_relative "backend/nokogiri_adapter"
38
- Nokogiri
39
- when :nokolexbor
40
- require_relative "backend/nokolexbor_adapter"
41
- Nokolexbor
35
+ when :makiri
36
+ require_relative "backend/makiri_adapter"
37
+ Makiri
42
38
  else
43
- raise ArgumentError, "Unknown backend: #{name.inspect}. Use :nokogiri or :nokolexbor."
39
+ raise ArgumentError, "Unknown backend: #{name.inspect}. Use :makiri."
44
40
  end
45
41
  end
46
42
 
43
+ # Stable per-document identity key for a backend node, used to cache DOM
44
+ # wrappers and key per-node side tables. Makiri mints a fresh wrapper per traversal
45
+ # but never frees nodes (arena-owned), so it keys on the stable node pointer.
46
+ def identity_key(node)
47
+ current.identity_key(node)
48
+ end
49
+
50
+ # Deep (or shallow) copy of an element/node, detached and owned by the
51
+ # same document — the backing for DOM cloneNode.
52
+ def clone_node(node, deep:)
53
+ current.clone_node(node, deep: deep)
54
+ end
55
+
56
+ # Deep copy of a whole document (DOM cloneNode on the document).
57
+ def clone_document(doc)
58
+ current.clone_document(doc)
59
+ end
60
+
61
+ # A fresh, empty HTML-backed document.
62
+ def empty_document
63
+ current.empty_document
64
+ end
65
+
66
+ # A fresh, empty XML-backed document — for `new Document()` / createDocument,
67
+ # which the DOM defines as XML documents (case-preserving, real CDATA
68
+ # nodeType, namespaces).
69
+ def empty_xml_document
70
+ current.empty_xml_document
71
+ end
72
+
73
+ # An empty backing document matching `doc`'s kind (HTML stays HTML, XML stays
74
+ # XML) — for a shallow document clone, whose result keeps the source flavor.
75
+ def empty_document_like(doc)
76
+ current.empty_document_like(doc)
77
+ end
78
+
79
+ # Bring `node` (already detached from its old tree) into `target_doc`,
80
+ # returning the backend node now owned by `target_doc`. Makiri can't move a node
81
+ # between arenas, so it imports a copy — callers must reseat any wrapper
82
+ # onto the returned node.
83
+ def adopt(node, target_doc)
84
+ current.adopt(node, target_doc)
85
+ end
86
+
87
+ # Whether the backend can move a node between documents in place or must adopt a copy first
88
+ # (Lexbor's arenas can't move a node, so inserting a foreign node requires importing
89
+ # it). Lets callers skip a needless — and on an empty target, root-less and
90
+ # therefore crashing — adoption on backends that don't need it.
91
+ def moves_nodes_across_documents?
92
+ current.respond_to?(:moves_nodes_across_documents?) ? current.moves_nodes_across_documents? : true
93
+ end
94
+
95
+ # CSS query that honors Dommy's custom pseudo-classes
96
+ # (`:disabled`/`:enabled`/`:checked`/`:scope`). Each backend applies its
97
+ # own mechanism (Lexbor native pseudos plus a
98
+ # `:scope` rewrite). `scope_node` binds `:scope` to that element.
99
+ def select_all(node, selector, scope_node: nil)
100
+ current.select_all(node, selector, scope_node: scope_node)
101
+ end
102
+
103
+ def select_first(node, selector, scope_node: nil)
104
+ current.select_first(node, selector, scope_node: scope_node)
105
+ end
106
+
47
107
  # Delegate calls so internal code can use `Backend.parse(...)`.
48
108
  def parse(html)
49
109
  current.parse(html)
50
110
  end
51
111
 
52
- # Parse XML input into an XML document. Backends without a real XML parser
53
- # (HTML-only, e.g. Nokolexbor) fall back to the HTML parser.
112
+ # Parse XML input into an XML document.
113
+ # fall back to the HTML parser.
54
114
  def parse_xml(xml)
55
- current.respond_to?(:parse_xml) ? current.parse_xml(xml) : current.parse(xml)
115
+ current.parse_xml(xml)
56
116
  end
57
117
 
58
118
  def fragment(html, owner_doc:)
59
119
  current.fragment(html, owner_doc: owner_doc)
60
120
  end
61
121
 
122
+ # Make `node` the sole document element of `doc` (used by
123
+ # DOMImplementation.createDocument). Lexbor-backed documents are seeded with an <html> shell
124
+ # that must be cleared first.
125
+ def set_document_root(doc, node)
126
+ current.set_document_root(doc, node)
127
+ end
128
+
62
129
  def create_element(name, doc)
63
130
  current.create_element(name, doc)
64
131
  end
65
132
 
133
+ # Create a namespaced element permitting a DOM-valid qualified name that a
134
+ # strict XML backend would reject (an internal invalid char like "f}oo"),
135
+ # preserving case/prefix. Returns nil when the backend has no loose path
136
+ # (fall back to #create_element); raises ArgumentError for a genuinely
137
+ # invalid name (the caller maps it to InvalidCharacterError).
138
+ def create_element_loose(qualified_name, prefix, local, namespace, doc)
139
+ current.create_element_loose(qualified_name, prefix, local, namespace, doc)
140
+ end
141
+
142
+ # A detached DocumentType node owned by `doc` (for
143
+ # DOMImplementation.createDocumentType). Returns nil when the backend has no
144
+ # doctype factory (the caller falls back to a synthetic DocumentType); raises
145
+ # ArgumentError for a name the factory rejects (the caller then also falls
146
+ # back, since createDocumentType is permissive).
147
+ def create_document_type(name, public_id, system_id, doc)
148
+ current.respond_to?(:create_document_type) ? current.create_document_type(name, public_id, system_id, doc) : nil
149
+ end
150
+
151
+ # The parsed document's DocumentType node, or nil when it declares none.
152
+ def internal_subset(doc)
153
+ current.respond_to?(:internal_subset) ? current.internal_subset(doc) : nil
154
+ end
155
+
156
+ # The backend class for a DocumentType node (nil if unsupported), so the
157
+ # wrapper cache can route it to Dommy::DocumentType.
158
+ def document_type_class
159
+ current.respond_to?(:document_type_class) ? current.document_type_class : nil
160
+ end
161
+
66
162
  def create_text(content, doc)
67
163
  current.create_text(content, doc)
68
164
  end
@@ -81,16 +177,40 @@ module Dommy
81
177
  current.respond_to?(:cdata_class) ? current.cdata_class : nil
82
178
  end
83
179
 
180
+ # ProcessingInstruction node (`<?target data?>`). Supported by every
181
+ # backend Dommy ships (both Makiri document families), so — unlike CDATA —
182
+ # there is no text-node fallback.
183
+ def create_processing_instruction(target, data, doc)
184
+ current.create_processing_instruction(target, data, doc)
185
+ end
186
+
187
+ def processing_instruction_class
188
+ current.processing_instruction_class
189
+ end
190
+
84
191
  def namespace_of(node)
85
192
  current.namespace_of(node)
86
193
  end
87
194
 
195
+ # The element's in-scope namespace declarations (each responds to
196
+ # `prefix`/`href`). Empty on backends without an XML namespace model.
197
+ def namespace_definitions(node)
198
+ current.namespace_definitions(node)
199
+ end
200
+
201
+ # The content child nodes of a `<template>` element. HTML5 parsers model
202
+ # template contents differently — Lexbor/Makiri in a separate content fragment — so reading them goes
203
+ # through the backend. Used by the template-content registry's migration.
204
+ def template_content_nodes(node)
205
+ current.template_content_nodes(node)
206
+ end
207
+
88
208
  def add_namespace_definition(node, prefix, href)
89
209
  current.add_namespace_definition(node, prefix, href)
90
210
  end
91
211
 
92
212
  # Namespaced attribute access (DOM *AttributeNS). `namespace` is an href
93
- # String or nil. Nokolexbor degrades to qualified-name (null-namespace).
213
+ # String or nil.
94
214
  def get_attribute_ns(node, namespace, local_name)
95
215
  current.get_attribute_ns(node, namespace, local_name)
96
216
  end
@@ -148,25 +268,15 @@ module Dommy
148
268
  private
149
269
 
150
270
  def detect_default
151
- try_nokogiri ||
152
- try_nokolexbor ||
153
- raise(BackendNotAvailable, "Dommy requires either 'nokogiri' or 'nokolexbor' gem to be installed.")
154
- end
155
-
156
- def try_nokogiri
157
- require "nokogiri"
158
-
159
- require_relative "backend/nokogiri_adapter"
160
- Nokogiri
161
- rescue LoadError
162
- nil
271
+ try_makiri ||
272
+ raise(BackendNotAvailable, "Dommy requires either 'makiri' gem to be installed.")
163
273
  end
164
274
 
165
- def try_nokolexbor
166
- require "nokolexbor"
275
+ def try_makiri
276
+ require "makiri"
167
277
 
168
- require_relative "backend/nokolexbor_adapter"
169
- Nokolexbor
278
+ require_relative "backend/makiri_adapter"
279
+ Makiri
170
280
  rescue LoadError
171
281
  nil
172
282
  end
data/lib/dommy/blob.rb CHANGED
@@ -21,19 +21,26 @@ module Dommy
21
21
  # Promises (they need a scheduler). A window-less Blob falls back to a
22
22
  # synchronous result, which `await` still handles.
23
23
  def initialize(parts = [], options = {}, window = nil)
24
+ # WebIDL: an omitted / `undefined` blobParts argument defaults to an empty
25
+ # sequence (`new Blob()` / `new Blob(undefined)` is a zero-length Blob), so
26
+ # it must not be coerced to the string "undefined".
27
+ parts = [] if parts.nil? || (defined?(Bridge::UNDEFINED) && parts.equal?(Bridge::UNDEFINED))
24
28
  parts = [parts] unless parts.is_a?(Array)
25
29
  @data = collect_bytes(parts)
26
30
  @size = @data.bytesize
27
- raw_type = options["type"] || options[:type] || ""
28
- @type = raw_type.to_s.downcase
31
+ raw_type = (options["type"] || options[:type] || "").to_s
32
+ # A type string with any code point outside U+0020..U+007E is discarded
33
+ # (→ ""); otherwise it is ASCII-lowercased (FileAPI "parse a MIME type"
34
+ # gate, applied to both the constructor and slice's contentType).
35
+ @type = raw_type.match?(/[^ -~]/) ? "" : raw_type.downcase
29
36
  @window = window
30
37
  end
31
38
 
32
39
  # Return a new Blob over a byte range of this one.
33
40
  # Negative indices are treated as offsets from the end (per spec).
34
41
  def slice(start = 0, last = @size, content_type = "")
35
- s = clamp_index(start.to_i, @size)
36
- e = clamp_index(last.to_i, @size)
42
+ s = clamp_index(clamp_long_long(start), @size)
43
+ e = clamp_index(clamp_long_long(last), @size)
37
44
  e = s if e < s
38
45
  Blob.new([@data.byteslice(s, e - s) || ""], {"type" => content_type.to_s}, @window)
39
46
  end
@@ -51,6 +58,13 @@ module Dommy
51
58
  Bridge::ArrayBuffer.new(@data.bytes)
52
59
  end
53
60
 
61
+ # Read the bytes as a Uint8Array (the spec return type). The DOM spec returns
62
+ # a Promise<Uint8Array>; Dommy is synchronous. `Bridge::Bytes` crosses the JS
63
+ # boundary as a Uint8Array (vs `ArrayBuffer` for #array_buffer).
64
+ def bytes
65
+ Bridge::Bytes.new(@data.bytes)
66
+ end
67
+
54
68
  # Raw binary bytes (Ruby ASCII-8BIT string). Used by FormData /
55
69
  # fetch when serializing multipart bodies.
56
70
  def __dommy_bytes__
@@ -63,23 +77,38 @@ module Dommy
63
77
  @size
64
78
  when "type"
65
79
  @type
80
+ else
81
+ Bridge::ABSENT
66
82
  end
67
83
  end
68
84
 
69
85
  # Methods routed through __js_call__ (keep in sync with its when-arms).
70
86
  # File < Blob inherits these (it adds only properties).
71
87
  include Bridge::Methods
72
- js_methods %w[slice text arrayBuffer]
88
+ js_methods %w[slice text arrayBuffer bytes]
73
89
  def __js_call__(method, args)
74
90
  case method
75
91
  when "slice"
76
- slice(args[0] || 0, args[1] || @size, args[2] || "")
92
+ # An omitted / `undefined` start|end uses the default (0 / size); map
93
+ # UNDEFINED to nil so the `|| default` fallbacks apply (a bare UNDEFINED
94
+ # is truthy and has no #to_i). contentType is a plain DOMString: omitted
95
+ # / undefined → "" (default), but an explicit JS null coerces to "null".
96
+ a = args.map { |v| v.equal?(Bridge::UNDEFINED) ? nil : v }
97
+ ctype = if args.length < 3 || args[2].equal?(Bridge::UNDEFINED)
98
+ ""
99
+ else
100
+ args[2].nil? ? "null" : args[2].to_s
101
+ end
102
+ slice(a[0] || 0, a[1] || @size, ctype)
77
103
  when "text"
78
104
  # WHATWG: Blob.text() returns a Promise<string>.
79
105
  promise_or_value(text)
80
106
  when "arrayBuffer"
81
107
  # WHATWG: Blob.arrayBuffer() returns a Promise<ArrayBuffer>.
82
108
  promise_or_value(array_buffer)
109
+ when "bytes"
110
+ # WHATWG: Blob.bytes() returns a Promise<Uint8Array>.
111
+ promise_or_value(bytes)
83
112
  end
84
113
  end
85
114
 
@@ -113,6 +142,21 @@ module Dommy
113
142
  idx = length + idx if idx.negative?
114
143
  idx.clamp(0, length)
115
144
  end
145
+
146
+ # WebIDL `[Clamp] long long` conversion of a slice bound: a fractional value
147
+ # rounds to the nearest integer, ties to even (banker's rounding) — so
148
+ # `slice(1.5)` starts at 2 and `slice(3.5)` at 4, per the [Clamp] extended
149
+ # attribute on Blob.slice.
150
+ def clamp_long_long(value)
151
+ return value if value.is_a?(Integer)
152
+
153
+ f = value.to_f
154
+ return 0 if f.nan?
155
+
156
+ f.round(half: :even)
157
+ rescue StandardError
158
+ 0
159
+ end
116
160
  end
117
161
 
118
162
  # `File` — Blob with a filename and an optional last-modified
data/lib/dommy/bridge.rb CHANGED
@@ -37,6 +37,17 @@ module Dommy
37
37
  def UNDEFINED.inspect = "#<Dommy::Bridge::UNDEFINED>"
38
38
  UNDEFINED.freeze
39
39
 
40
+ # The sentinel a `__js_get__` returns for a GENUINELY-ABSENT property (a key
41
+ # the object does not have), as distinct from a present property whose value
42
+ # is `nil`/JS null or `UNDEFINED`/JS undefined. It marshals to JS `undefined`
43
+ # for the value, but the proxy reports `("x" in obj) === false` for it — so
44
+ # feature detection like `isUndefined(window.Vue)` AND `"Vue" in window` are
45
+ # both correct. (UNDEFINED means present-but-undefined → reported present.)
46
+ ABSENT = Object.new
47
+ def ABSENT.to_s = "undefined"
48
+ def ABSENT.inspect = "#<Dommy::Bridge::ABSENT>"
49
+ ABSENT.freeze
50
+
40
51
  # An opaque handle to a JS-side value that Ruby only stores and hands back
41
52
  # (an AbortSignal's reason, a CustomEvent's detail). A non-plain JS object
42
53
  # (Error, class instance, …) crosses as one of these instead of being