ruby_reactor 0.3.2 → 0.4.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-config.json +15 -0
  3. data/.release-please-manifest.json +3 -0
  4. data/.tool-versions +1 -0
  5. data/CHANGELOG.md +13 -0
  6. data/README.md +80 -4
  7. data/lib/ruby_reactor/context_serializer.rb +10 -1
  8. data/lib/ruby_reactor/map/result_enumerator.rb +4 -3
  9. data/lib/ruby_reactor/rate_limit.rb +2 -2
  10. data/lib/ruby_reactor/sidekiq_workers/worker.rb +58 -1
  11. data/lib/ruby_reactor/version.rb +1 -1
  12. metadata +7 -52
  13. data/documentation/DAG.md +0 -457
  14. data/documentation/README.md +0 -135
  15. data/documentation/async_reactors.md +0 -381
  16. data/documentation/composition.md +0 -199
  17. data/documentation/core_concepts.md +0 -676
  18. data/documentation/data_pipelines.md +0 -230
  19. data/documentation/examples/inventory_management.md +0 -748
  20. data/documentation/examples/order_processing.md +0 -380
  21. data/documentation/examples/payment_processing.md +0 -565
  22. data/documentation/getting_started.md +0 -242
  23. data/documentation/images/failed_order_processing.png +0 -0
  24. data/documentation/images/payment_workflow.png +0 -0
  25. data/documentation/interrupts.md +0 -163
  26. data/documentation/locks_and_semaphores.md +0 -459
  27. data/documentation/retry_configuration.md +0 -362
  28. data/documentation/testing.md +0 -994
  29. data/gui/.gitignore +0 -24
  30. data/gui/README.md +0 -73
  31. data/gui/eslint.config.js +0 -23
  32. data/gui/index.html +0 -13
  33. data/gui/package-lock.json +0 -5925
  34. data/gui/package.json +0 -46
  35. data/gui/postcss.config.js +0 -6
  36. data/gui/public/vite.svg +0 -1
  37. data/gui/src/App.css +0 -42
  38. data/gui/src/App.tsx +0 -51
  39. data/gui/src/assets/react.svg +0 -1
  40. data/gui/src/components/DagVisualizer.tsx +0 -424
  41. data/gui/src/components/Dashboard.tsx +0 -163
  42. data/gui/src/components/ErrorBoundary.tsx +0 -47
  43. data/gui/src/components/ReactorDetail.tsx +0 -135
  44. data/gui/src/components/StepInspector.tsx +0 -492
  45. data/gui/src/components/__tests__/DagVisualizer.test.tsx +0 -140
  46. data/gui/src/components/__tests__/ReactorDetail.test.tsx +0 -111
  47. data/gui/src/components/__tests__/StepInspector.test.tsx +0 -408
  48. data/gui/src/globals.d.ts +0 -7
  49. data/gui/src/index.css +0 -14
  50. data/gui/src/lib/utils.ts +0 -13
  51. data/gui/src/main.tsx +0 -14
  52. data/gui/src/test/setup.ts +0 -11
  53. data/gui/tailwind.config.js +0 -11
  54. data/gui/tsconfig.app.json +0 -28
  55. data/gui/tsconfig.json +0 -7
  56. data/gui/tsconfig.node.json +0 -26
  57. data/gui/vite.config.ts +0 -8
  58. data/gui/vitest.config.ts +0 -13
