async 2.24.0 → 2.27.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 95c1315007ff80d78bdd1538d2b2963ed951db5aa92906ea6f22560612540cbc
4
- data.tar.gz: 03ffadcf7c2523827bd6c49340df0a846f8ab3214593d897b4359b548a765cef
3
+ metadata.gz: 4c9d36d758f8a197b7c00d3d2e4d83d280cfd8ddf4fb60bba1d7b9e95b64ec47
4
+ data.tar.gz: c0c19dcc509563b18a9385c1ff07a982054e6e984c9ed9c1fd715ee027c74f09
5
5
  SHA512:
6
- metadata.gz: 62c15e0c19e7f3277ca9e4981b0304d5f039367fb0d6b996a8c562be170b3436f66e97f33fcfd5eb0ed8ec84e760ae2b565a022604e026f3b2c797613534fd59
7
- data.tar.gz: 79c7148d35f3e06243b3845721b8a1ebcf5fd2e6244eab18ac5a0f42894088334df307c4da52d9d9c59b0d9499e457f0990022d570abf109202416cf168211a1
6
+ metadata.gz: b936e5c17d8e9e2eec7f3d9b3d2d4b00b5a16359cfb267a036f72fb254b53aeb234f8b9368ba1cd2ba41010438dd8da120621005bc1791fc554ee62647e46ed1
7
+ data.tar.gz: 808b0ce51e2bfa4a28d01126d59627409e6a25b37ee8e9e1f8146c138fcef995ab693e699f9c3ece326e234100a397c19e917a55c9d1e34dd797d26531e411f5
checksums.yaml.gz.sig CHANGED
Binary file
data/agent.md ADDED
@@ -0,0 +1,47 @@
1
+ # Agent
2
+
3
+ ## Context
4
+
5
+ This section provides links to documentation from installed packages. It is automatically generated and may be updated by running `bake agent:context:install`.
6
+
7
+ **Important:** Before performing any code, documentation, or analysis tasks, always read and apply the full content of any relevant documentation referenced in the following sections. These context files contain authoritative standards and best practices for documentation, code style, and project-specific workflows. **Do not proceed with any actions until you have read and incorporated the guidance from relevant context files.**
8
+
9
+ ### agent-context
10
+
11
+ Install and manage context files from Ruby gems.
12
+
13
+ #### [Usage Guide](.context/agent-context/usage.md)
14
+
15
+ `agent-context` is a tool that helps you discover and install contextual information from Ruby gems for AI agents. Gems can provide additional documentation, examples, and guidance in a `context/` ...
16
+
17
+ ### decode
18
+
19
+ Code analysis for documentation generation.
20
+
21
+ #### [Getting Started with Decode](.context/decode/getting-started.md)
22
+
23
+ The Decode gem provides programmatic access to Ruby code structure and metadata. It can parse Ruby files and extract definitions, comments, and documentation pragmas, enabling code analysis, docume...
24
+
25
+ #### [Documentation Coverage](.context/decode/coverage.md)
26
+
27
+ This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks.
28
+
29
+ #### [Ruby Documentation](.context/decode/ruby-documentation.md)
30
+
31
+ This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate A...
32
+
33
+ ### sus
34
+
35
+ A fast and scalable test runner.
36
+
37
+ #### [Using Sus Testing Framework](.context/sus/usage.md)
38
+
39
+ Sus is a modern Ruby testing framework that provides a clean, BDD-style syntax for writing tests. It's designed to be fast, simple, and expressive.
40
+
41
+ #### [Mocking](.context/sus/mocking.md)
42
+
43
+ There are two types of mocking in sus: `receive` and `mock`. The `receive` matcher is a subset of full mocking and is used to set expectations on method calls, while `mock` can be used to replace m...
44
+
45
+ #### [Shared Test Behaviors and Fixtures](.context/sus/shared.md)
46
+
47
+ Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files.
data/lib/async/barrier.md CHANGED
@@ -1,6 +1,5 @@
1
1
  A synchronization primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
2
2
 
3
-
4
3
  ## Example
5
4
 
6
5
  ~~~ ruby
data/lib/async/barrier.rb CHANGED
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2019-2024, by Samuel Williams.
4
+ # Copyright, 2019-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "list"
7
7
  require_relative "task"
