Class: Dommy::AbortSignal
- Inherits:
-
Object
- Object
- Dommy::AbortSignal
- 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
nullreason (abort(null)keeps reason null, butabort()/abort(undefined)default to a fresh AbortError). Object.new
Class Method Summary collapse
-
.abort(*reason) ⇒ Object
Spec:
AbortSignal.abort(reason?)returns a fresh, pre-aborted signal. -
.any(signals) ⇒ Object
Spec:
AbortSignal.any([sig, ...])returns a composite signal that aborts as soon as any of the inputs aborts. -
.timeout(ms, scheduler: nil) ⇒ Object
Spec:
AbortSignal.timeout(ms)returns a signal that aborts itself aftermsmilliseconds with aTimeoutErrorreason.
Instance Method Summary collapse
-
#__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.
- #__internal_add_dependent__(dependent) ⇒ Object
- #__internal_add_source__(source) ⇒ Object
- #__internal_dependent__? ⇒ Boolean
-
#__internal_make_dependent__ ⇒ Object
----- dependent-signal plumbing (package-private, used by .any) -----.
-
#__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).
-
#__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.
-
#__internal_schedule_thread_timeout__(ms, reason) ⇒ Object
Background-thread timeout used by
AbortSignal.timeoutwhen no scheduler is provided. -
#__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.
- #__internal_source_signals__ ⇒ Object
- #__js_call__(method, args) ⇒ Object
- #__js_get__(key) ⇒ Object
-
#__js_set__(key, value) ⇒ Object
signal.onabort = fnis an event-handler IDL attribute: it registers a single "abort" listener (replacing any previous one); null/undefined clears it. - #aborted? ⇒ Boolean
-
#initialize ⇒ AbortSignal
constructor
A new instance of AbortSignal.
-
#invoke_abort_algorithm(algo) ⇒ Object
Run one abort algorithm (a JS callback over the bridge, or a Ruby proc).
- #reason ⇒ Object
-
#throw_if_aborted ⇒ Object
(also: #throwIfAborted)
Spec: throws
signal.reasonif aborted, otherwise no-op.
Methods included from Bridge::Methods
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
#initialize ⇒ AbortSignal
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
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
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 |
#reason ⇒ Object
1490 1491 1492 |
# File 'lib/dommy/event.rb', line 1490 def reason @reason end |
#throw_if_aborted ⇒ Object Also known as: throwIfAborted
Spec: throws signal.reason if aborted, otherwise no-op. Used by
consumer code that polls before doing async work.
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 |