axn 0.1.0.pre.alpha.3 → 0.1.0.pre.alpha.4

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/commands/pr.md +36 -0
  3. data/CHANGELOG.md +15 -1
  4. data/Rakefile +102 -2
  5. data/docs/.vitepress/config.mjs +12 -8
  6. data/docs/advanced/conventions.md +1 -1
  7. data/docs/advanced/mountable.md +4 -90
  8. data/docs/advanced/profiling.md +26 -30
  9. data/docs/advanced/rough.md +27 -8
  10. data/docs/intro/overview.md +1 -1
  11. data/docs/recipes/formatting-context-for-error-tracking.md +186 -0
  12. data/docs/recipes/memoization.md +102 -17
  13. data/docs/reference/async.md +269 -0
  14. data/docs/reference/class.md +113 -50
  15. data/docs/reference/configuration.md +226 -75
  16. data/docs/reference/form-object.md +252 -0
  17. data/docs/strategies/client.md +212 -0
  18. data/docs/strategies/form.md +235 -0
  19. data/docs/usage/setup.md +2 -2
  20. data/docs/usage/writing.md +99 -1
  21. data/lib/axn/async/adapters/active_job.rb +19 -10
  22. data/lib/axn/async/adapters/disabled.rb +15 -0
  23. data/lib/axn/async/adapters/sidekiq.rb +25 -32
  24. data/lib/axn/async/batch_enqueue/config.rb +38 -0
  25. data/lib/axn/async/batch_enqueue.rb +99 -0
  26. data/lib/axn/async/enqueue_all_orchestrator.rb +363 -0
  27. data/lib/axn/async.rb +121 -4
  28. data/lib/axn/configuration.rb +53 -13
  29. data/lib/axn/context.rb +1 -0
  30. data/lib/axn/core/automatic_logging.rb +47 -51
  31. data/lib/axn/core/context/facade_inspector.rb +1 -1
  32. data/lib/axn/core/contract.rb +73 -30
  33. data/lib/axn/core/contract_for_subfields.rb +1 -1
  34. data/lib/axn/core/contract_validation.rb +14 -9
  35. data/lib/axn/core/contract_validation_for_subfields.rb +14 -7
  36. data/lib/axn/core/default_call.rb +63 -0
  37. data/lib/axn/core/flow/exception_execution.rb +5 -0
  38. data/lib/axn/core/flow/handlers/descriptors/message_descriptor.rb +19 -7
  39. data/lib/axn/core/flow/handlers/invoker.rb +4 -30
  40. data/lib/axn/core/flow/handlers/matcher.rb +4 -14
  41. data/lib/axn/core/flow/messages.rb +1 -1
  42. data/lib/axn/core/hooks.rb +1 -0
  43. data/lib/axn/core/logging.rb +16 -5
  44. data/lib/axn/core/memoization.rb +53 -0
  45. data/lib/axn/core/tracing.rb +77 -4
  46. data/lib/axn/core/validation/validators/type_validator.rb +1 -1
  47. data/lib/axn/core.rb +31 -46
  48. data/lib/axn/extras/strategies/client.rb +150 -0
  49. data/lib/axn/extras/strategies/vernier.rb +121 -0
  50. data/lib/axn/extras.rb +4 -0
  51. data/lib/axn/factory.rb +22 -2
  52. data/lib/axn/form_object.rb +90 -0
  53. data/lib/axn/internal/logging.rb +5 -1
  54. data/lib/axn/mountable/helpers/class_builder.rb +41 -10
  55. data/lib/axn/mountable/helpers/namespace_manager.rb +6 -34
  56. data/lib/axn/mountable/inherit_profiles.rb +2 -2
  57. data/lib/axn/mountable/mounting_strategies/_base.rb +10 -6
  58. data/lib/axn/mountable/mounting_strategies/method.rb +2 -2
  59. data/lib/axn/mountable.rb +41 -7
  60. data/lib/axn/rails/generators/axn_generator.rb +19 -1
  61. data/lib/axn/rails/generators/templates/action.rb.erb +1 -1
  62. data/lib/axn/result.rb +2 -2
  63. data/lib/axn/strategies/form.rb +98 -0
  64. data/lib/axn/strategies/transaction.rb +7 -0
  65. data/lib/axn/util/callable.rb +120 -0
  66. data/lib/axn/util/contract_error_handling.rb +32 -0
  67. data/lib/axn/util/execution_context.rb +34 -0
  68. data/lib/axn/util/global_id_serialization.rb +52 -0
  69. data/lib/axn/util/logging.rb +87 -0
  70. data/lib/axn/version.rb +1 -1
  71. data/lib/axn.rb +9 -0
  72. metadata +22 -4
  73. data/lib/axn/core/profiling.rb +0 -124
  74. data/lib/axn/mountable/mounting_strategies/enqueue_all.rb +0 -55