8
+ require_relative "queue"
8
9
 
9
10
  module Async
10
11
  # A general purpose synchronisation primitive, which allows one task to wait for a number of other tasks to complete. It can be used in conjunction with {Semaphore}.
@@ -16,6 +17,7 @@ module Async
16
17
  # @public Since *Async v1*.
17
18
  def initialize(parent: nil)
18
19
  @tasks = List.new
20
+ @finished = Queue.new
19
21
 
20
22
  @parent = parent
21
23
  end
@@ -41,11 +43,15 @@ module Async
41
43
  # Execute a child task and add it to the barrier.
42
44
  # @asynchronous Executes the given block concurrently.
43
45
  def async(*arguments, parent: (@parent or Task.current), **options, &block)
44
- task = parent.async(*arguments, **options, &block)
46
+ waiting = nil
45
47
 
46
- @tasks.append(TaskNode.new(task))
47
-
48
- return task
48
+ parent.async(*arguments, **options) do |task, *arguments|
49
+ waiting = TaskNode.new(task)
50
+ @tasks.append(waiting)
51
+ block.call(task, *arguments)
52
+ ensure
53
+ @finished.signal(waiting)
54
+ end
49
55
  end
50
56
 
51
57
  # Whether there are any tasks being held by the barrier.
@@ -55,14 +61,27 @@ module Async
55
61
  end
56
62
 
57
63
  # Wait for all tasks to complete by invoking {Task#wait} on each waiting task, which may raise an error. As long as the task has completed, it will be removed from the barrier.
64
+ #
65
+ # @yields {|task| ...} If a block is given, the unwaited task is yielded. You must invoke {Task#wait} yourself. In addition, you may `break` if you have captured enough results.
66
+ #
58
67
  # @asynchronous Will wait for tasks to finish executing.
59
68
  def wait
60
- @tasks.each do |waiting|
69
+ while !@tasks.empty?
70
+ # Wait for a task to finish (we get the task node):
71
+ return unless waiting = @finished.wait
72
+
73
+ # Remove the task as it is now finishing:
74
+ @tasks.remove?(waiting)
75
+
76
+ # Get the task:
61
77
  task = waiting.task
62
- begin
78
+
79
+ # If a block is given, the user can implement their own behaviour:
80
+ if block_given?
81
+ yield task
82
+ else
83
+ # Wait for it to either complete or raise an error:
63
84
  task.wait
64
- ensure
65
- @tasks.remove?(waiting) unless task.alive?
66
85
  end
67
86
  end
68
87
  end
@@ -73,6 +92,8 @@ module Async
73
92
  @tasks.each do |waiting|
74
93
  waiting.task.stop
75
94
  end
95
+
96
+ @finished.close
76
97
  end
77
98
  end
78
99
  end
data/lib/async/clock.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2022, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  # A convenient wrapper around the internal monotonic clock.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2024, by Samuel Williams.
4
+ # Copyright, 2017-2025, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
 
7
7
  require "fiber"
@@ -42,6 +42,8 @@ module Async
42
42
 
43
43
  # @deprecated Replaced by {#waiting?}
44
44
  def empty?
