async-enumerable 0.1.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 (70) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.standard.yml +5 -0
  4. data/CHANGELOG.md +5 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +416 -0
  8. data/Rakefile +127 -0
  9. data/benchmark/async_all.yaml +38 -0
  10. data/benchmark/async_any.yaml +39 -0
  11. data/benchmark/async_each.yaml +51 -0
  12. data/benchmark/async_find.yaml +37 -0
  13. data/benchmark/async_map.yaml +50 -0
  14. data/benchmark/async_select.yaml +31 -0
  15. data/benchmark/early_termination/any_early.yaml +17 -0
  16. data/benchmark/early_termination/any_late.yaml +17 -0
  17. data/benchmark/early_termination/find_middle.yaml +17 -0
  18. data/benchmark/size_comparison/map_10.yaml +17 -0
  19. data/benchmark/size_comparison/map_100.yaml +17 -0
  20. data/benchmark/size_comparison/map_1000.yaml +20 -0
  21. data/benchmark/size_comparison/map_10000.yaml +23 -0
  22. data/docs/reference/README.md +43 -0
  23. data/docs/reference/concurrency_bounder.md +234 -0
  24. data/docs/reference/enumerable.md +258 -0
  25. data/docs/reference/enumerator.md +221 -0
  26. data/docs/reference/methods/converters.md +97 -0
  27. data/docs/reference/methods/predicates.md +254 -0
  28. data/docs/reference/methods/transformers.md +104 -0
  29. data/lib/async/enumerable/comparable.rb +26 -0
  30. data/lib/async/enumerable/concurrency_bounder.rb +37 -0
  31. data/lib/async/enumerable/configurable.rb +140 -0
  32. data/lib/async/enumerable/methods/aggregators.rb +40 -0
  33. data/lib/async/enumerable/methods/converters.rb +21 -0
  34. data/lib/async/enumerable/methods/each.rb +39 -0
  35. data/lib/async/enumerable/methods/iterators.rb +27 -0
  36. data/lib/async/enumerable/methods/predicates/all.rb +47 -0
  37. data/lib/async/enumerable/methods/predicates/any.rb +47 -0
  38. data/lib/async/enumerable/methods/predicates/find.rb +55 -0
  39. data/lib/async/enumerable/methods/predicates/find_index.rb +50 -0
  40. data/lib/async/enumerable/methods/predicates/include.rb +23 -0
  41. data/lib/async/enumerable/methods/predicates/none.rb +27 -0
  42. data/lib/async/enumerable/methods/predicates/one.rb +48 -0
  43. data/lib/async/enumerable/methods/predicates.rb +29 -0
  44. data/lib/async/enumerable/methods/slicers.rb +34 -0
  45. data/lib/async/enumerable/methods/transformers/compact.rb +18 -0
  46. data/lib/async/enumerable/methods/transformers/filter_map.rb +19 -0
  47. data/lib/async/enumerable/methods/transformers/flat_map.rb +20 -0
  48. data/lib/async/enumerable/methods/transformers/map.rb +22 -0
  49. data/lib/async/enumerable/methods/transformers/reject.rb +19 -0
  50. data/lib/async/enumerable/methods/transformers/select.rb +21 -0
  51. data/lib/async/enumerable/methods/transformers/sort.rb +18 -0
  52. data/lib/async/enumerable/methods/transformers/sort_by.rb +19 -0
  53. data/lib/async/enumerable/methods/transformers/uniq.rb +18 -0
  54. data/lib/async/enumerable/methods/transformers.rb +35 -0
  55. data/lib/async/enumerable/methods.rb +26 -0
  56. data/lib/async/enumerable/version.rb +10 -0
  57. data/lib/async/enumerable.rb +72 -0
  58. data/lib/async/enumerator.rb +33 -0
  59. data/lib/enumerable/async.rb +38 -0
  60. data/scripts/debug_config.rb +26 -0
  61. data/scripts/debug_config2.rb +34 -0
  62. data/scripts/sketch.rb +30 -0
  63. data/scripts/test_aggregators.rb +66 -0
  64. data/scripts/test_ancestors.rb +12 -0
  65. data/scripts/test_async_chaining.rb +30 -0
  66. data/scripts/test_direct_method_calls.rb +53 -0
  67. data/scripts/test_example.rb +37 -0
  68. data/scripts/test_issue_7.rb +69 -0
  69. data/scripts/test_method_source.rb +15 -0
  70. metadata +145 -0
