dommy 0.5.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 +7 -0
- data/README.md +213 -0
- data/lib/dommy/attr.rb +200 -0
- data/lib/dommy/blob.rb +182 -0
- data/lib/dommy/bridge.rb +141 -0
- data/lib/dommy/css.rb +283 -0
- data/lib/dommy/custom_elements.rb +125 -0
- data/lib/dommy/data_transfer.rb +98 -0
- data/lib/dommy/document.rb +674 -0
- data/lib/dommy/dom_exception.rb +258 -0
- data/lib/dommy/dom_parser.rb +88 -0
- data/lib/dommy/element.rb +1975 -0
- data/lib/dommy/event.rb +589 -0
- data/lib/dommy/fetch.rb +241 -0
- data/lib/dommy/form_data.rb +208 -0
- data/lib/dommy/html_collection.rb +207 -0
- data/lib/dommy/html_elements.rb +4455 -0
- data/lib/dommy/internal/cookie_jar.rb +27 -0
- data/lib/dommy/internal/dom_matching.rb +141 -0
- data/lib/dommy/internal/mutation_coordinator.rb +172 -0
- data/lib/dommy/internal/node_traversal.rb +36 -0
- data/lib/dommy/internal/node_wrapper_cache.rb +179 -0
- data/lib/dommy/internal/observer_manager.rb +31 -0
- data/lib/dommy/internal/observer_matcher.rb +31 -0
- data/lib/dommy/internal/scope_resolution.rb +27 -0
- data/lib/dommy/internal/shadow_root_registry.rb +35 -0
- data/lib/dommy/internal/template_content_registry.rb +97 -0
- data/lib/dommy/minitest/assertions.rb +105 -0
- data/lib/dommy/minitest.rb +17 -0
- data/lib/dommy/navigator.rb +271 -0
- data/lib/dommy/node.rb +218 -0
- data/lib/dommy/observer.rb +199 -0
- data/lib/dommy/parser.rb +29 -0
- data/lib/dommy/promise.rb +199 -0
- data/lib/dommy/router.rb +275 -0
- data/lib/dommy/rspec/capy_style_matchers.rb +356 -0
- data/lib/dommy/rspec/matchers.rb +230 -0
- data/lib/dommy/rspec.rb +18 -0
- data/lib/dommy/scheduler.rb +135 -0
- data/lib/dommy/shadow_root.rb +255 -0
- data/lib/dommy/storage.rb +112 -0
- data/lib/dommy/test_helpers.rb +78 -0
- data/lib/dommy/tree_walker.rb +425 -0
- data/lib/dommy/url.rb +479 -0
- data/lib/dommy/version.rb +5 -0
- data/lib/dommy/world.rb +209 -0
- data/lib/dommy.rb +119 -0
- metadata +110 -0
data/lib/dommy/event.rb
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dommy
|
|
4
|
+
# Note: `Callback` and `Constructor` live in `Dommy::Bridge::*` —
|
|
5
|
+
# they're bridge-adapter classes, not part of the public DOM
|
|
6
|
+
# surface.
|
|
7
|
+
|
|
8
|
+
module EventTarget
|
|
9
|
+
def add_event_listener(type, listener = nil, options = nil, &block)
|
|
10
|
+
cb = listener || block
|
|
11
|
+
return nil if type.nil? || cb.nil?
|
|
12
|
+
|
|
13
|
+
list = listeners_for(type.to_s)
|
|
14
|
+
# Per spec, the same listener (by identity) registered on the
|
|
15
|
+
# same type is silently deduplicated.
|
|
16
|
+
return nil if list.any? { |entry| entry.listener.equal?(cb) }
|
|
17
|
+
|
|
18
|
+
list << Listener.new(cb, options)
|
|
19
|
+
|
|
20
|
+
# `{ signal: AbortSignal }` — when the signal aborts, auto-
|
|
21
|
+
# remove the listener. Per spec, if the signal is already aborted
|
|
22
|
+
# the listener must not be registered at all.
|
|
23
|
+
signal = options.is_a?(Hash) ? (options["signal"] || options[:signal]) : nil
|
|
24
|
+
if signal.respond_to?(:__js_get__)
|
|
25
|
+
if signal.__js_get__("aborted")
|
|
26
|
+
remove_event_listener(type, cb)
|
|
27
|
+
else
|
|
28
|
+
target = self
|
|
29
|
+
signal.__js_call__(
|
|
30
|
+
"addEventListener",
|
|
31
|
+
[
|
|
32
|
+
"abort",
|
|
33
|
+
proc {
|
|
34
|
+
target.remove_event_listener(type, cb)
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def remove_event_listener(type, listener)
|
|
45
|
+
return nil if type.nil? || listener.nil?
|
|
46
|
+
|
|
47
|
+
listeners_for(type.to_s).reject! { |entry| entry.listener.equal?(listener) }
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def dispatch_event(event)
|
|
52
|
+
return true if event.nil?
|
|
53
|
+
|
|
54
|
+
# Per spec, dispatchEvent must receive an Event instance.
|
|
55
|
+
raise TypeError, "dispatchEvent requires an Event, got #{event.class}" unless event.is_a?(Event)
|
|
56
|
+
|
|
57
|
+
event.__prepare_for_dispatch__(self)
|
|
58
|
+
path = if event.bubbles?
|
|
59
|
+
event.__js_get__("composed") ? composed_bubble_path(event) : event_bubble_path
|
|
60
|
+
else
|
|
61
|
+
[self]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
event.__record_path__(path) if event.respond_to?(:__record_path__)
|
|
65
|
+
path.each do |target|
|
|
66
|
+
event.__set_current_target__(target)
|
|
67
|
+
target.__deliver_event__(event)
|
|
68
|
+
break if event.propagation_stopped?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
!event.default_prevented?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def __deliver_event__(event)
|
|
75
|
+
listeners = listeners_for(event.type).dup
|
|
76
|
+
listeners.each do |entry|
|
|
77
|
+
invoke_listener(entry.listener, event)
|
|
78
|
+
if entry.once?
|
|
79
|
+
listeners_for(event.type).reject! { |candidate| candidate.listener.equal?(entry.listener) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
break if event.immediate_propagation_stopped?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
Listener = Struct.new(:listener, :options) do
|
|
91
|
+
def once?
|
|
92
|
+
case options
|
|
93
|
+
when Hash
|
|
94
|
+
options["once"] || options[:once]
|
|
95
|
+
else
|
|
96
|
+
false
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def listeners_for(type)
|
|
102
|
+
@event_listeners ||= Hash.new { |h, k| h[k] = [] }
|
|
103
|
+
@event_listeners[type]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def event_bubble_path
|
|
107
|
+
path = [self]
|
|
108
|
+
current = self
|
|
109
|
+
while (current = current.send(:__event_parent__))
|
|
110
|
+
path << current
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
path
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Build the propagation path with optional shadow-boundary
|
|
117
|
+
# crossing. When the in-flight event has `composed: true`, the
|
|
118
|
+
# walk continues from a ShadowRoot to its host; otherwise it
|
|
119
|
+
# stops at the shadow boundary (nil from `__event_parent__`).
|
|
120
|
+
def composed_bubble_path(event)
|
|
121
|
+
path = [self]
|
|
122
|
+
current = self
|
|
123
|
+
loop do
|
|
124
|
+
nxt = current.send(:__event_parent__)
|
|
125
|
+
if nxt.nil? && event.respond_to?(:__js_get__) && event.__js_get__("composed")
|
|
126
|
+
# Try to cross a shadow boundary
|
|
127
|
+
if current.is_a?(ShadowRoot)
|
|
128
|
+
# If current is a ShadowRoot, jump to its host
|
|
129
|
+
nxt = current.host
|
|
130
|
+
else
|
|
131
|
+
# If current is a node inside a ShadowRoot, find and jump to host
|
|
132
|
+
sr = enclosing_shadow_root_of(current)
|
|
133
|
+
break unless sr
|
|
134
|
+
|
|
135
|
+
nxt = sr.host
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
break unless nxt
|
|
140
|
+
|
|
141
|
+
path << nxt
|
|
142
|
+
current = nxt
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
path
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def enclosing_shadow_root_of(target)
|
|
151
|
+
return nil unless target.respond_to?(:__node__)
|
|
152
|
+
|
|
153
|
+
doc = target.instance_variable_get(:@document)
|
|
154
|
+
return nil unless doc && doc.respond_to?(:__shadow_root_containing__)
|
|
155
|
+
|
|
156
|
+
doc.__shadow_root_containing__(target.__node__)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
public
|
|
160
|
+
|
|
161
|
+
def invoke_listener(listener, event)
|
|
162
|
+
# DOM spec: a listener can be (a) a function, or (b) an object
|
|
163
|
+
# with a `handleEvent` method. Both Ruby and JS-bridged callables
|
|
164
|
+
# are supported.
|
|
165
|
+
if listener.respond_to?(:handle_event)
|
|
166
|
+
listener.handle_event(event)
|
|
167
|
+
elsif listener.respond_to?(:call) && !listener.is_a?(Module)
|
|
168
|
+
listener.call(event)
|
|
169
|
+
elsif listener.respond_to?(:__js_call__)
|
|
170
|
+
# Prefer handleEvent if the bridge object advertises it; fall
|
|
171
|
+
# back to call. We can't introspect on the JS side, so we just
|
|
172
|
+
# try call (the common case for JS.callback {}).
|
|
173
|
+
listener.__js_call__("call", [event])
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
class StandaloneEventTarget
|
|
179
|
+
include EventTarget
|
|
180
|
+
|
|
181
|
+
def __js_call__(method, args)
|
|
182
|
+
case method
|
|
183
|
+
when "addEventListener"
|
|
184
|
+
add_event_listener(args[0], args[1], args[2])
|
|
185
|
+
when "removeEventListener"
|
|
186
|
+
remove_event_listener(args[0], args[1])
|
|
187
|
+
when "dispatchEvent"
|
|
188
|
+
dispatch_event(args[0])
|
|
189
|
+
else
|
|
190
|
+
nil
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def __event_parent__
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class Event
|
|
200
|
+
def initialize(type, init = nil)
|
|
201
|
+
@type = type.to_s
|
|
202
|
+
@bubbles = !!read_init(init, "bubbles")
|
|
203
|
+
@cancelable = !!read_init(init, "cancelable")
|
|
204
|
+
@composed = !!read_init(init, "composed")
|
|
205
|
+
@default_prevented = false
|
|
206
|
+
@propagation_stopped = false
|
|
207
|
+
@immediate_propagation_stopped = false
|
|
208
|
+
@target = nil
|
|
209
|
+
@current_target = nil
|
|
210
|
+
@composed_path = []
|
|
211
|
+
# `timeStamp` is the high-resolution timestamp at construction
|
|
212
|
+
# in ms (browser uses performance.now). We use monotonic time
|
|
213
|
+
# for determinism across spec runs.
|
|
214
|
+
@time_stamp = (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000.0)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
attr_reader :type
|
|
218
|
+
|
|
219
|
+
def bubbles?
|
|
220
|
+
@bubbles
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def default_prevented?
|
|
224
|
+
@default_prevented
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def propagation_stopped?
|
|
228
|
+
@propagation_stopped
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def immediate_propagation_stopped?
|
|
232
|
+
@immediate_propagation_stopped
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def __prepare_for_dispatch__(target)
|
|
236
|
+
@target ||= target
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def __set_current_target__(target)
|
|
240
|
+
@current_target = target
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def __js_get__(key)
|
|
244
|
+
case key
|
|
245
|
+
when "type"
|
|
246
|
+
@type
|
|
247
|
+
when "bubbles"
|
|
248
|
+
@bubbles
|
|
249
|
+
when "cancelable"
|
|
250
|
+
@cancelable
|
|
251
|
+
when "composed"
|
|
252
|
+
@composed
|
|
253
|
+
when "defaultPrevented"
|
|
254
|
+
@default_prevented
|
|
255
|
+
when "target"
|
|
256
|
+
@target
|
|
257
|
+
when "currentTarget"
|
|
258
|
+
@current_target
|
|
259
|
+
when "timeStamp"
|
|
260
|
+
@time_stamp
|
|
261
|
+
when "cancelBubble"
|
|
262
|
+
@propagation_stopped
|
|
263
|
+
when "eventPhase"
|
|
264
|
+
event_phase
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def __js_set__(key, value)
|
|
269
|
+
case key
|
|
270
|
+
when "cancelBubble"
|
|
271
|
+
# Setting to truthy stops propagation; spec quirk that
|
|
272
|
+
# `cancelBubble = false` does NOT un-stop (browser observation).
|
|
273
|
+
@propagation_stopped = true if value
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
nil
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def __js_call__(method, args)
|
|
280
|
+
case method
|
|
281
|
+
when "preventDefault"
|
|
282
|
+
@default_prevented = true if @cancelable
|
|
283
|
+
nil
|
|
284
|
+
when "stopPropagation"
|
|
285
|
+
@propagation_stopped = true
|
|
286
|
+
nil
|
|
287
|
+
when "stopImmediatePropagation"
|
|
288
|
+
@propagation_stopped = true
|
|
289
|
+
@immediate_propagation_stopped = true
|
|
290
|
+
nil
|
|
291
|
+
when "composedPath"
|
|
292
|
+
@composed_path.dup
|
|
293
|
+
when "initEvent"
|
|
294
|
+
init_event(args[0], args[1], args[2])
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Deprecated `Event#initEvent(type, bubbles, cancelable)` — older
|
|
299
|
+
# browsers used `document.createEvent("Event").initEvent(...)`.
|
|
300
|
+
# Resets internal flags as a side effect.
|
|
301
|
+
def init_event(type, bubbles = false, cancelable = false)
|
|
302
|
+
@type = type.to_s
|
|
303
|
+
@bubbles = !!bubbles
|
|
304
|
+
@cancelable = !!cancelable
|
|
305
|
+
@default_prevented = false
|
|
306
|
+
@propagation_stopped = false
|
|
307
|
+
@immediate_propagation_stopped = false
|
|
308
|
+
nil
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Filled in by EventTarget#dispatch_event as the event walks the
|
|
312
|
+
# bubble path so `composedPath()` returns the right list.
|
|
313
|
+
#
|
|
314
|
+
# Per spec, `load` events do not propagate to the Window when
|
|
315
|
+
# composed paths are computed (resource-finished signal stays at
|
|
316
|
+
# the target).
|
|
317
|
+
def __record_path__(targets)
|
|
318
|
+
@composed_path = if @type == "load"
|
|
319
|
+
targets.reject { |t| t.is_a?(Window) }
|
|
320
|
+
else
|
|
321
|
+
targets
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
def event_phase
|
|
328
|
+
# 0 = NONE (default), 2 = AT_TARGET, 3 = BUBBLING_PHASE. We don't
|
|
329
|
+
# implement capturing (phase 1) by design.
|
|
330
|
+
return 0 if @current_target.nil?
|
|
331
|
+
return 2 if @current_target.equal?(@target)
|
|
332
|
+
|
|
333
|
+
3
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
public
|
|
337
|
+
|
|
338
|
+
private
|
|
339
|
+
|
|
340
|
+
def read_init(init, key)
|
|
341
|
+
case init
|
|
342
|
+
when Hash
|
|
343
|
+
init[key] || init[key.to_sym]
|
|
344
|
+
else
|
|
345
|
+
init.respond_to?(:__js_get__) ? init.__js_get__(key) : nil
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
class CustomEvent < Event
|
|
351
|
+
def initialize(type, init = nil)
|
|
352
|
+
super
|
|
353
|
+
@detail = read_init(init, "detail")
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def __js_get__(key)
|
|
357
|
+
return @detail if key == "detail"
|
|
358
|
+
|
|
359
|
+
super
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
class MouseEvent < Event
|
|
364
|
+
def initialize(type, init = nil)
|
|
365
|
+
super
|
|
366
|
+
@button = read_init(init, "button") || 0
|
|
367
|
+
@ctrl_key = !!read_init(init, "ctrlKey")
|
|
368
|
+
@shift_key = !!read_init(init, "shiftKey")
|
|
369
|
+
@alt_key = !!read_init(init, "altKey")
|
|
370
|
+
@meta_key = !!read_init(init, "metaKey")
|
|
371
|
+
@client_x = read_init(init, "clientX") || 0
|
|
372
|
+
@client_y = read_init(init, "clientY") || 0
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def __js_get__(key)
|
|
376
|
+
case key
|
|
377
|
+
when "button"
|
|
378
|
+
@button
|
|
379
|
+
when "ctrlKey"
|
|
380
|
+
@ctrl_key
|
|
381
|
+
when "shiftKey"
|
|
382
|
+
@shift_key
|
|
383
|
+
when "altKey"
|
|
384
|
+
@alt_key
|
|
385
|
+
when "metaKey"
|
|
386
|
+
@meta_key
|
|
387
|
+
when "clientX"
|
|
388
|
+
@client_x
|
|
389
|
+
when "clientY"
|
|
390
|
+
@client_y
|
|
391
|
+
else
|
|
392
|
+
super
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# `DragEvent` — fired during drag-and-drop with a `dataTransfer`
|
|
398
|
+
# payload. Inherits from MouseEvent so coordinates / modifier keys
|
|
399
|
+
# are available alongside the dragged data.
|
|
400
|
+
class DragEvent < MouseEvent
|
|
401
|
+
def initialize(type, init = nil)
|
|
402
|
+
super
|
|
403
|
+
@data_transfer = read_init(init, "dataTransfer")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
attr_reader :data_transfer
|
|
407
|
+
|
|
408
|
+
def __js_get__(key)
|
|
409
|
+
case key
|
|
410
|
+
when "dataTransfer"
|
|
411
|
+
@data_transfer
|
|
412
|
+
else
|
|
413
|
+
super
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
class KeyboardEvent < Event
|
|
419
|
+
def initialize(type, init = nil)
|
|
420
|
+
super
|
|
421
|
+
@key = read_init(init, "key").to_s
|
|
422
|
+
@ctrl_key = !!read_init(init, "ctrlKey")
|
|
423
|
+
@shift_key = !!read_init(init, "shiftKey")
|
|
424
|
+
@alt_key = !!read_init(init, "altKey")
|
|
425
|
+
@meta_key = !!read_init(init, "metaKey")
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def __js_get__(key)
|
|
429
|
+
case key
|
|
430
|
+
when "key"
|
|
431
|
+
@key
|
|
432
|
+
when "ctrlKey"
|
|
433
|
+
@ctrl_key
|
|
434
|
+
when "shiftKey"
|
|
435
|
+
@shift_key
|
|
436
|
+
when "altKey"
|
|
437
|
+
@alt_key
|
|
438
|
+
when "metaKey"
|
|
439
|
+
@meta_key
|
|
440
|
+
else
|
|
441
|
+
super
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# `AbortController` + `AbortSignal` subset. Signal fires an
|
|
447
|
+
# "abort" event and flips `[:aborted]` to true when the controller's
|
|
448
|
+
# `abort()` is called; otherwise it stays inert.
|
|
449
|
+
class AbortSignal
|
|
450
|
+
include EventTarget
|
|
451
|
+
|
|
452
|
+
# Spec: `AbortSignal.abort(reason?)` returns a fresh, pre-aborted
|
|
453
|
+
# signal. Convenient for APIs that need an already-cancelled token.
|
|
454
|
+
def self.abort(reason = nil)
|
|
455
|
+
signal = new
|
|
456
|
+
signal.__mark_aborted__(reason)
|
|
457
|
+
signal
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Spec: `AbortSignal.timeout(ms)` returns a signal that aborts
|
|
461
|
+
# itself after `ms` milliseconds with a `TimeoutError` reason.
|
|
462
|
+
# Without a Window/scheduler we fall back to a Thread-based timer
|
|
463
|
+
# so the signal works in vanilla CRuby; embedders that want
|
|
464
|
+
# microtask integration can pass a window via `__schedule_via__`.
|
|
465
|
+
def self.timeout(ms, scheduler: nil)
|
|
466
|
+
signal = new
|
|
467
|
+
reason = DOMException::TimeoutError.new("operation timed out")
|
|
468
|
+
if scheduler
|
|
469
|
+
scheduler.set_timeout(proc { signal.__mark_aborted__(reason) }, ms.to_i)
|
|
470
|
+
else
|
|
471
|
+
signal.__schedule_thread_timeout__(ms.to_i, reason)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
signal
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
# Spec: `AbortSignal.any([sig, ...])` returns a composite signal
|
|
478
|
+
# that aborts as soon as any of the inputs aborts. If any input
|
|
479
|
+
# is already aborted, the returned signal is pre-aborted with
|
|
480
|
+
# that input's reason.
|
|
481
|
+
def self.any(signals)
|
|
482
|
+
composite = new
|
|
483
|
+
list = Array(signals).select { |s| s.is_a?(AbortSignal) }
|
|
484
|
+
already = list.find(&:aborted?)
|
|
485
|
+
if already
|
|
486
|
+
composite.__mark_aborted__(already.reason)
|
|
487
|
+
return composite
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
list.each do |sig|
|
|
491
|
+
sig.add_event_listener("abort", proc { composite.__mark_aborted__(sig.reason) })
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
composite
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def initialize
|
|
498
|
+
@aborted = false
|
|
499
|
+
@reason = nil
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Background-thread timeout used by `AbortSignal.timeout` when no
|
|
503
|
+
# scheduler is provided. Kept package-private; tests can also
|
|
504
|
+
# drive the abort manually via `__mark_aborted__`.
|
|
505
|
+
def __schedule_thread_timeout__(ms, reason)
|
|
506
|
+
Thread.new do
|
|
507
|
+
sleep(ms.to_f / 1000.0)
|
|
508
|
+
__mark_aborted__(reason)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
nil
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def aborted?
|
|
515
|
+
@aborted
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def reason
|
|
519
|
+
@reason
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Spec: throws `signal.reason` if aborted, otherwise no-op. Used by
|
|
523
|
+
# consumer code that polls before doing async work.
|
|
524
|
+
def throw_if_aborted
|
|
525
|
+
return unless @aborted
|
|
526
|
+
|
|
527
|
+
raise @reason.is_a?(Exception) ? @reason : RuntimeError.new(@reason.to_s)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
alias throwIfAborted throw_if_aborted
|
|
531
|
+
|
|
532
|
+
def __js_get__(key)
|
|
533
|
+
case key
|
|
534
|
+
when "aborted"
|
|
535
|
+
@aborted
|
|
536
|
+
when "reason"
|
|
537
|
+
@reason
|
|
538
|
+
end
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def __js_set__(_key, _value)
|
|
542
|
+
nil
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
def __js_call__(method, args)
|
|
546
|
+
case method
|
|
547
|
+
when "addEventListener"
|
|
548
|
+
add_event_listener(args[0], args[1], args[2])
|
|
549
|
+
when "removeEventListener"
|
|
550
|
+
remove_event_listener(args[0], args[1])
|
|
551
|
+
when "dispatchEvent"
|
|
552
|
+
dispatch_event(args[0])
|
|
553
|
+
when "throwIfAborted"
|
|
554
|
+
throw_if_aborted
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def __mark_aborted__(reason = nil)
|
|
559
|
+
return if @aborted
|
|
560
|
+
|
|
561
|
+
@aborted = true
|
|
562
|
+
@reason = reason
|
|
563
|
+
dispatch_event(Event.new("abort", "bubbles" => false, "cancelable" => false))
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
class AbortController
|
|
568
|
+
attr_reader :signal
|
|
569
|
+
|
|
570
|
+
def initialize
|
|
571
|
+
@signal = AbortSignal.new
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def __js_get__(key)
|
|
575
|
+
@signal if key == "signal"
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def __js_set__(_key, _value)
|
|
579
|
+
nil
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def __js_call__(method, args)
|
|
583
|
+
case method
|
|
584
|
+
when "abort"
|
|
585
|
+
@signal.__mark_aborted__(args[0])
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
end
|