45
+ warn("`Async::Condition#empty?` is deprecated, use `Async::Condition#waiting?` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
46
+
45
47
  @waiting.empty?
46
48
  end
47
49
 
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2025, by Samuel Williams.
5
5
 
6
6
  # The implementation lives in `queue.rb` but later we may move it here for better autoload/inference.
7
7
  require_relative "queue"
8
+
9
+ module Async
10
+ class LimitedQueue < Queue
11
+ singleton_class.remove_method(:new)
12
+ end
13
+ end
data/lib/async/list.rb CHANGED
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2022-2024, by Samuel Williams.
4
+ # Copyright, 2022-2025, by Samuel Williams.
5
+ # Copyright, 2025, by Shopify Inc.
5
6
 
6
7
  module Async
7
8
  # A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
@@ -18,6 +19,7 @@ module Async
18
19
  sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
19
20
  end
20
21
 
22
+ # @returns [String] A short summary of the list.
21
23
  alias inspect to_s
22
24
 
23
25
  # Fast, safe, unbounded accumulation of children.
@@ -134,7 +136,7 @@ module Async
134
136
  return removed(node)
135
137
  end
136
138
 
137
- # @returns [Boolean] Returns true if the list is empty.
139
+ # @returns [Boolean] True if the list is empty.
138
140
  def empty?
139
141
  @size == 0
140
142
  end
@@ -143,26 +145,26 @@ module Async
143
145
  # previous = self
144
146
  # current = @tail
145
147
  # found = node.equal?(self)
146
-
148
+
147
149
  # while true
148
150
  # break if current.equal?(self)
149
-
151
+
150
152
  # if current.head != previous
151
153
  # raise "Invalid previous linked list node!"
152
154
  # end
153
-
155
+
154
156
  # if current.is_a?(List) and !current.equal?(self)
155
157
  # raise "Invalid list in list node!"
156
158
  # end
157
-
159
+
158
160
  # if node
159
161
  # found ||= current.equal?(node)
160
162
  # end
161
-
163
+
162
164
  # previous = current
163
165
  # current = current.tail
164
166
  # end
165
-
167
+
166
168
  # if node and !found
167
169
  # raise "Node not found in list!"
168
170
  # end
@@ -238,6 +240,12 @@ module Async
238
240
  attr_accessor :head
239
241
  attr_accessor :tail
240
242
 
243
+ # @returns [String] A string representation of the node.
244
+ def to_s
245
+ sprintf("#<%s:0x%x>", self.class.name, object_id)
246
+ end
247
+
248
+ # @returns [String] A string representation of the node.
241
249
  alias inspect to_s
242
250
  end
243
251
 
data/lib/async/node.rb CHANGED
@@ -4,6 +4,7 @@
4
4
  # Copyright, 2017-2024, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2022, by Shannon Skipper.
7
+ # Copyright, 2025, by Shopify Inc.
7
8
 
8
9
  require "fiber/annotation"
9
10
 
@@ -180,6 +181,7 @@ module Async
180
181
  "\#<#{self.description}>"
181
182
  end
182
183
 
184
+ # @returns [String] A description of the node.
183
185
  alias inspect to_s
184
186
 
185
187
  # Change the parent of this node.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2024, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
 
6
6
  require_relative "condition"
7
7
 
@@ -10,12 +10,14 @@ module Async
10
10
  # @public Since *Async v1*.
11
11
  class Notification < Condition
12
12
  # Signal to a given task that it should resume operations.
13
+ #
14
+ # @returns [Boolean] if a task was signalled.
13
15
  def signal(value = nil, task: Task.current)
14
- return if @waiting.empty?
16
+ return false if @waiting.empty?
15
17
 
16
18
  Fiber.scheduler.push Signal.new(self.exchange, value)
17
19
 
18
- return nil
20
+ return true
19
21
  end
20
22
 
21
23
  Signal = Struct.new(:waiting, :value) do
data/lib/async/queue.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2018-2024, by Samuel Williams.
4
+ # Copyright, 2018-2025, by Samuel Williams.
5
5
  # Copyright, 2019, by Ryan Musgrave.
6
6
  # Copyright, 2020-2022, by Bruno Sutic.
7
+ # Copyright, 2025, by Jahfer Husain.
8
+ # Copyright, 2025, by Shopify Inc.
7
9
 
8
10
  require_relative "notification"
9
11
 
@@ -14,16 +16,31 @@ module Async
14
16
  #
15
17
  # @public Since *Async v1*.
16
18
  class Queue
19
+ # An error raised when trying to enqueue items to a closed queue.
20
+ # @public Since *Async v2.24*.
21
+ class ClosedError < RuntimeError
22
+ end
23
+
17
24
  # Create a new queue.
18
25
  #
19
26
  # @parameter parent [Interface(:async) | Nil] The parent task to use for async operations.
20
27
  # @parameter available [Notification] The notification to use for signaling when items are available.
21
28
  def initialize(parent: nil, available: Notification.new)
22
29
  @items = []
30
+ @closed = false
23
31
  @parent = parent
24
32
  @available = available
25
33
  end
26
34
 
35
+ # Close the queue, causing all waiting tasks to return `nil`. Any subsequent calls to {enqueue} will raise an exception.
36
+ def close
37
+ @closed = true
38
+
39
+ while @available.waiting?
40
+ @available.signal(nil)
41
+ end
42
+ end
43
+
27
44
  # @attribute [Array] The items in the queue.
28
45
  attr :items
29
46
 
@@ -39,6 +56,10 @@ module Async
39
56
 
40
57
  # Add an item to the queue.
41
58
  def push(item)
59
+ if @closed
60
+ raise ClosedError, "Cannot push items to a closed queue."
61
+ end
62
+
42
63
  @items << item
43
64
 
44
65
  @available.signal unless self.empty?
@@ -51,6 +72,10 @@ module Async
51
72
 
52
73
  # Add multiple items to the queue.
53
74
  def enqueue(*items)
75
+ if @closed
76
+ raise ClosedError, "Cannot enqueue items to a closed queue."
77
+ end
78
+
54
79
  @items.concat(items)
55
80
 
56
81
  @available.signal unless self.empty?
@@ -59,6 +84,10 @@ module Async
59
84
  # Remove and return the next item from the queue.
60
85
  def dequeue
61
86
  while @items.empty?
87
+ if @closed
88
+ return nil
89
+ end
90
+
62
91
  @available.wait
63
92
  end
64
93
 
@@ -105,6 +134,13 @@ module Async
105
134
  # A queue which limits the number of items that can be enqueued.
106
135
  # @public Since *Async v1*.
107
136
  class LimitedQueue < Queue
137
+ # @private This exists purely for emitting a warning.
138
+ def self.new(...)
139
+ warn("`require 'async/limited_queue'` to use `Async::LimitedQueue`.", uplevel: 1, category: :deprecated) if $VERBOSE
140
+
141
+ super
142
+ end
143
+
108
144
  # Create a new limited queue.
109
145
  #
110
146
  # @parameter limit [Integer] The maximum number of items that can be enqueued.
@@ -119,9 +155,19 @@ module Async
119
155
  # @attribute [Integer] The maximum number of items that can be enqueued.
120
156
  attr :limit
121
157
 
158
+ # Close the queue, causing all waiting tasks to return `nil`. Any subsequent calls to {enqueue} will raise an exception.
159
+ # Also signals all tasks waiting for the queue to be full.
160
+ def close
161
+ super
162
+
163
+ while @full.waiting?
164
+ @full.signal(nil)
165
+ end
166
+ end
167
+
122
168
  # @returns [Boolean] Whether trying to enqueue an item would block.
123
169
  def limited?
124
- @items.size >= @limit
170
+ !@closed && @items.size >= @limit
125
171
  end
126
172
 
127
173
  # Add an item to the queue.
@@ -148,6 +194,10 @@ module Async
148
194
  @full.wait
149
195
  end
150
196
 
197
+ if @closed
198
+ raise ClosedError, "Cannot enqueue items to a closed queue."
199
+ end
200
+
151
201
  available = @limit - @items.size
152
202
  @items.concat(items.shift(available))
153
203
 
data/lib/async/reactor.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2017-2024, by Samuel Williams.
4
+ # Copyright, 2017-2025, by Samuel Williams.
5
5
  # Copyright, 2017, by Kent Gruber.
6
6
  # Copyright, 2018, by Sokolov Yura.
7
7
 
@@ -12,6 +12,8 @@ module Async
12
12
  class Reactor < Scheduler
13
13
  # @deprecated Replaced by {Kernel::Async}.
14
14
  def self.run(...)
15
+ warn("`Async::Reactor.run{}` is deprecated, use `Async{}` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
16
+
15
17
  Async(...)
16
18
  end
17
19
 
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2020-2024, by Samuel Williams.
4
+ # Copyright, 2020-2025, by Samuel Williams.
5
5
  # Copyright, 2020, by Jun Jiang.
6
6
  # Copyright, 2021, by Julien Portalier.
7
+ # Copyright, 2025, by Shopify Inc.
7
8
 
8
9
  require_relative "clock"
9
10
  require_relative "task"
10
11
  require_relative "timeout"
11
- require_relative "worker_pool"
12
12
 
13
13
  require "io/event"
14
14
 
@@ -46,6 +46,28 @@ module Async
46
46
  true
47
47
  end
48
48
 
49
+ # Used to augment the scheduler to add support for blocking operations.
50
+ module BlockingOperationWait
51
+ # Wait for the given work to be executed.
52
+ #
53
+ # @public Since *Async v2.21* and *Ruby v3.4*.
54
+ # @asynchronous May be non-blocking.
55
+ #
56
+ # @parameter work [Proc] The work to execute on a background thread.
57
+ # @returns [Object] The result of the work.
58
+ def blocking_operation_wait(work)
59
+ @worker_pool.call(work)
60
+ end
61
+ end
62
+
63
+ private_constant :BlockingOperationWait
64
+
65
+ if ::IO::Event.const_defined?(:WorkerPool)
66
+ WorkerPool = ::IO::Event::WorkerPool
67
+ else
68
+ WorkerPool = nil
69
+ end
70
+
49
71
  # Create a new scheduler.
50
72
  #
51
73
  # @public Since *Async v1*.
@@ -65,14 +87,15 @@ module Async
65
87
  @idle_time = 0.0
66
88
 
67
89
  @timers = ::IO::Event::Timers.new
90
+
68
91
  if worker_pool == true
69
- @worker_pool = WorkerPool.new
92
+ @worker_pool = WorkerPool&.new
70
93
  else
71
94
  @worker_pool = worker_pool
72
95
  end
73
96
 
74
97
  if @worker_pool
75
- self.singleton_class.prepend(WorkerPool::BlockingOperationWait)
98
+ self.singleton_class.prepend(BlockingOperationWait)
76
99
  end
77
100
  end
78
101
 
@@ -97,7 +120,7 @@ module Async
97
120
  return @busy_time / total_time
98
121
  end
99
122
  end
100
-
123
+
101
124
  # Invoked when the fiber scheduler is being closed.
102
125
  #
103
126
  # Executes the run loop until all tasks are finished, then closes the scheduler.
@@ -234,7 +257,7 @@ module Async
234
257
  # @parameter blocker [Object] The object that was blocking the fiber.
235
258
  # @parameter fiber [Fiber] The fiber to unblock.
236
259
  def unblock(blocker, fiber)
237
- # $stderr.puts "unblock(#{blocker}, #{fiber})"
260
+ # Fiber.blocking{$stderr.puts "unblock(#{blocker}, #{fiber})"}
238
261
 
239
262
  # This operation is protected by the GVL:
240
263
  if selector = @selector
@@ -250,6 +273,8 @@ module Async
250
273
  #
251
274
  # @parameter duration [Numeric | Nil] The time in seconds to sleep, or if nil, indefinitely.
252
275
  def kernel_sleep(duration = nil)
276
+ # Fiber.blocking{$stderr.puts "kernel_sleep(#{duration}, #{Fiber.current})"}
277
+
253
278
  if duration
254
279
  self.block(nil, duration)
255
280
  else
@@ -348,6 +373,34 @@ module Async
348
373
  end
349
374
  end
350
375
 
376
+ # Used to defer stopping the current task until later.
377
+ class FiberInterrupt
378
+ # Create a new stop later operation.
379
+ #
380
+ # @parameter task [Task] The task to stop later.
381
+ def initialize(fiber, exception)
382
+ @fiber = fiber
383
+ @exception = exception
384
+ end
385
+
386
+ # @returns [Boolean] Whether the task is alive.
387
+ def alive?
388
+ @fiber.alive?
389
+ end
390
+
391
+ # Transfer control to the operation - this will stop the task.
392
+ def transfer
393
+ # Fiber.blocking{$stderr.puts "FiberInterrupt#transfer(#{@fiber}, #{@exception})"}
394
+ @fiber.raise(@exception)
395
+ end
396
+ end
397
+
398
+ # Raise an exception on the specified fiber, waking up the event loop if necessary.
399
+ def fiber_interrupt(fiber, exception)
400
+ # Fiber.blocking{$stderr.puts "fiber_interrupt(#{fiber}, #{exception})"}
401
+ unblock(nil, FiberInterrupt.new(fiber, exception))
402
+ end
403
+
351
404
  # Wait for the specified process ID to exit.
352
405
  #
353
406
  # @public Since *Async v2*.
@@ -361,6 +414,19 @@ module Async
361
414
  return @selector.process_wait(Fiber.current, pid, flags)
362
415
  end
363
416
 
417
+ # Wait for the specified IOs to become ready for the specified events.
418
+ #
419
+ # @public Since *Async v2.25*.
420
+ # @asynchronous May be non-blocking.
421
+ def io_select(...)
422
+ Thread.new do
423
+ # Don't make unnecessary output, since we will propagate the exception:
424
+ Thread.current.report_on_exception = false
425
+
426
+ ::IO.select(...)
427
+ end.value
428
+ end
429
+
364
430
  # Run one iteration of the event loop.
365
431
  #
366
432
  # When terminating the event loop, we already know we are finished. So we don't need to check the task tree. This is a logical requirement because `run_once` ignores transient tasks. For example, a single top level transient task is not enough to keep the reactor running, but during termination we must still process it in order to terminate child tasks.
@@ -517,7 +583,7 @@ module Async
517
583
  # @yields {|task| ...} Executed within the task.
518
584
  # @returns [Task] The task that was scheduled into the reactor.
519
585
  def async(*arguments, **options, &block)
520
- # warn "Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated
586
+ warn("Async::Scheduler#async is deprecated. Use `run` or `Task#async` instead.", uplevel: 1, category: :deprecated) if $VERBOSE
521
587
 
522
588
  Kernel.raise ClosedError if @selector.nil?
523
589
 
@@ -528,6 +594,8 @@ module Async
528
594
  return task
529
595
  end
530
596
 
597
+ # Create a new fiber and return it without starting execution.
598
+ # @returns [Fiber] The fiber that was created.
531
599
  def fiber(...)
532
600
  return async(...).fiber
533
601
  end
data/lib/async/stop.rb ADDED
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2025, by Samuel Williams.
5
+
6
+ require "fiber"
7
+ require "console"
8
+
9
+ module Async
10
+ # Raised when a task is explicitly stopped.
11
+ class Stop < Exception
12
+ # Represents the source of the stop operation.
13
+ class Cause < Exception
14
+ if RUBY_VERSION >= "3.4"
15
+ # @returns [Array(Thread::Backtrace::Location)] The backtrace of the caller.
16
+ def self.backtrace
17
+ caller_locations(2..-1)
18
+ end
19
+ else
20
+ # @returns [Array(String)] The backtrace of the caller.
21
+ def self.backtrace
22
+ caller(2..-1)
23
+ end
24
+ end
25
+
26
+ # Create a new cause of the stop operation, with the given message.
27
+ #
28
+ # @parameter message [String] The error message.
29
+ # @returns [Cause] The cause of the stop operation.
30
+ def self.for(message = "Task was stopped")
31
+ instance = self.new(message)
32
+ instance.set_backtrace(self.backtrace)
33
+ return instance
34
+ end
35
+ end
36
+
37
+ if RUBY_VERSION < "3.5"
38
+ # Create a new stop operation.
39
+ #
40
+ # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}
41
+ #
42
+ # @parameter message [String | Hash] The error message or a hash containing the cause.
43
+ def initialize(message = "Task was stopped")
44
+ if message.is_a?(Hash)
45
+ @cause = message[:cause]
46
+ message = "Task was stopped"
47
+ end
48
+
49
+ super(message)
50
+ end
51
+
52
+ # @returns [Exception] The cause of the stop operation.
53
+ #
54
+ # This is a compatibility method for Ruby versions before 3.5 where cause is not propagated correctly when using {Fiber#raise}, we explicitly capture the cause here.
55
+ def cause
56
+ super || @cause
57
+ end
58
+ end
59
+
60
+ # Used to defer stopping the current task until later.
61
+ class Later
62
+ # Create a new stop later operation.
63
+ #
64
+ # @parameter task [Task] The task to stop later.
65
+ # @parameter cause [Exception] The cause of the stop operation.
66
+ def initialize(task, cause = nil)
67
+ @task = task
68
+ @cause = cause
69
+ end
70
+
71
+ # @returns [Boolean] Whether the task is alive.
72
+ def alive?
73
+ true
74
+ end
75
+
76
+ # Transfer control to the operation - this will stop the task.
77
+ def transfer
78
+ @task.stop(false, cause: @cause)
79
+ end
80
+ end
81
+ end
82
+ end