@@ -0,0 +1,97 @@
1
+ # Converter Methods
2
+
3
+ Converter methods transform async enumerables into other data types, typically arrays.
4
+
5
+ ## to_a
6
+
7
+ Converts the wrapped enumerable to an array.
8
+
9
+ This method simply converts the wrapped enumerable to an array without any async processing. Note that async operations like map and select already return arrays internally.
10
+
11
+ ### Returns
12
+ `Array` - The wrapped enumerable converted to an array
13
+
14
+ ### Examples
15
+
16
+ ```ruby
17
+ # Basic conversion
18
+ async_enum = (1..3).async
19
+ async_enum.to_a # => [1, 2, 3]
20
+
21
+ # Converting a Set
22
+ async_set = Set[1, 2, 3].async
23
+ async_set.to_a # => [1, 2, 3] (order may vary)
24
+
25
+ # After transformations
26
+ [1, 2, 3].async.map { |n| n * 2 }.to_a # => [2, 4, 6]
27
+ ```
28
+
29
+ ### Implementation Notes
30
+ - Uses `enumerable_source` to get the appropriate source
31
+ - Handles self-referential sources to avoid infinite recursion
32
+ - Delegates to the source's `to_a` method
33
+
34
+ ## sync
35
+
36
+ Synchronizes the async enumerable back to a regular array. This is an alias for `to_a` that provides a more semantic way to end an async chain and get the results.
37
+
38
+ ### Returns
39
+ `Array` - The wrapped enumerable converted to an array
40
+
41
+ ### Examples
42
+
43
+ ```ruby
44
+ # Chaining with sync
45
+ result = [:foo, :bar].async
46
+ .map { |sym| fetch_data(sym) }
47
+ .sync
48
+ # => [<data for :foo>, <data for :bar>]
49
+
50
+ # Alternative to to_a
51
+ data.async.select { |x| x.valid? }.sync # same as .to_a
52
+
53
+ # Complete async pipeline
54
+ [1, 2, 3, 4, 5].async
55
+ .map { |n| expensive_operation(n) }
56
+ .select { |result| result.success? }
57
+ .map { |result| result.value }
58
+ .sync # Materializes final results
59
+ ```
60
+
61
+ ### Why Use sync?
62
+
63
+ The `sync` method provides semantic clarity:
64
+ - `to_a` implies conversion to array format
65
+ - `sync` implies waiting for async operations to complete and collecting results
66
+ - Both do the same thing, but `sync` better expresses intent in async contexts
67
+
68
+ ## Usage Patterns
69
+
70
+ ### Basic Conversion
71
+ ```ruby
72
+ # Simple enumerable to array
73
+ (1..5).async.to_a # => [1, 2, 3, 4, 5]
74
+ ```
75
+
76
+ ### After Async Operations
77
+ ```ruby
78
+ # Process data asynchronously, then collect results
79
+ urls.async
80
+ .map { |url| fetch_data(url) }
81
+ .select { |data| data.valid? }
82
+ .sync # Get final array of valid data
83
+ ```
84
+
85
+ ### With Custom Enumerables
86
+ ```ruby
87
+ class MyCollection
88
+ include Enumerable
89
+ def each
90
+ yield 1
91
+ yield 2
92
+ yield 3
93
+ end
94
+ end
95
+
96
+ MyCollection.new.async.to_a # => [1, 2, 3]
97
+ ```
@@ -0,0 +1,254 @@
1
+ # Predicate Methods
2
+
3
+ Predicate methods test elements in the enumerable and return boolean values. All async predicate methods support early termination - they stop processing as soon as the result is determined.
4
+
5
+ ## any?
6
+
7
+ Asynchronously checks if any element satisfies the given condition.
8
+
9
+ Executes the block for each element in parallel and returns true as soon as any element returns a truthy value. Short-circuits and stops processing remaining elements once a match is found.
10
+
11
+ ### Parameters
12
+ - `pattern` (optional): Pattern to match against elements
13
+ - `&block`: Block to test each element
14
+
15
+ ### Returns
16
+ `Boolean` - true if any element satisfies the condition, false otherwise
17
+
18
+ ### Examples
19
+
20
+ ```ruby
21
+ # Check if any number is negative
22
+ [1, 2, -3].async.any? { |n| n < 0 } # => true (stops after -3)
23
+ [1, 2, 3].async.any? { |n| n < 0 } # => false
24
+
25
+ # With API calls
26
+ servers.async.any? { |server| server_responding?(server) }
27
+ # Checks all servers in parallel, returns true on first response
28
+ ```
29
+
30
+ ### Implementation Notes
31
+ - Uses `Concurrent::AtomicBoolean` for thread-safe early termination
32
+ - Delegates pattern/no-block cases to wrapped enumerable to avoid break issues
33
+ - Stops barrier execution as soon as a match is found
34
+
35
+ ## all?
36
+
37
+ Asynchronously checks if all elements satisfy the given condition.
38
+
39
+ Executes the block for each element in parallel and returns false as soon as any element returns a falsy value. Short-circuits and stops processing remaining elements once a non-match is found.
40
+
41
+ ### Parameters
42
+ - `pattern` (optional): Pattern to match against elements
43
+ - `&block`: Block to test each element
44
+
45
+ ### Returns
46
+ `Boolean` - true if all elements satisfy the condition, false otherwise
47
+
48
+ ### Examples
49
+
50
+ ```ruby
51
+ # Check if all numbers are positive
52
+ [1, 2, 3].async.all? { |n| n > 0 } # => true
53
+ [1, -2, 3].async.all? { |n| n > 0 } # => false (stops after -2)
54
+
55
+ # With validation
56
+ forms.async.all? { |form| validate_form(form) }
57
+ # Validates all forms in parallel, returns false on first invalid
58
+ ```
59
+
60
+ ### Implementation Notes
61
+ - Uses `Concurrent::AtomicBoolean` to track if any element fails the test
62
+ - Delegates pattern/no-block cases to wrapped enumerable
63
+ - Stops barrier execution as soon as a non-match is found
64
+
65
+ ## none?
66
+
67
+ Asynchronously checks if no elements satisfy the given condition.
68
+
69
+ Executes the block for each element in parallel and returns false as soon as any element returns a truthy value. Short-circuits and stops processing remaining elements once a match is found.
70
+
71
+ ### Parameters
72
+ - `pattern` (optional): Pattern to match against elements
73
+ - `&block`: Block to test each element
74
+
75
+ ### Returns
76
+ `Boolean` - true if no elements satisfy the condition, false otherwise
77
+
78
+ ### Examples
79
+
80
+ ```ruby
81
+ # Check if no numbers are negative
82
+ [1, 2, 3].async.none? { |n| n < 0 } # => true
83
+ [1, -2, 3].async.none? { |n| n < 0 } # => false (stops after -2)
84
+
85
+ # With validation
86
+ errors.async.none? { |error| error.critical? }
87
+ # Checks all errors in parallel, returns false on first critical
88
+ ```
89
+
90
+ ### Implementation Notes
91
+ - Uses `Concurrent::AtomicBoolean` to track if any element matches
92
+ - Essentially the inverse of `any?`
93
+ - Delegates pattern/no-block cases to wrapped enumerable
94
+
95
+ ## one?
96
+
97
+ Asynchronously checks if exactly one element satisfies the given condition.
98
+
99
+ Executes the block for each element in parallel and returns true if exactly one element returns a truthy value. Short-circuits and returns false as soon as a second match is found.
100
+
101
+ ### Parameters
102
+ - `pattern` (optional): Pattern to match against elements
103
+ - `&block`: Block to test each element
104
+
105
+ ### Returns
106
+ `Boolean` - true if exactly one element satisfies the condition
107
+
108
+ ### Examples
109
+
110
+ ```ruby
111
+ # Check for single admin
112
+ users.async.one? { |u| u.admin? } # => true if exactly one admin
113
+
114
+ # With validation
115
+ configs.async.one? { |c| c.primary? }
116
+ # Validates all configs in parallel, ensures only one is primary
117
+ ```
118
+
119
+ ### Implementation Notes
120
+ - Uses `Concurrent::AtomicFixnum` to count matches
121
+ - Stops barrier execution when count exceeds 1
122
+ - Delegates pattern/no-block cases to wrapped enumerable
123
+
124
+ ## find / detect
125
+
126
+ Asynchronously finds the first element that satisfies the given condition.
127
+
128
+ **Important:** Returns the **fastest completing** match, not necessarily the first element by position in the collection. Due to parallel execution, whichever element completes evaluation first will be returned. If you need the first element by position, use synchronous `find` instead.
129
+
130
+ ### Parameters
131
+ - `ifnone` (optional): Proc to call if no element is found
132
+ - `&block`: Block to test each element
133
+
134
+ ### Returns
135
+ The first matching element, or nil/ifnone result if not found
136
+
137
+ ### Examples
138
+
139
+ ```ruby
140
+ # Find any prime number (fastest to compute)
141
+ numbers.async.find { |n| prime?(n) }
142
+
143
+ # With fallback
144
+ users.async.find(-> { User.new }) { |u| u.admin? }
145
+ # Returns new User if no admin found
146
+
147
+ # With expensive checks - returns fastest result
148
+ documents.async.find { |doc| analyze_content(doc).contains_keyword? }
149
+ # Analyzes all documents in parallel, returns fastest match
150
+
151
+ # When order matters, use synchronous version
152
+ first_prime = numbers.find { |n| prime?(n) }
153
+ ```
154
+
155
+ ### Implementation Notes
156
+ - Uses `Concurrent::AtomicReference` with compare-and-set for first completion
157
+ - Returns whichever matching element completes evaluation first
158
+ - Supports `ifnone` proc for custom fallback behavior
159
+ - Stops all remaining evaluations once a match is found
160
+
161
+ ## find_index
162
+
163
+ Asynchronously finds the index of the first element that satisfies the given condition.
164
+
165
+ **Important:** Returns the index of the **fastest completing** match, not necessarily the first by position in the collection. Due to parallel execution, whichever element completes evaluation first will have its index returned. If you need the first index by position, use synchronous `find_index` instead.
166
+
167
+ ### Parameters
168
+ - `value` (optional): Value to find the index of
169
+ - `&block`: Block to test each element
170
+
171
+ ### Returns
172
+ `Integer` or `nil` - Index of first matching element, or nil if not found
173
+
174
+ ### Examples
175
+
176
+ ```ruby
177
+ # Find index of any large file (fastest to check)
178
+ files.async.find_index { |f| f.size > 1_000_000 }
179
+
180
+ # Find specific value - returns index of fastest equality check
181
+ items.async.find_index("target")
182
+
183
+ # With validation - returns index of fastest validation
184
+ results.async.find_index { |r| r.status == :success }
185
+
186
+ # When order matters, use synchronous version
187
+ first_index = data.find_index { |item| expensive_check(item) }
188
+ ```
189
+
190
+ ### Implementation Notes
191
+ - Uses `Concurrent::AtomicReference` with compare-and-set for first completion
192
+ - Returns index of whichever element completes evaluation first
193
+ - Handles both value-based and block-based searches
194
+ - Stops all remaining evaluations once a match is found
195
+
196
+ ## include? / member?
197
+
198
+ Asynchronously checks if the enumerable includes a given value.
199
+
200
+ Checks all elements in parallel for equality with the given value. Short-circuits and returns true as soon as a match is found.
201
+
202
+ ### Parameters
203
+ - `obj`: Object to search for
204
+
205
+ ### Returns
206
+ `Boolean` - true if the enumerable includes the object
207
+
208
+ ### Examples
209
+
210
+ ```ruby
211
+ # Check for value
212
+ [1, 2, 3].async.include?(2) # => true
213
+
214
+ # With objects
215
+ users.async.include?(target_user)
216
+
217
+ # Complex equality
218
+ configs.async.include?(production_config)
219
+ ```
220
+
221
+ ### Implementation Notes
222
+ - Uses `Concurrent::AtomicBoolean` for thread-safe early termination
223
+ - Relies on the object's `==` method for comparison
224
+ - Short-circuits on first match
225
+
226
+ ## Pattern Matching Support
227
+
228
+ When called with a pattern argument instead of a block, predicate methods delegate to the wrapped enumerable's synchronous implementation. This ensures correct behavior with Ruby's pattern matching:
229
+
230
+ ```ruby
231
+ # Pattern matching
232
+ [1, 2, 3].async.any?(Integer) # => true
233
+ [:a, :b].async.all?(Symbol) # => true
234
+ [1, 2, 3].async.none?(String) # => true
235
+ [1, 2, 3].async.one?(2) # => true
236
+ ```
237
+
238
+ ## No-Block Behavior
239
+
240
+ When called without a block or pattern, predicate methods check for truthiness:
241
+
242
+ ```ruby
243
+ [nil, false, 1].async.any? # => true (1 is truthy)
244
+ [1, 2, 3].async.all? # => true (all truthy)
245
+ [nil, false].async.none? # => true (none truthy)
246
+ [nil, false, 1].async.one? # => true (exactly one truthy)
247
+ ```
248
+
249
+ ## Performance Considerations
250
+
251
+ - All predicate methods use early termination to minimize unnecessary work
252
+ - Thread-safe atomic variables prevent race conditions
253
+ - Barrier stops immediately when result is determined
254
+ - Pattern/no-block cases delegate to avoid async overhead when not needed
@@ -0,0 +1,104 @@
1
+ # Transformer Methods
2
+
3
+ Transformer methods create modified collections from the original enumerable. All transformer methods return an `Async::Enumerator` for chaining operations.
4
+
5
+ ## Overview
6
+
7
+ Transformer methods delegate to the standard Enumerable implementation but wrap the result in a new `Async::Enumerator` to enable continued chaining. The actual transformation happens through the parent Enumerable module.
8
+
9
+ ## Available Methods
10
+
11
+ ### map / collect
12
+
13
+ Transforms each element using the given block.
14
+
15
+ ```ruby
16
+ [1, 2, 3].async.map { |n| n * 2 } # => Async::Enumerator([2, 4, 6])
17
+ ```
18
+
19
+ ### select / filter / find_all
20
+
21
+ Selects elements for which the block returns true.
22
+
23
+ ```ruby
24
+ [1, 2, 3, 4].async.select { |n| n.even? } # => Async::Enumerator([2, 4])
25
+ ```
26
+
27
+ ### reject
28
+
29
+ Rejects elements for which the block returns true.
30
+
31
+ ```ruby
32
+ [1, 2, 3, 4].async.reject { |n| n.even? } # => Async::Enumerator([1, 3])
33
+ ```
34
+
35
+ ### filter_map
36
+
37
+ Maps and filters in a single pass, removing nil values.
38
+
39
+ ```ruby
40
+ [1, 2, 3, 4].async.filter_map { |n| n * 2 if n.even? } # => Async::Enumerator([4, 8])
41
+ ```
42
+
43
+ ### flat_map / collect_concat
44
+
45
+ Maps and flattens the result by one level.
46
+
47
+ ```ruby
48
+ [[1, 2], [3, 4]].async.flat_map { |arr| arr.map { |n| n * 2 } }
49
+ # => Async::Enumerator([2, 4, 6, 8])
50
+ ```
51
+
52
+ ### compact
53
+
54
+ Removes nil elements.
55
+
56
+ ```ruby
57
+ [1, nil, 2, nil, 3].async.compact # => Async::Enumerator([1, 2, 3])
58
+ ```
59
+
60
+ ### uniq
61
+
62
+ Removes duplicate elements.
63
+
64
+ ```ruby
65
+ [1, 1, 2, 2, 3].async.uniq # => Async::Enumerator([1, 2, 3])
66
+ ```
67
+
68
+ ### sort
69
+
70
+ Sorts elements using their natural ordering or a provided comparison.
71
+
72
+ ```ruby
73
+ [3, 1, 2].async.sort # => Async::Enumerator([1, 2, 3])
74
+ [3, 1, 2].async.sort { |a, b| b <=> a } # => Async::Enumerator([3, 2, 1])
75
+ ```
76
+
77
+ ### sort_by
78
+
79
+ Sorts elements by the result of the given block.
80
+
81
+ ```ruby
82
+ users.async.sort_by { |u| u.age } # Sorts users by age
83
+ files.async.sort_by { |f| f.size } # Sorts files by size
84
+ ```
85
+
86
+ ## Chaining
87
+
88
+ All transformer methods return an `Async::Enumerator`, enabling method chaining:
89
+
90
+ ```ruby
91
+ result = [1, 2, 3, 4, 5].async
92
+ .map { |n| n * 2 } # [2, 4, 6, 8, 10]
93
+ .select { |n| n > 4 } # [6, 8, 10]
94
+ .map { |n| n + 1 } # [7, 9, 11]
95
+ .sort { |a, b| b <=> a } # [11, 9, 7]
96
+ .sync # Materializes the result
97
+ ```
98
+
99
+ ## Implementation Notes
100
+
101
+ - Transformer methods leverage the standard Enumerable module for transformation logic
102
+ - Each method wraps the result in a new `Async::Enumerator` with the same fiber limit
103
+ - Methods returning enumerators without blocks are handled correctly
104
+ - The `max_fibers` setting is preserved through the transformation chain
@@ -0,0 +1,26 @@
1
+ module Async
2
+ module Enumerable
3
+ module Comparable
4
+ def self.included(base)
5
+ base.include(::Comparable)
6
+ end
7
+
8
+ # Compares with another enumerable.
9
+ # @param other [Object] Object to compare
10
+ # @return [Integer, nil] Comparison result
11
+ def <=>(other)
12
+ return nil unless other.respond_to?(:to_a)
13
+ to_a <=> other.to_a
14
+ end
15
+
16
+ # Checks equality with another enumerable.
17
+ # @param other [Object] Object to compare
18
+ # @return [Boolean] True if equal
19
+ def ==(other)
20
+ return false unless other.respond_to?(:to_a)
21
+ to_a == other.to_a
22
+ end
23
+ alias_method :eql?, :==
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async
4
+ module Enumerable
5
+ # Provides bounded concurrency control for async operations.
6
+ # See docs/reference/concurrency_bounder.md for detailed documentation.
7
+ # @api private
8
+ module ConcurrencyBounder
9
+ def self.included(base) = base.include(Configurable)
10
+
11
+ # Executes block with bounded concurrency.
12
+ # @param early_termination [Boolean] Support early stop
13
+ # @yield [barrier] Barrier for spawning async tasks
14
+ def __async_enumerable_bounded_concurrency(early_termination: false, limit: nil, &block)
15
+ Sync do |parent|
16
+ limit ||= __async_enumerable_config.max_fibers
17
+ semaphore = Async::Semaphore.new(limit, parent:)
18
+ barrier = Async::Barrier.new(parent: semaphore)
19
+
20
+ # Yield the barrier for task spawning
21
+ yield barrier
22
+
23
+ # Wait for all tasks to complete (or early termination)
24
+ if early_termination
25
+ begin
26
+ barrier.wait
27
+ rescue Async::Stop
28
+ # Expected when barrier.stop is called for early termination
29
+ end
30
+ else
31
+ barrier.wait
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async
4
+ module Enumerable
5
+ # Manages configuration and collection resolution for async enumerables.
6
+ # Provides a DSL for defining async enumerable sources and configuration.
7
+ module Configurable
8
+ # Configuration data class for managing async enumerable settings.
9
+ # Uses Ruby's Data class for immutability and clean API.
10
+ class Config < Data.define(:collection_ref, :max_fibers)
11
+ DEFAULT_MAX_FIBERS = 1024
12
+
13
+ def initialize(collection_ref: nil, max_fibers: DEFAULT_MAX_FIBERS)
14
+ super
15
+ end
16
+
17
+ # Define the struct class once to avoid redefinition warnings
18
+ ConfigStruct = Struct.new(*members)
19
+
20
+ # Creates mutable struct for configuration editing.
21
+ # @return [ConfigStruct] Mutable config struct
22
+ def to_struct
23
+ ConfigStruct.new(*deconstruct)
24
+ end
25
+ end
26
+
27
+ class << self
28
+ def included(base)
29
+ unless base.instance_variable_get(:@__async_enumerable_config_ref)
30
+ ref = Concurrent::AtomicReference.new
31
+ base.instance_variable_set(:@__async_enumerable_config_ref, ref)
32
+ end
33
+ end
34
+ end
35
+
36
+ # Class methods for defining async enumerable sources
37
+ module ClassMethods
38
+ # Defines enumerable source for async operations.
39
+ # @param collection_ref [Symbol] Method/ivar returning enumerable
40
+ # @param kwargs [Hash] Configuration options (max_fibers, etc.)
41
+ def def_async_enumerable(collection_ref = nil, **kwargs)
42
+ # Store only the class-specific overrides, not a full config
43
+ @__async_enumerable_class_overrides = {collection_ref:}.compact.merge(kwargs)
44
+ end
45
+
46
+ # Gets the collection reference from config.
47
+ # @return [Symbol, nil] Collection reference
48
+ def __async_enumerable_collection_ref
49
+ __async_enumerable_config.collection_ref
50
+ end
51
+
52
+ # Gets config with class-level overrides merged.
53
+ # @return [Config] Merged configuration
54
+ def __async_enumerable_config
55
+ # Dynamically merge module config with class overrides
56
+ base = Async::Enumerable.config
57
+ if @__async_enumerable_class_overrides
58
+ base.with(**@__async_enumerable_class_overrides)
59
+ else
60
+ base
61
+ end
62
+ end
63
+
64
+ # Returns nil as classes don't cache config refs.
65
+ # @return [nil] Always nil for class level
66
+ def __async_enumerable_config_ref
67
+ nil
68
+ end
69
+ end
70
+
71
+ # Instance methods for configuration and collection resolution
72
+
73
+ # Gets or updates configuration with block.
74
+ # @yield [ConfigStruct] Mutable config for editing
75
+ # @return [Config] Current or updated configuration
76
+ def __async_enumerable_configure
77
+ # Get the current config (with hierarchy)
78
+ if @__async_enumerable_config_ref
79
+ current = @__async_enumerable_config_ref.get
80
+ else
81
+ # Build config from hierarchy
82
+ current_hash = __async_enumerable_merge_all_config
83
+ current = Config.new(**current_hash)
84
+ end
85
+
86
+ return current unless block_given?
87
+
88
+ mutable = current.to_struct
89
+ yield mutable
90
+ final = __async_enumerable_merge_all_config(mutable.to_h)
91
+
92
+ Config.new(**final).tap do |updated|
93
+ @__async_enumerable_config_ref = Concurrent::AtomicReference.new(updated)
94
+ end
95
+ end
96
+ alias_method :__async_enumerable_config, :__async_enumerable_configure
97
+
98
+ # Merges configs from all hierarchy levels.
99
+ # @param config [Hash, nil] Additional config to merge
100
+ # @return [Hash] Merged configuration hash
101
+ def __async_enumerable_merge_all_config(config = nil)
102
+ [Async::Enumerable.config].tap do |arr|
103
+ class_cfg = self.class.respond_to?(:__async_enumerable_config) ? self.class.__async_enumerable_config : nil
104
+
105
+ arr << class_cfg
106
+ arr << @__async_enumerable_config
107
+ arr << config
108
+ end.compact.map(&:to_h).reduce(&:merge)
109
+ end
110
+
111
+ # Gets the config reference for this object.
112
+ # @return [AtomicReference] Config reference
113
+ def __async_enumerable_config_ref
114
+ # First check for instance-level config ref
115
+ @__async_enumerable_config_ref || Async::Enumerable.config_ref
116
+ end
117
+
118
+ # Collection resolution methods
119
+
120
+ # Gets collection reference from class.
121
+ # @return [Symbol, nil] Collection reference
122
+ def __async_enumerable_collection_ref
123
+ self.class.__async_enumerable_collection_ref
124
+ end
125
+
126
+ # Resolves the actual enumerable collection.
127
+ # @return [Enumerable] The collection to enumerate
128
+ def __async_enumerable_collection
129
+ return self unless __async_enumerable_collection_ref.is_a?(Symbol)
130
+
131
+ ref = __async_enumerable_collection_ref
132
+ if ref.to_s.start_with?("@")
133
+ instance_variable_get(ref)
134
+ else
135
+ send(ref)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Async
4
+ module Enumerable
5
+ module Methods
6
+ # Aggregators module for enumerable aggregation methods.
7
+ #
8
+ # Aggregation methods like reduce, inject, sum, count, and tally are
9
+ # inherited from the standard Enumerable module. When used with async
10
+ # enumerables, these methods automatically benefit from parallel execution
11
+ # through our async #each implementation.
12
+ #
13
+ # The block passed to these methods (when applicable) executes concurrently
14
+ # for each element, though the aggregation itself maintains correct ordering
15
+ # and thread-safe accumulation.
16
+ #
17
+ # Methods available through Enumerable:
18
+ # - reduce/inject: Combines elements using a binary operation
19
+ # - sum: Calculates the sum of elements (block executes async)
20
+ # - count: Counts elements matching a condition (block executes async)
21
+ # - tally: Counts occurrences of each element
22
+ # - min/max/minmax: Finds minimum/maximum elements (block executes async)
23
+ # - min_by/max_by: Finds elements by computed values (block executes async)
24
+ module Aggregators
25
+ def self.included(base)
26
+ base.include(Each) # Dependency
27
+ base.include(Configurable) # Dependency for collection resolution
28
+
29
+ # Delegate non-parallelizable aggregator methods directly to the collection
30
+ base.extend(Forwardable)
31
+ # is lazy really an aggregator? no, but i don't want to figure out
32
+ # _what_ it is either
33
+ base.def_delegators :__async_enumerable_collection, :size, :length, :lazy
34
+ end
35
+ # This module is intentionally empty as aggregation methods are
36
+ # inherited from Enumerable and automatically use our async #each
37
+ end
38
+ end
39
+ end
40
+ end