@@ -0,0 +1,186 @@
1
+ # Formatting Context for Error Tracking Systems
2
+
3
+ The `context` hash passed to the global `on_exception` handler may contain complex objects (like ActiveRecord models, `ActionController::Parameters`, or `Axn::FormObject` instances) that aren't easily serialized by error tracking systems. You can format these values to make them more readable.
4
+
5
+ ## Basic Example
6
+
7
+ ```ruby
8
+ Axn.configure do |c|
9
+ c.on_exception = proc do |e, action:, context:|
10
+ formatted_context = format_hash_values(context)
11
+
12
+ Honeybadger.notify(e, context: { axn_context: formatted_context })
13
+ end
14
+ end
15
+
16
+ def format_hash_values(hash)
17
+ hash.transform_values do |v|
18
+ if v.respond_to?(:to_global_id)
19
+ v.to_global_id.to_s
20
+ elsif v.is_a?(ActionController::Parameters)
21
+ v.to_unsafe_h
22
+ elsif v.is_a?(Axn::FormObject)
23
+ v.to_h
24
+ else
25
+ v
26
+ end
27
+ end
28
+ end
29
+ ```
30
+
31
+ ## What This Converts
32
+
33
+ - **ActiveRecord objects** → Their global ID string (via `to_global_id`)
34
+ - **`ActionController::Parameters`** → A plain hash
35
+ - **`Axn::FormObject` instances** → Their hash representation
36
+ - **Other values** → Remain unchanged
37
+
38
+ This ensures that your error tracking system receives serializable, readable context data instead of complex objects that may not serialize properly.
39
+
40
+ ## Recursive Formatting
41
+
42
+ If your context contains nested hashes with complex objects, you may want to recursively format the entire structure:
43
+
44
+ ```ruby
45
+ def format_hash_values(hash)
46
+ hash.transform_values do |v|
47
+ case v
48
+ when Hash
49
+ format_hash_values(v)
50
+ when Array
51
+ v.map { |item| item.is_a?(Hash) ? format_hash_values(item) : format_value(item) }
52
+ else
53
+ format_value(v)
54
+ end
55
+ end
56
+ end
57
+
58
+ def format_value(v)
59
+ if v.respond_to?(:to_global_id)
60
+ v.to_global_id.to_s
61
+ elsif v.is_a?(ActionController::Parameters)
62
+ v.to_unsafe_h
63
+ elsif v.is_a?(Axn::FormObject)
64
+ v.to_h
65
+ else
66
+ v
67
+ end
68
+ end
69
+ ```
70
+
71
+ ## Advanced Example: Production Implementation
72
+
73
+ Here's a comprehensive example that includes additional context, a retry command generator, and proper handling of ActiveRecord models:
74
+
75
+ ```ruby
76
+ Axn.configure do |c|
77
+ def format_hash_values(hash)
78
+ hash.transform_values do |v|
79
+ if v.respond_to?(:to_global_id)
80
+ v.to_global_id.to_s
81
+ elsif v.is_a?(ActionController::Parameters)
82
+ v.to_unsafe_h
83
+ elsif v.is_a?(Axn::FormObject)
84
+ v.to_h
85
+ else
86
+ v
87
+ end
88
+ end
89
+ end
90
+
91
+ # Format values for retry commands - produces copy-pasteable Ruby code
92
+ def format_value_for_retry_command(value)
93
+ # Handle ActiveRecord model instances
94
+ if value.respond_to?(:to_global_id) && value.respond_to?(:id) && !value.is_a?(Class)
95
+ begin
96
+ model_class = value.class.name
97
+ id = value.id
98
+ return "#{model_class}.find(#{id.inspect})"
99
+ rescue StandardError
100
+ # If accessing id fails, fall through to default behavior
101
+ end
102
+ end
103
+
104
+ # Handle GlobalID strings (useful for serialized values)
105
+ if value.is_a?(String) && value.start_with?("gid://")
106
+ begin
107
+ gid = GlobalID.parse(value)
108
+ if gid
109
+ model_class = gid.model_class.name
110
+ id = gid.model_id
111
+ return "#{model_class}.find(#{id.inspect})"
112
+ end
113
+ rescue StandardError
114
+ # If parsing fails, fall through to default behavior
115
+ end
116
+ end
117
+
118
+ # Default: use inspect for other types
119
+ value.inspect
120
+ end
121
+
122
+ def retry_command(action:, context:)
123
+ action_name = action.class.name
124
+ return nil if action_name.nil?
125
+
126
+ expected_fields = action.internal_field_configs.map(&:field)
127
+
128
+ return "#{action_name}.call()" if expected_fields.empty?
129
+
130
+ args = expected_fields.map do |field|
131
+ value = context[field]
132
+ "#{field}: #{format_value_for_retry_command(value)}"
133
+ end.join(", ")
134
+
135
+ "#{action_name}.call(#{args})"
136
+ end
137
+
138
+ c.on_exception = proc do |e, action:, context:|
139
+ axn_name = action.class.name || "AnonymousClass"
140
+ message = "[#{axn_name}] Raised #{e.class.name}: #{e.message}"
141
+
142
+ hb_context = {
143
+ axn: axn_name,
144
+ axn_context: format_hash_values(context),
145
+ current_attributes: format_hash_values(Current.attributes),
146
+ retry_command: retry_command(action:, context:),
147
+ exception: e,
148
+ }
149
+
150
+ fingerprint = [axn_name, e.class.name, e.message].join(" - ")
151
+ Honeybadger.notify(message, context: hb_context, backtrace: e.backtrace, fingerprint:)
152
+ rescue StandardError => rep
153
+ Rails.logger.warn "!! Axn failed to report action failure to honeybadger!\nOriginal exception: #{e}\nReporting exception: #{rep}"
154
+ end
155
+ end
156
+ ```
157
+
158
+ This example includes:
159
+
160
+ - **Formatted context**: Uses `format_hash_values` to serialize complex objects for readable error tracking
161
+ - **Smart retry commands**: Generates copy-pasteable Ruby code, converting ActiveRecord models to `Model.find(id)` calls instead of raw inspect output
162
+ - **GlobalID support**: Handles both live model instances and serialized GlobalID strings
163
+ - **Additional context**: Includes `Current.attributes` (if using a Current pattern) for request-level context
164
+ - **Error fingerprinting**: Creates a fingerprint from action name, exception class, and message to group similar errors
165
+ - **Error handling**: Wraps the Honeybadger notification in a rescue block to prevent reporting failures from masking the original exception
166
+
167
+ ### Example Output
168
+
169
+ For an action like:
170
+
171
+ ```ruby
172
+ class UpdateUser
173
+ include Axn
174
+ expects :user, model: User
175
+ expects :name, type: String
176
+ end
177
+ ```
178
+
179
+ The retry command would generate:
180
+
181
+ ```ruby
182
+ UpdateUser.call(user: User.find(123), name: "Alice")
183
+ ```
184
+
185
+ This can be copied directly from your error tracking system and pasted into a Rails console to reproduce the error.
186
+
@@ -1,46 +1,131 @@
1
- ### Adding memoization
1
+ # Memoization
2
2
 
