Class: Dommy::AbortSignal

Inherits:
Object
  • Object
show all
Includes:
Bridge::Methods, EventTarget
Defined in:
lib/dommy/event.rb

Overview

AbortController + AbortSignal subset. Signal fires an "abort" event and flips [:aborted] to true when the controller's abort() is called; otherwise it stays inert.

Constant Summary collapse

NO_REASON =

Sentinel meaning "no reason argument was supplied" — distinct from an explicit null reason (abort(null) keeps reason null, but abort() / abort(undefined) default to a fresh AbortError).

Object.new

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Bridge::Methods

included

Methods included from EventTarget

#__dommy_dump_event_failure__, #__internal_deliver_event__, #__internal_event_parent__, #__internal_process_event_handler_return__, #add_event_listener, capture_flag, #deliver_at, #dispatch_event, #event_name_from_on, #invoke_listener_isolated, js_truthy?, #on_handler, #remove_event_listener, #set_on_handler

Constructor Details

#initializeAbortSignal

Returns a new instance of AbortSignal.



1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
# File 'lib/dommy/event.rb', line 1458

def initialize
  @aborted = false
  @reason = nil
  # The WHATWG "dependent signal" model backing AbortSignal.any: a composite
  # signal flattens to its original (non-dependent) source signals, and each
  # source holds an ordered list of the composites depending on it — so abort
  # propagation fires in source-then-dependents order, not via event chaining.
  @dependent = false
  @source_signals = []
  @dependent_signals = []
  # WHATWG "abort algorithms": callbacks run synchronously during abort,
  # BEFORE the "abort" event — so a consumer (e.g. an Observable subscription)
  # can tear down ahead of any externally-registered abort listener.
  @abort_algorithms = []
end

Class Method Details

.abort(*reason) ⇒ Object

Spec: AbortSignal.abort(reason?) returns a fresh, pre-aborted signal. Convenient for APIs that need an already-cancelled token.



1405
1406
1407
1408
1409
# File 'lib/dommy/event.rb', line 1405

def self.abort(*reason)
  signal = new
  signal.__internal_mark_aborted__(*reason)
  signal
end

.any(signals) ⇒ Object

Spec: AbortSignal.any([sig, ...]) returns a composite signal that aborts as soon as any of the inputs aborts. If any input is already aborted, the returned signal is pre-aborted with that input's reason.



1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
# File 'lib/dommy/event.rb', line 1432

def self.any(signals)
  composite = new
  list = Array(signals).select { |s| s.is_a?(AbortSignal) }
  already = list.find(&:aborted?)
  if already
    composite.__internal_mark_aborted__(already.reason)
    return composite
  end

  composite.__internal_make_dependent__
  list.each do |sig|
    # A plain source signal links directly; a composite is flattened to its
    # own (already-flat) source signals, so the dependency graph stays one
    # level deep and abort order is well-defined.
    sources = sig.__internal_dependent__? ? sig.__internal_source_signals__ : [sig]
    sources.each do |source|
      next if composite.__internal_source_signals__.include?(source)

      composite.__internal_add_source__(source)
      source.__internal_add_dependent__(composite)
    end
  end

  composite
end

.timeout(ms, scheduler: nil) ⇒ Object

Spec: AbortSignal.timeout(ms) returns a signal that aborts itself after ms milliseconds with a TimeoutError reason. Without a Window/scheduler we fall back to a Thread-based timer so the signal works in vanilla CRuby; embedders that want microtask integration can pass a window via __schedule_via__.



1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
# File 'lib/dommy/event.rb', line 1416

def self.timeout(ms, scheduler: nil)
  signal = new
  reason = DOMException::TimeoutError.new("operation timed out")
  if scheduler
    scheduler.set_timeout(proc { signal.__internal_mark_aborted__(reason) }, ms.to_i)
  else
    signal.__internal_schedule_thread_timeout__(ms.to_i, reason)
  end

  signal
end

Instance Method Details

#__internal_add_abort_algorithm__(callback) ⇒ Object

Register an abort algorithm (WHATWG "add an algorithm to a signal"): it runs synchronously when the signal aborts, before the "abort" event. A no-op once already aborted — the caller handles that case itself. Exposed to the JS Observable polyfill so a subscription's consumer-abort fires ahead of any external abort listener (the spec's downstream-before-upstream ordering).



1558
1559
1560
1561
# File 'lib/dommy/event.rb', line 1558

def __internal_add_abort_algorithm__(callback)
  @abort_algorithms << callback unless @aborted
  nil
end

#__internal_add_dependent__(dependent) ⇒ Object



1609
1610
1611
# File 'lib/dommy/event.rb', line 1609

def __internal_add_dependent__(dependent)
  @dependent_signals << dependent
end

#__internal_add_source__(source) ⇒ Object



1605
1606
1607
# File 'lib/dommy/event.rb', line 1605

def __internal_add_source__(source)
  @source_signals << source
end

#__internal_dependent__?Boolean

Returns:

  • (Boolean)


1597
1598
1599
# File 'lib/dommy/event.rb', line 1597

def __internal_dependent__?
  @dependent
end

#__internal_make_dependent__Object

----- dependent-signal plumbing (package-private, used by .any) -----



1593
1594
1595
# File 'lib/dommy/event.rb', line 1593

def __internal_make_dependent__
  @dependent = true
end

#__internal_mark_aborted__(reason = NO_REASON) ⇒ Object

WHATWG "signal abort": set the reason, then propagate to dependent signals — firing "abort" at this signal FIRST and then at each dependent in the order it was registered (the reasons are all stamped before any event fires, so a handler observing a sibling sees it already aborted).



1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
# File 'lib/dommy/event.rb', line 1572