@@ -1,242 +0,0 @@
1
- # Getting Started with RubyReactor
2
-
3
- This guide walks you through installation, configuration, and your first reactor.
4
-
5
- ## Installation
6
-
7
- Add RubyReactor to your Gemfile:
8
-
9
- ```ruby
10
- gem 'ruby_reactor'
11
- ```
12
-
13
- Or install directly:
14
-
15
- ```bash
16
- gem install ruby_reactor
17
- ```
18
-
19
- ## Configuration
20
-
21
- RubyReactor uses Redis for state persistence and Sidekiq for async execution. Configure both before running any reactors:
22
-
23
- ```ruby
24
- RubyReactor.configure do |config|
25
- # Redis configuration for state persistence
26
- config.storage.adapter = :redis
27
- config.storage.redis_url = ENV.fetch("REDIS_URL", "redis://localhost:6379/0")
28
- config.storage.redis_options = { timeout: 1 }
29
-
30
- # Sidekiq configuration for async execution
31
- config.sidekiq_queue = :default
32
- config.sidekiq_retry_count = 3
33
-
34
- # Logger
35
- config.logger = Logger.new($stdout)
36
- end
37
- ```
38
-
39
- ## Your First Reactor
40
-
41
- Reactors are subclasses of `RubyReactor::Reactor`. Declare `input`s, define `step`s with their `argument`s and `run` block, and optionally a `returns` step:
42
-
43
- ```ruby
44
- require 'ruby_reactor'
45
-
46
- class OrderProcessingReactor < RubyReactor::Reactor
47
- input :order_id do
48
- required(:order_id).filled(:integer, gt?: 0)
49
- end
50
-
51
- step :validate_order do
52
- argument :order_id, input(:order_id)
53
-
54
- run do |args, _context|
55
- order = Order.find_by(id: args[:order_id])
56
- return Failure("Order not found") unless order
57
- return Failure("Order already processed") if order.processed?
58
-
59
- Success({ order: order })
60
- end
61
- end
62
-
63
- step :process_payment do
64
- argument :order, result(:validate_order, :order)
65
-
66
- run do |args, _context|
67
- payment = PaymentService.charge(args[:order].total, args[:order].customer.card_token)
68
- payment.success? ? Success({ payment_id: payment.id }) : Failure("Payment failed")
69
- end
70
- end
71
-
72
- step :update_inventory do
73
- argument :order, result(:validate_order, :order)
74
-
75
- run do |args, _context|
76
- args[:order].items.each do |item|
77
- InventoryService.decrement(item.product_id, item.quantity)
78
- end
79
- Success({ inventory_updated: true })
80
- end
81
- end
82
-
83
- step :send_confirmation do
84
- argument :order, result(:validate_order, :order)
85
- argument :payment_id, result(:process_payment, :payment_id)
86
-
87
- run do |args, _context|
88
- EmailService.send_confirmation(args[:order].customer.email, order: args[:order])
89
- Success({ confirmation_sent: true })
90
- end
91
- end
92
-
93
- returns :send_confirmation
94
- end
95
- ```
96
-
97
- ### Run blocks always receive `(arguments, context)`
98
-
99
- Every step's `run` block receives two positional arguments: the resolved arguments hash and the execution context. Use `argument :name, source` to declare which value goes into `args[:name]`.
100
-
101
- ## Executing a Reactor
102
-
103
- ### Synchronous Execution
104
-
105
- ```ruby
106
- result = OrderProcessingReactor.run(order_id: 123)
107
-
108
- if result.success?
109
- puts "Order processed: #{result.value}"
110
- else
111
- puts "Order processing failed: #{result.error}"
112
- end
113
- ```
114
-
115
- `Reactor.run` returns one of:
116
-
117
- - `RubyReactor::Success` — `result.success?` is `true`, `result.value` holds the step output for `returns` (or the full `intermediate_results` hash if no `returns` is set).
118
- - `RubyReactor::Failure` — `result.failure?` is `true`. Useful readers: `result.error`, `result.step_name`, `result.exception_class`, `result.backtrace`, `result.step_arguments`.
119
- - `RubyReactor::AsyncResult` — returned when the reactor (or a step) is async. Holds `job_id`, `execution_id`, and any `intermediate_results` available at handoff.
120
- - `RubyReactor::InterruptResult` — returned when an `interrupt` step pauses execution. Use `result.execution_id` and `result.correlation_id` to resume later.
121
-
122
- ### Asynchronous Execution
123
-
124
- For async execution, configure Sidekiq and either mark the reactor `async true` or mark individual steps async:
125
-
126
- ```ruby
127
- class OrderProcessingReactor < RubyReactor::Reactor
128
- async true # Entire reactor runs in a Sidekiq worker
129
-
130
- # ... steps defined above
131
- end
132
-
133
- async_result = OrderProcessingReactor.run(order_id: 123)
134
- async_result.execution_id # => UUID for looking up state later
135
- ```
136
-
137
- To inspect a running execution, reload it from storage:
138
-
139
- ```ruby
140
- reactor = OrderProcessingReactor.find(async_result.execution_id)
141
- reactor.context.status # => "running" | "completed" | "failed" | "paused"
142
- reactor.result # Success / Failure / InterruptResult
143
- ```
144
-
145
- See [Async Reactors](async_reactors.md) for the full async model.
146
-
147
- ## Inspecting the Context
148
-
149
- Step outputs are stored on the context, not the result object. After a sync execution you can reach them via the reactor instance:
150
-
151
- ```ruby
152
- reactor = OrderProcessingReactor.new
153
- reactor.run(order_id: 123)
154
-
155
- reactor.context.intermediate_results[:validate_order] # => { order: <Order> }
156
- reactor.context.intermediate_results[:process_payment] # => { payment_id: "pay_123" }
157
- reactor.context.status # => "completed"
158
- reactor.execution_trace # => [{ type: :run, step: :validate_order, ... }, ...]
159
- ```
160
-
161
- For black-box assertions in tests, use the `test_reactor` helper described in [Testing with RSpec](testing.md).
162
-
163
- ## Step Dependencies
164
-
165
- Steps depend on each other through `argument :name, result(:other_step)`. The dependency graph topologically sorts steps; circular dependencies raise `RubyReactor::Error::DependencyError`:
166
-
167
- ```ruby
168
- class ComplexReactor < RubyReactor::Reactor
169
- step :validate_order do
170
- run { |args, _ctx| validate_order_logic(args) }
171
- end
172
-
173
- step :check_inventory do
174
- argument :order, result(:validate_order)
175
-
176
- run do |args, _context|
177
- check_inventory_for_order(args[:order])
178
- end
179
- end
180
-
181
- step :process_payment do
182
- argument :order, result(:check_inventory)
183
-
184
- run do |args, _context|
185
- process_payment_for_order(args[:order])
186
- end
187
- end
188
- end
189
- ```
190
-
191
- You can also declare order without data flow using `wait_for :step_name`.
192
-
193
- ## Error Handling and Compensation
194
-
195
- When a step fails (returns `Failure(...)` or raises), the reactor:
196
-
197
- 1. Runs the **`compensate`** block of the failing step (signature: `|error, arguments, context|`).
198
- 2. Walks back through previously successful steps and runs each one's **`undo`** block in reverse order (signature: `|result, arguments, context|`).
199
- 3. Returns a `Failure`.
200
-
201
- ```ruby
202
- class OrderProcessingReactor < RubyReactor::Reactor
203
- step :process_payment do
204
- argument :order, result(:validate_order)
205
-
206
- run { |args, _ctx| PaymentService.charge(args[:order]) }
207
-
208
- undo do |result, _args, _ctx|
209
- # Runs if a later step fails
210
- PaymentService.refund(result[:payment_id])
211
- Success()
212
- end
213
- end
214
-
215
- step :update_inventory do
216
- argument :order, result(:validate_order)
217
-
218
- run { |args, _ctx| InventoryService.decrement_all(args[:order].items) }
219
-
220
- compensate do |_error, args, _ctx|
221
- # Runs only if THIS step fails
222
- args[:order].items.each { |i| InventoryService.increment(i.product_id, i.quantity) }
223
- Success()
224
- end
225
- end
226
- end
227
- ```
228
-
229
- If `update_inventory` fails: its `compensate` runs, then `process_payment`'s `undo` runs.
230
-
231
- See [Core Concepts](core_concepts.md#compensation) for the full compensation/undo model.
232
-
233
- ## Next Steps
234
-
235
- - [Core Concepts](core_concepts.md) — Reactors, Steps, Context, Results
236
- - [Async Reactors](async_reactors.md) — Full and step-level async execution
237
- - [Retry Configuration](retry_configuration.md) — Backoff strategies and retry policies
238
- - [Interrupts](interrupts.md) — Pause/resume workflows
239
- - [Composition](composition.md) — Build complex flows from smaller reactors
240
- - [Data Pipelines](data_pipelines.md) — Map over collections in parallel
241
- - [Testing with RSpec](testing.md) — `test_reactor`, mocks, matchers
242
- - [Examples](examples/) — End-to-end workflows
@@ -1,163 +0,0 @@
1
- # Interrupts (Pause & Resume)
2
-
3
- RubyReactor introduces the `interrupt` mechanism to support long-running processes that require external input, such as user approvals, webhooks, or asynchronous job completions. Unlike standard steps that execute immediately, an `interrupt` pauses the reactor execution and persists its state, waiting for a signal to resume.
4
-
5
- ## DSL Usage
6
-
7
- Use the `interrupt` keyword to define a pause point in your reactor.
8
-
9
- ```ruby
10
- class ReportReactor < RubyReactor::Reactor
11
- step :request_report do
12
- run do |_args, _ctx|
13
- response = HTTP.post("https://api.example.com/reports")
14
- Success(response.fetch(:id))
15
- end
16
- end
17
-
18
- interrupt :wait_for_report do
19
- # Declare dependency: execution must trigger this interrupt only after :request_report succeeds
20
- wait_for :request_report
21
-
22
- # Optional: deterministic correlation ID for looking up this execution later
23
- correlation_id do |context|
24
- "report-#{context.result(:request_report)}"
25
- end
26
-
27
- # Optional: timeout in seconds
28
- # Strategies:
29
- # - :lazy (default) - checked only when resume is attempted
30
- # - :active - schedules a background job to wake up the reactor and fail it
31
- timeout 1800, strategy: :active
32
-
33
- # Optional: validate incoming payload immediately using dry-validation
34
- validate do
35
- required(:status).filled(:string)
36
- required(:url).filled(:string)
37
- end
38
-
39
- # Optional: limit validation attempts (default: 1)
40
- # If exhausted, the reactor is cancelled and compensated.
41
- # Use :infinity for unlimited attempts.
42
- max_attempts 3
43
- end
44
-
45
- step :process_report do
46
- # The result of the interrupt step is the payload provided when resuming
47
- argument :webhook_payload, result(:wait_for_report)
48
-
49
- run do |args, _ctx|
50
- Success(ReportProcessor.call(args[:webhook_payload]))
51
- end
52
- end
53
- end
54
- ```
55
-
56
- ### Options
57
-
58
- * **`wait_for`**: declare dependencies similar to `step`.
59
- * **`correlation_id`**: A block that returns a unique string to identify this execution. This allows you to resume the reactor using a business key (e.g., order ID) instead of the internal execution UUID.
60
- * **`timeout`**: Set a time limit for the interrupt.
61
- * **`validate`**: A `dry-validation` schema block to validate the payload provided when resuming.
62
- * **`max_attempts`**: Limit the number of times `continue` can be called with an invalid payload before the reactor is automatically cancelled and compensated. Defaults to 1. Set to `:infinity` for unlimited retries.
63
-
64
- ## Runtime Behavior
65
-
66
- When a reactor encounters an `interrupt`:
67
-
68
- 1. It executes any dependencies.
69
- 2. It persists the full `Context` (results of previous steps) to the configured storage (e.g., Redis).
70
- 3. It returns an `InterruptResult` and halts execution.
71
-
72
- ```ruby
73
- execution = ReportReactor.run(company_id: 1)
74
-
75
- if execution.paused?
76
- execution.execution_id # => "uuid-123"
77
- execution.correlation_id # => "report-..." (if defined)
78
- execution.status # => :paused
79
- end
80
- ```
81
-
82
- ## Resuming Execution
83
-
84
- You can resume a paused reactor using its UUID or the defined `correlation_id`.
85
-
86
- ### By UUID
87
-
88
- ```ruby
89
- ReportReactor.continue(
90
- id: "uuid-123",
91
- payload: { status: "completed", url: "..." },
92
- step_name: :wait_for_report
93
- )
94
- ```
95
-
96
- ### By Correlation ID
97
-
98
- ```ruby
99
- ReportReactor.continue_by_correlation_id(
100
- correlation_id: "report-999",
101
- payload: { status: "completed", url: "..." },
102
- step_name: :wait_for_report
103
- )
104
- ```
105
-
106
- ### Resuming Method Styles
107
-
108
- There are two ways to invoke continuation:
109
-
110
- 1. **Strict / Fire-and-Forget (Class Method)**:
111
- * `Reactor.continue(...)`
112
- * If payload is invalid, it **automatically compensates (undo)** and cancels the reactor.
113
- * Best for webhooks where you can't ask the sender to fix the payload.
114
-
115
- 2. **Flexible (Instance Method)**:
116
- * First find the reactor: `reactor = ReportReactor.find("uuid-123")`
117
- * Then call: `result = reactor.continue(payload: ..., step_name: :wait_for_report)`
118
- * If payload is invalid, it returns a failure result but **does not** cancel execution.
119
- * Allows you to handle the error (e.g., show a form error to a user) and try again.
120
-
121
- ## Cancellation & Undo
122
-
123
- You can cancel a paused reactor if the operation is no longer needed.
124
-
125
- ```ruby
126
- # Undo: Runs defined undo/compensate blocks for completed steps in reverse order,
127
- # then marks the execution as cancelled.
128
- ReportReactor.undo("uuid-123")
129
-
130
- # Cancel: Stops execution immediately and marks the reactor as cancelled with the provided reason.
131
- # The context is preserved for inspection, but resumption is disabled.
132
- ReportReactor.cancel(id: "uuid-123", reason: "User cancelled")
133
- ```
134
-
135
- ## Common Use Cases
136
-
137
- ### Human Approvals
138
-
139
- ```ruby
140
- interrupt :wait_for_approval do
141
- wait_for :submit_request
142
- correlation_id { |ctx| "approval-#{ctx.input(:request_id)}" }
143
- end
144
-
145
- step :process_decision do
146
- argument :decision, result(:wait_for_approval)
147
- run do |args, _ctx|
148
- if args[:decision][:approved]
149
- Success("Approved")
150
- else
151
- Failure("Rejected")
152
- end
153
- end
154
- end
155
- ```
156
-
157
- ### Webhooks
158
-
159
- Use `correlation_id` to map an external resource ID (like a Payment Intent ID) back to the reactor waiting for confirmation.
160
-
161
- ### Scheduled Follow-ups
162
-
163
- Using `timeout` with `strategy: :active` to wake up a reactor after a delay if no external event occurs (e.g., expiring a reservation).