3
- For a practical example of [the `additional_includes` configuration](/reference/configuration#additional-includes) in practice, consider adding new functionality to all Actions.
3
+ Axn has built-in memoization support via the `memo` helper. This caches the result of method calls, ensuring they're only computed once per action execution.
4
4
 
5
- For instance, at Teamshares we automatically add memoization support (via [memo_wise](https://github.com/panorama-ed/memo_wise)) to all Actions. But we didn't want to add another dependency to the core library, so we've implemented this by:
5
+ ## Basic Usage
6
6
 
7
+ The `memo` helper works out of the box for methods without arguments:
7
8
 
8
9
  ```ruby
9
- Axn.configure do |c|
10
- c.additional_includes = [TS::Memoization]
10
+ class GenerateReport
11
+ include Axn
12
+
13
+ expects :company, model: Company
14
+ exposes :report
15
+
16
+ def call
17
+ expose report: {
18
+ total_revenue: total_revenue,
19
+ top_products: top_products.map(&:name),
20
+ # top_products is only queried once, even though it's called twice
21
+ product_count: top_products.count
22
+ }
23
+ end
24
+
25
+ private
26
+
27
+ memo def top_products
28
+ company.products.order(sales_count: :desc).limit(10)
29
+ end
30
+
31
+ memo def total_revenue
32
+ company.orders.sum(:total)
11
33
  end
34
+ end
12
35
  ```
13
36
 
37
+ ## How It Works
38
+
39
+ - `memo` wraps the method and caches its return value on first call
40
+ - Subsequent calls return the cached value without re-executing the method
41
+ - Memoization is scoped to the action instance, so each `call` starts fresh
42
+
43
+ ## Methods With Arguments
44
+
45
+ For methods that accept arguments, Axn supports the `memo_wise` gem:
46
+
47
+ ```ruby
48
+ # Gemfile
49
+ gem "memo_wise"
50
+ ```
51
+
52
+ With `memo_wise` available, you can automatically memoize methods with arguments:
53
+
14
54
  ```ruby
15
- module TS::Memoization
16
- extend ActiveSupport::Concern
55
+ class CalculatePricing
56
+ include Axn
57
+
58
+ expects :product
59
+ exposes :pricing
17
60
 
18
- included do
19
- prepend MemoWise
61
+ def call
62
+ expose pricing: {
63
+ retail: price_for(:retail),
64
+ wholesale: price_for(:wholesale),
65
+ # Each unique argument is cached separately
66
+ bulk: price_for(:bulk)
67
+ }
20
68
  end
21
69
 
22
- class_methods do
23
- def memo(...) = memo_wise(...)
70
+ private
71
+
72
+ memo def price_for(tier)
73
+ # Complex pricing calculation...
74
+ PricingEngine.calculate(product, tier:)
24
75
  end
25
76
  end
26
77
  ```
27
78
 
28
- And with those pieces in place `memo` is available in all Actions:
79
+ If you try to use `memo` on a method with arguments without `memo_wise` installed, you'll get a helpful error:
80
+
81
+ ```
82
+ ArgumentError: Memoization of methods with arguments requires the 'memo_wise' gem.
83
+ Please add 'memo_wise' to your Gemfile or use a method without arguments.
84
+ ```
85
+
86
+ ## When to Use Memoization
87
+
88
+ Memoization is particularly useful for:
89
+
90
+ - **Database queries** called multiple times within an action
91
+ - **API calls** or external service lookups
92
+ - **Complex computations** that are expensive to repeat
29
93
 
30
94
  ```ruby
31
- class ContrivedExample
95
+ class SyncUserData
32
96
  include Axn
33
97
 
34
- exposes :nums
98
+ expects :user, model: User
35
99
 
36
100
  def call
37
- expose nums: Array.new(10) { random_number }
101
+ update_profile if needs_profile_update?
102
+ update_preferences if needs_preferences_update?
103
+ notify_if_changed
38
104
  end
39
105
 
40
106
  private
41
107
 
42
- memo def random_number = rand(1..100) # [!code focus]
108
+ # Called multiple times - only fetches once
109
+ memo def external_data
110
+ ExternalApi.fetch_user_data(user.external_id)
111
+ end
112
+
113
+ def needs_profile_update?
114
+ external_data[:profile_version] > user.profile_version
115
+ end
116
+
117
+ def needs_preferences_update?
118
+ external_data[:preferences_hash] != user.preferences_hash
119
+ end
120
+
121
+ def notify_if_changed
122
+ # ...
123
+ end
43
124
  end
44
125
  ```
45
126
 
46
- Because of the `memo` usage, `ContrivedExample.call.nums` will be a ten-element array of _the same number_, rather than re-calling `rand` for each element.
127
+ ## Notes
128
+
129
+ - Memoization persists only for the duration of a single action execution
130
+ - When `memo_wise` is available, Axn automatically uses it (no configuration needed)
131
+ - See the [memo_wise documentation](https://github.com/panorama-ed/memo_wise) for advanced features like cache resetting
@@ -158,3 +158,272 @@ end
158
158
  # The job will be retried up to 3 times before giving up
159
159
  FailingAction.call_async(data: "test")
160
160
  ```
161
+
162
+ ## Batch Enqueueing with `enqueues_each`
163
+
164
+ The `enqueues_each` method provides a declarative way to set up batch enqueueing. It automatically iterates over collections and enqueues each item as a separate background job.
165
+
166
+ ### Basic Usage
167
+
168
+ ```ruby
169
+ class SyncForCompany
170
+ include Axn
171
+ async :sidekiq
172
+
173
+ expects :company, model: Company
174
+
175
+ def call
176
+ puts "Syncing data for company: #{company.name}"
177
+ # Sync individual company data
178
+ end
179
+
180
+ # No enqueues_each needed! Source is auto-inferred from model: Company
181
+ end
182
+
183
+ # Usage
184
+ SyncForCompany.enqueue_all # Automatically iterates Company.all and enqueues each company
185
+ ```
186
+
187
+ **How it works:**
188
+ 1. `enqueue_all` validates configuration upfront (async configured, static args present)
189
+ 2. If all arguments are serializable, enqueues an `EnqueueAllOrchestrator` job in the background
190
+ 3. If any argument is unserializable (e.g., an AR relation override), executes in the foreground instead
191
+ 4. During execution, iterates over the source collection and enqueues individual jobs
192
+ 5. Model-based iterations (using `find_each`) are processed first for memory efficiency
193
+
194
+ ### Auto-Inference from `model:` Declarations
195
+
196
+ If a field has a `model:` declaration and the model class responds to `find_each`, you **don't need to explicitly declare `enqueues_each`**. The source collection is automatically inferred:
197
+
198
+ ```ruby
199
+ class SyncForCompany
200
+ include Axn
201
+ async :sidekiq
202
+
203
+ expects :company, model: Company # Auto-inferred: Company.all
204
+
205
+ def call
206
+ # ... sync logic
207
+ end
208
+
209
+ # No enqueues_each needed - automatically iterates Company.all
210
+ end
211
+
212
+ SyncForCompany.enqueue_all # Works without explicit enqueues_each!
213
+ ```
214
+
215
+ ### Explicit Configuration with `enqueues_each`
216
+
217
+ Use `enqueues_each` when you need to:
218
+ - Override the default source (e.g., `Company.active` instead of `Company.all`)
219
+ - Add filtering logic
220
+ - Extract specific attributes
221
+ - Iterate over fields without `model:` declarations
222
+
223
+ ```ruby
224
+ class SyncForCompany
225
+ include Axn
226
+ async :sidekiq
227
+
228
+ expects :company, model: Company
229
+
230
+ def call
231
+ # ... sync logic
232
+ end
233
+
234
+ # Override default source
235
+ enqueues_each :company, from: -> { Company.active }
236
+
237
+ # With extraction (passes company_id instead of company object)
238
+ enqueues_each :company_id, from: -> { Company.active }, via: :id
239
+
240
+ # With filter block
241
+ enqueues_each :company do |company|
242
+ company.active? && !company.in_exit?
243
+ end
244
+
245
+ # Method name as source
246
+ enqueues_each :company, from: :active_companies
247
+ end
248
+ ```
249
+
250
+ ### Overriding on `enqueue_all` Call
251
+
252
+ You can override iteration sources or make fields static when calling `enqueue_all`:
253
+
254
+ ```ruby
255
+ class SyncForCompany
256
+ include Axn
257
+ async :sidekiq
258
+
259
+ expects :company, model: Company
260
+ expects :user, model: User
261
+
262
+ def call
263
+ # ... sync logic
264
+ end
265
+
266
+ # Default: iterates Company.all
267
+ enqueues_each :company
268
+ end
269
+
270
+ # Override with a subset (enumerable kwarg replaces source)
271
+ SyncForCompany.enqueue_all(company: Company.active.limit(10))
272
+
273
+ # Override with a single value (scalar kwarg makes it static, no iteration)
274
+ SyncForCompany.enqueue_all(company: Company.find(123))
275
+
276
+ # Mix static and iterated fields
277
+ SyncForCompany.enqueue_all(
278
+ company: Company.active, # Iterates over active companies
279
+ user: User.find(1) # Static: same user for all jobs
280
+ )
281
+ ```
282
+
283
+ ### Dynamic Iteration via Kwargs
284
+
285
+ You can iterate over fields without any `enqueues_each` declaration by passing enumerables directly:
286
+
287
+ ```ruby
288
+ class ProcessFormats
289
+ include Axn
290
+ async :sidekiq
291
+
292
+ expects :format
293
+ expects :mode
294
+
295
+ def call
296
+ # ... process logic
297
+ end
298
+ end
299
+
300
+ # Pass enumerables to create cross-product iteration
301
+ ProcessFormats.enqueue_all(
302
+ format: [:csv, :json, :xml], # Iterates: 3 jobs
303
+ mode: :full # Static: same mode for all
304
+ )
305
+
306
+ # Multiple enumerables create cross-product
307
+ ProcessFormats.enqueue_all(
308
+ format: [:csv, :json], # 2 formats
309
+ mode: [:full, :incremental] # 2 modes
310
+ )
311
+ # Result: 2 × 2 = 4 jobs total
312
+ ```
313
+
314
+ **Note:** Arrays and Sets are treated as static values (not iterated) when the field expects an enumerable type:
315
+
316
+ ```ruby
317
+ expects :tags, type: Array
318
+
319
+ # This passes the entire array as a static value
320
+ ProcessTags.enqueue_all(tags: ["ruby", "rails", "testing"])
321
+ ```
322
+
323
+ ### Multi-Field Cross-Product Iteration
324
+
325
+ Multiple `enqueues_each` declarations create a cross-product of all combinations:
326
+
327
+ ```ruby
328
+ class SyncForUserAndCompany
329
+ include Axn
330
+ async :sidekiq
331
+
332
+ expects :user, model: User
333
+ expects :company, model: Company
334
+
335
+ def call
336
+ # ... sync logic for user + company combination
337
+ end
338
+
339
+ enqueues_each :user, from: -> { User.active }
340
+ enqueues_each :company, from: -> { Company.active }
341
+ end
342
+
343
+ # Creates user_count × company_count jobs
344
+ # Each combination of (user, company) gets its own job
345
+ SyncForUserAndCompany.enqueue_all
346
+ ```
347
+
348
+ ### Static Fields
349
+
350
+ Fields declared with `expects` but not covered by `enqueues_each` (or auto-inference) become static fields that must be passed to `enqueue_all`:
351
+
352
+ ```ruby
353
+ class SyncWithMode
354
+ include Axn
355
+ async :sidekiq
356
+
357
+ expects :company, model: Company # Auto-inferred, will iterate
358
+ expects :sync_mode # Static, must be provided
359
+
360
+ def call
361
+ # Uses both company (iterated) and sync_mode (static)
362
+ end
363
+ end
364
+
365
+ # sync_mode must be provided - it's passed to every enqueued job
366
+ SyncWithMode.enqueue_all(sync_mode: :full)
367
+ ```
368
+
369
+ ### Memory Efficiency
370
+
371
+ For optimal memory usage, model-based configs (using `find_each`) are automatically processed first in nested iterations. This ensures ActiveRecord-style batch processing happens before loading potentially large enumerables into memory.
372
+
373
+ ```ruby
374
+ # Model-based iteration uses find_each (memory efficient)
375
+ expects :company, model: Company # Processed first
376
+
377
+ # Array-based iteration uses each (loads all into memory)
378
+ enqueues_each :format, from: -> { [:csv, :json, :xml] } # Processed second
379
+ ```
380
+
381
+ ### Iteration Method Selection
382
+
383
+ - **`find_each`**: Used when the source responds to `find_each` (ActiveRecord collections) - processes in batches for memory efficiency
384
+ - **`each`**: Used for plain arrays and other enumerables - loads all items into memory
385
+
386
+ ### Background vs Foreground Execution
387
+
388
+ By default, `enqueue_all` enqueues an `EnqueueAllOrchestrator` job to perform the iteration and individual job enqueueing in the background. This makes it safe to call directly from clock processes (e.g., Heroku scheduler) without risking memory bloat from loading large collections.
389
+
390
+ However, if you pass an **enumerable override** (like an ActiveRecord relation or array), `enqueue_all` automatically falls back to foreground execution—iterating and enqueueing immediately in the current process. This is because the iteration source (a lambda wrapping the enumerable) cannot be serialized for background execution.
391
+
392
+ ```ruby
393
+ class SyncForCompany
394
+ include Axn
395
+ async :sidekiq
396
+
397
+ expects :company, model: Company
398
+
399
+ def call
400
+ # ... sync logic
401
+ end
402
+
403
+ enqueues_each :company, from: -> { Company.active }
404
+ end
405
+
406
+ # Default: enqueues orchestrator job in background (safe for clock processes)
407
+ SyncForCompany.enqueue_all
408
+
409
+ # Override with a relation: executes in foreground (relation can't be serialized)
410
+ SyncForCompany.enqueue_all(company: Company.where(plan: "enterprise"))
411
+
412
+ # Override with a single value: runs in background (GlobalID-serializable scalar)
413
+ SyncForCompany.enqueue_all(company: Company.find(123))
414
+ ```
415
+
416
+ **Why this matters:**
417
+ - Configure your default iteration source via `enqueues_each` for scheduled/recurring jobs
418
+ - By default, `enqueue_all` runs safely in the background without loading your entire dataset into memory
419
+ - For one-off manual calls with a filtered subset, pass an enumerable override—foreground execution handles it automatically
420
+ - Scalar overrides (single objects) are serialized via GlobalID and still run in the background
421
+
422
+ This design lets you use the same action class for both scheduled batch processing and ad-hoc targeted runs.
423
+
424
+ ### Edge Cases and Limitations
425
+
426
+ 1. **Fields expecting enumerable types**: If a field expects `Array` or `Set`, arrays/sets passed to `enqueue_all` are treated as static values (not iterated)
427
+ 2. **Strings and Hashes**: Always treated as static values, even though they respond to `:each`
428
+ 3. **No model or source**: If a field has no `model:` declaration and no `enqueues_each` with `from:`, you must pass it as a kwarg to `enqueue_all` or it will raise an error
429
+ 4. **Required static fields**: Fields without defaults that aren't covered by iteration must be provided to `enqueue_all`