def __internal_mark_aborted__(reason = NO_REASON)
  return if @aborted

  @aborted = true
  no_reason = reason.equal?(NO_REASON) || (defined?(Bridge::UNDEFINED) && reason.equal?(Bridge::UNDEFINED))
  @reason = no_reason ? DOMException::AbortError.new("signal is aborted without reason") : reason

  dependents_to_abort = []
  @dependent_signals.each do |dependent|
    next if dependent.aborted?

    dependent.__internal_set_reason__(@reason)
    dependents_to_abort << dependent
  end

  __internal_run_abort_steps__
  dependents_to_abort.each(&:__internal_run_abort_steps__)
end

#__internal_run_abort_steps__Object

The spec's "abort steps": run the registered abort algorithms first (synchronously, ahead of any listener), then fire the trusted "abort" event.



1622
1623
1624
1625
1626
1627
# File 'lib/dommy/event.rb', line 1622

def __internal_run_abort_steps__
  algorithms = @abort_algorithms
  @abort_algorithms = []
  algorithms.each { |algo| invoke_abort_algorithm(algo) }
  dispatch_event(Event.new("abort", "bubbles" => false, "cancelable" => false).__internal_mark_trusted__)
end

#__internal_schedule_thread_timeout__(ms, reason) ⇒ Object

Background-thread timeout used by AbortSignal.timeout when no scheduler is provided. Kept package-private; tests can also drive the abort manually via __internal_mark_aborted__.



1477
1478
1479
1480
1481
1482
1483
1484
# File 'lib/dommy/event.rb', line 1477

def __internal_schedule_thread_timeout__(ms, reason)
  Thread.new do
    sleep(ms.to_f / 1000.0)
    __internal_mark_aborted__(reason)
  end

  nil
end

#__internal_set_reason__(reason) ⇒ Object

Mark aborted with a reason WITHOUT firing — the orchestrating source stamps every dependent's reason up front, then fires the events in order.



1615
1616
1617
1618
# File 'lib/dommy/event.rb', line 1615

def __internal_set_reason__(reason)
  @aborted = true
  @reason = reason
end

#__internal_source_signals__Object



1601
1602
1603
# File 'lib/dommy/event.rb', line 1601

def __internal_source_signals__
  @source_signals
end

#__js_call__(method, args) ⇒ Object



1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
# File 'lib/dommy/event.rb', line 1538

def __js_call__(method, args)
  case method
  when "addEventListener"
    add_event_listener(args[0], args[1], args[2])
  when "removeEventListener"
    remove_event_listener(args[0], args[1], args[2])
  when "dispatchEvent"
    dispatch_event(args[0])
  when "throwIfAborted"
    throw_if_aborted
  when "__internalAddAbortAlgorithm"
    __internal_add_abort_algorithm__(args[0])
  end
end

#__js_get__(key) ⇒ Object



1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
# File 'lib/dommy/event.rb', line 1508

def __js_get__(key)
  case key
  when "aborted"
    @aborted
  when "reason"
    # A non-aborted signal's reason is `undefined` (not null); once aborted
    # it is the abort reason (an explicit value or the default AbortError).
    @aborted ? @reason : Bridge::UNDEFINED
  when "onabort"
    @onabort_handler
  else
    Bridge::ABSENT
  end
end

#__js_set__(key, value) ⇒ Object

signal.onabort = fn is an event-handler IDL attribute: it registers a single "abort" listener (replacing any previous one); null/undefined clears it. (Setting it after the signal is already aborted never fires.)



1526
1527
1528
1529
1530
1531
1532
1533
1534
# File 'lib/dommy/event.rb', line 1526

def __js_set__(key, value)
  return Bridge::UNHANDLED unless key == "onabort"

  remove_event_listener("abort", @onabort_handler) if @onabort_handler
  cleared = value.nil? || (defined?(Bridge::UNDEFINED) && value.equal?(Bridge::UNDEFINED))
  @onabort_handler = cleared ? nil : value
  add_event_listener("abort", @onabort_handler) if @onabort_handler
  nil
end

#aborted?Boolean

Returns:

  • (Boolean)


1486
1487
1488
# File 'lib/dommy/event.rb', line 1486

def aborted?
  @aborted
end

#invoke_abort_algorithm(algo) ⇒ Object

Run one abort algorithm (a JS callback over the bridge, or a Ruby proc). A throw is swallowed deliberately: WHATWG runs every abort algorithm and then fires the "abort" event regardless, so one algorithm's failure must not abort the run. (The sole caller — the Observable polyfill — already catches inside its own teardown, so this is a defensive backstop.)



1634
1635
1636
1637
1638
1639
1640
1641
1642
# File 'lib/dommy/event.rb', line 1634

def invoke_abort_algorithm(algo)
  if algo.respond_to?(:__js_call__)
    algo.__js_call__("call", [])
  elsif algo.respond_to?(:call)
    algo.call
  end
rescue StandardError
  nil
end

#reasonObject



1490
1491
1492
# File 'lib/dommy/event.rb', line 1490

def reason
  @reason
end

#throw_if_abortedObject Also known as: throwIfAborted

Spec: throws signal.reason if aborted, otherwise no-op. Used by consumer code that polls before doing async work.

Raises:



1496
1497
1498
1499
1500
1501
1502
1503
1504
# File 'lib/dommy/event.rb', line 1496

def throw_if_aborted
  return unless @aborted
  # An Exception reason (the default AbortError, or an explicit DOMException)
  # is raised so the bridge tags it as a real JS error; any other reason — a
  # string, number, or opaque JSValue — is thrown verbatim, identity kept.
  raise @reason if @reason.is_a?(Exception)

  raise Bridge::ThrowValue.new(@reason)
end