active_saga 0.1.0 → 0.1.3

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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/ADAPTERS.md +6 -6
  3. data/CHANGELOG.md +1 -1
  4. data/GUIDE.md +10 -10
  5. data/LICENSE +1 -1
  6. data/README.md +51 -51
  7. data/lib/{active_workflow → active_saga}/backoff.rb +1 -1
  8. data/lib/{active_workflow → active_saga}/configuration.rb +3 -3
  9. data/lib/{active_workflow → active_saga}/context.rb +1 -1
  10. data/lib/{active_workflow → active_saga}/dsl/options.rb +8 -8
  11. data/lib/{active_workflow → active_saga}/dsl/signals.rb +5 -5
  12. data/lib/{active_workflow → active_saga}/dsl/steps.rb +11 -11
  13. data/lib/{active_workflow → active_saga}/errors.rb +1 -1
  14. data/lib/{active_workflow → active_saga}/execution.rb +5 -5
  15. data/lib/{active_workflow → active_saga}/jobs/runner_job.rb +8 -8
  16. data/lib/active_saga/railtie.rb +17 -0
  17. data/lib/{active_workflow → active_saga}/serializers/json.rb +1 -1
  18. data/lib/{active_workflow → active_saga}/stores/active_record.rb +34 -34
  19. data/lib/{active_workflow → active_saga}/stores/base.rb +4 -4
  20. data/lib/{active_workflow → active_saga}/task.rb +5 -5
  21. data/lib/active_saga/version.rb +5 -0
  22. data/lib/{active_workflow → active_saga}/workflow.rb +5 -5
  23. data/lib/{active_workflow.rb → active_saga.rb} +21 -21
  24. data/lib/generators/{active_workflow → active_saga}/install/install_generator.rb +3 -13
  25. data/lib/generators/active_saga/install/templates/initializer.rb +8 -0
  26. data/lib/generators/{active_workflow/install/templates/migrations/create_active_workflow_tables.rb → active_saga/install/templates/migrations/create_active_saga_tables.rb} +14 -14
  27. data/lib/generators/{active_workflow → active_saga}/install/templates/sample_workflow.rb +2 -2
  28. data/lib/generators/{active_workflow → active_saga}/workflow/templates/workflow.rb +1 -1
  29. data/lib/generators/{active_workflow → active_saga}/workflow/workflow_generator.rb +1 -1
  30. metadata +26 -26
  31. data/lib/active_workflow/railtie.rb +0 -17
  32. data/lib/active_workflow/version.rb +0 -5
  33. data/lib/generators/active_workflow/install/templates/initializer.rb +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a2bf52fe5b851576cd054d43f636cb86410c395d57737b6291c7bace1d6a9a43
4
- data.tar.gz: c3c6ff1b642992fe0f70dc163ff6535f0fe62477fb854d810ff2de19207fc1e4
3
+ metadata.gz: f22bd87e089cbb7d4ccd08fe763e561743824bb335131d37a41e2b6b78f8da45
4
+ data.tar.gz: 8ba648a0deac1b45cd719fef2c23501d5f5e2ba9f098938ae75f302b6b72b2c8
5
5
  SHA512:
6
- metadata.gz: ccec4ca4cbc4b6e85a7a63dc667f0621a818046430b0772988e6785f84e5ffee49b081debf18a87536116725989cb5f296f85efc6dcfc436769f7b79638dc049
7
- data.tar.gz: ae5f9a4576295244f9a8d2c9b09100b8bba352c41df97d2ea7637e014a4922513849be049476230a46f720b503e7d8052e99dc08123c9e3a8662b35c31bfb7e3
6
+ metadata.gz: 72361a30caa2c5d3bf49474f10e0cfbf97a926e6280b57429c786e6fd27a801a1541488db02140a274dc425b7941e726145d5e2891123318ed3893b37d81dd96
7
+ data.tar.gz: 88ffd609d73f55490aea49d7d60871697b9e49fad0a8347cb01cc5fb3138cab8bd8635791b569b1bbfa62ccd3809c93c5d19226272e2c0098a0b54950a1c1566
data/ADAPTERS.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Store Adapters
2
2
 
3
- ActiveWorkflow persists workflow state through a strategy interface. This document explains the
3
+ ActiveSaga persists workflow state through a strategy interface. This document explains the
4
4
  contract adapters must implement and the behaviours the engine expects.
5
5
 
6
6
  ## Base Contract
7
7
 
8
- All adapters inherit from `ActiveWorkflow::Stores::Base` and must implement:
8
+ All adapters inherit from `ActiveSaga::Stores::Base` and must implement:
9
9
 
10
10
  - `start_execution(workflow_class:, context:, steps:, idempotency_key:, timeout:, metadata:)`
11
11
  - `load_execution(id)`
@@ -41,9 +41,9 @@ Adapters are responsible for:
41
41
 
42
42
  The built-in adapter is designed for relational databases and uses three tables:
43
43
 
44
- - `aw_executions` – workflow metadata and context.
45
- - `aw_steps` – per-step state machine and retry/timeout metadata.
46
- - `aw_events` – signal payloads awaiting consumption.
44
+ - `as_executions` – workflow metadata and context.
45
+ - `as_steps` – per-step state machine and retry/timeout metadata.
46
+ - `as_events` – signal payloads awaiting consumption.
47
47
 
48
48
  If you need to customise the schema (prefixed table names, auditing columns), pass your own models or
49
49
  subclass the adapter. Ensure indexes that protect idempotency keys remain unique.
@@ -54,7 +54,7 @@ Use the acceptance spec suite as a smoke test for new adapters. You can swap the
54
54
  `spec_helper.rb` to your implementation and assert the scenarios continue to pass:
55
55
 
56
56
  ```ruby
57
- ActiveWorkflow.configure do |config|
57
+ ActiveSaga.configure do |config|
58
58
  config.store = MyRedisStore.new
59
59
  end
60
60
  ```
data/CHANGELOG.md CHANGED
@@ -4,4 +4,4 @@ All notable changes to this project will be documented here.
4
4
 
5
5
  ## [0.1.0] - 2025-12-10
6
6
 
7
- - Initial release of ActiveWorkflow with ActiveRecord store, async steps, signals, retries, compensations, cancellation API, idempotency, and generators.
7
+ - Initial release of ActiveSaga with ActiveRecord store, async steps, signals, retries, compensations, cancellation API, idempotency, and generators.
data/GUIDE.md CHANGED
@@ -1,7 +1,7 @@
1
- # ActiveWorkflow Guide
1
+ # ActiveSaga Guide
2
2
 
3
3
  This guide expands on the README and focuses on advanced topics you will likely encounter when
4
- operating ActiveWorkflow in production.
4
+ operating ActiveSaga in production.
5
5
 
6
6
  - [Security & Webhooks](#security--webhooks)
7
7
  - [Correlation Patterns](#correlation-patterns)
@@ -10,8 +10,8 @@ operating ActiveWorkflow in production.
10
10
 
11
11
  ## Security & Webhooks
12
12
 
13
- Async workflows commonly rely on webhook callbacks to call `ActiveWorkflow.complete_step!` or
14
- `ActiveWorkflow.fail_step!`. Treat these endpoints with the same rigor as payment webhooks:
13
+ Async workflows commonly rely on webhook callbacks to call `ActiveSaga.complete_step!` or
14
+ `ActiveSaga.fail_step!`. Treat these endpoints with the same rigor as payment webhooks:
15
15
 
16
16
  1. **Authenticate** inbound requests. Sign responses from your external system and verify via HMAC
17
17
  in the controller before mutating workflow state.
@@ -29,11 +29,11 @@ class ExportsController < ApplicationController
29
29
  before_action :verify_hmac!
30
30
 
31
31
  def ready
32
- ActiveWorkflow.complete_step!(params[:execution_id], :export,
32
+ ActiveSaga.complete_step!(params[:execution_id], :export,
33
33
  payload: params.require(:payload).permit(:url),
34
34
  idempotency_key: params[:event_id])
35
35
  head :ok
36
- rescue ActiveWorkflow::Errors::AsyncCompletionConflict
36
+ rescue ActiveSaga::Errors::AsyncCompletionConflict
37
37
  head :unprocessable_entity
38
38
  end
39
39
  end
@@ -41,7 +41,7 @@ end
41
41
 
42
42
  ## Correlation Patterns
43
43
 
44
- ActiveWorkflow does not impose a single correlation strategy. Some battle-tested options:
44
+ ActiveSaga does not impose a single correlation strategy. Some battle-tested options:
45
45
 
46
46
  - **Execution scoped** – `"#{execution.id}:#{step_name}"` works well when the external system does
47
47
  not need to know about your domain objects.
@@ -54,11 +54,11 @@ timeouts, heartbeats, and compensations.
54
54
 
55
55
  ## Extending Stores
56
56
 
57
- `ActiveWorkflow::Stores::Base` defines the contract for persistence. To write a custom adapter
57
+ `ActiveSaga::Stores::Base` defines the contract for persistence. To write a custom adapter
58
58
  (Redis, DynamoDB…), inherit from the base class and implement:
59
59
 
60
60
  - `start_execution` – create an execution with initial steps and enqueue the runner job.
61
- - `load_execution` – return an `ActiveWorkflow::Execution` snapshot.
61
+ - `load_execution` – return an `ActiveSaga::Execution` snapshot.
62
62
  - `process_execution` – pop and execute the next step atomically.
63
63
  - Async APIs – `complete_step!`, `fail_step!`, `extend_timeout!`, `heartbeat!`, `signal!`.
64
64
 
@@ -71,7 +71,7 @@ The store emits `ActiveSupport::Notifications` for every significant transition.
71
71
  app and ship them to OpenTelemetry, Datadog, or logs. A simple logger subscriber looks like this:
72
72
 
73
73
  ```ruby
74
- ActiveSupport::Notifications.subscribe(/active_workflow\./) do |event|
74
+ ActiveSupport::Notifications.subscribe(/active_saga\./) do |event|
75
75
  Rails.logger.info({ name: event.name, payload: event.payload, duration: event.duration }.to_json)
76
76
  end
77
77
  ```
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 ActiveWorkflow
3
+ Copyright (c) 2024 ActiveSaga
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # ActiveWorkflow
1
+ # ActiveSaga
2
2
 
3
3
  A workflow engine with durable steps, automatic retries with backoff and jitter, compensations, async waits, signals, timeouts, idempotency, and pluggable persistence (ActiveRecord included).
4
4
 
@@ -6,7 +6,7 @@ A workflow engine with durable steps, automatic retries with backoff and jitter,
6
6
 
7
7
  ## Table of Contents
8
8
 
9
- - [Why ActiveWorkflow?](#why-activeworkflow)
9
+ - [Why ActiveSaga?](#why-activesaga)
10
10
  - [Features](#features)
11
11
  - [Installation](#installation)
12
12
  - [Quick Start](#quick-start)
@@ -36,9 +36,9 @@ A workflow engine with durable steps, automatic retries with backoff and jitter,
36
36
 
37
37
  ---
38
38
 
39
- ## Why ActiveWorkflow?
39
+ ## Why ActiveSaga?
40
40
 
41
- Most apps need reliable business workflows (sagas) with retries, compensations and “wait for an external thing (webhook/job)”—without operating a heavyweight orchestration service. **ActiveWorkflow** gives you that on plain Rails + Active Job.
41
+ Most apps need reliable business workflows (sagas) with retries, compensations and “wait for an external thing (webhook/job)”—without operating a heavyweight orchestration service. **ActiveSaga** gives you that on plain Rails + Active Job.
42
42
 
43
43
  ---
44
44
 
@@ -60,16 +60,16 @@ Most apps need reliable business workflows (sagas) with retries, compensations a
60
60
 
61
61
  ```bash
62
62
  # Gemfile
63
- gem "active_workflow"
63
+ gem "active_saga"
64
64
 
65
65
  bundle install
66
- rails g active_workflow:install
66
+ rails g active_saga:install
67
67
  rails db:migrate
68
68
  ```
69
69
 
70
70
  The installer creates:
71
71
 
72
- - `config/initializers/active_workflow.rb`
72
+ - `config/initializers/active_saga.rb`
73
73
  - ActiveRecord migrations
74
74
  - Optional sample workflow
75
75
 
@@ -78,9 +78,9 @@ The installer creates:
78
78
  ## Quick Start
79
79
 
80
80
  ```ruby
81
- # config/initializers/active_workflow.rb
82
- ActiveWorkflow.configure do |c|
83
- c.store = ActiveWorkflow::Stores::ActiveRecord.new
81
+ # config/initializers/active_saga.rb
82
+ ActiveSaga.configure do |c|
83
+ c.store = ActiveSaga::Stores::ActiveRecord.new
84
84
  c.logger = Rails.logger
85
85
  c.serializer = :json
86
86
  c.clock = -> { Time.now.utc }
@@ -89,7 +89,7 @@ end
89
89
 
90
90
  ```ruby
91
91
  # app/workflows/checkout_flow.rb
92
- class CheckoutFlow < ActiveWorkflow::Workflow
92
+ class CheckoutFlow < ActiveSaga::Workflow
93
93
  idempotency_key { "checkout:#{ctx[:order_id]}" }
94
94
  defaults retry: { max: 5, backoff: :exponential, jitter: true, first_delay: 1.second }
95
95
  timeout 30.minutes
@@ -130,7 +130,7 @@ All step forms accept the **same options**:
130
130
  ### A) Method step
131
131
 
132
132
  ```ruby
133
- class ExampleFlow < ActiveWorkflow::Workflow
133
+ class ExampleFlow < ActiveSaga::Workflow
134
134
  step :do_work, compensate: :undo_work
135
135
 
136
136
  def do_work
@@ -147,7 +147,7 @@ end
147
147
  ### B) Task class
148
148
 
149
149
  ```ruby
150
- class ReserveStockTask < ActiveWorkflow::Task
150
+ class ReserveStockTask < ActiveSaga::Task
151
151
  def call(ctx)
152
152
  res = Inventory.reserve!(ctx[:order_id])
153
153
  { reservation_id: res.id } # merged into ctx
@@ -158,7 +158,7 @@ class ReserveStockTask < ActiveWorkflow::Task
158
158
  end
159
159
  end
160
160
 
161
- class CheckoutFlow < ActiveWorkflow::Workflow
161
+ class CheckoutFlow < ActiveSaga::Workflow
162
162
  task :reserve_stock, ReserveStockTask
163
163
  end
164
164
  ```
@@ -166,7 +166,7 @@ end
166
166
  ### C) Inline block
167
167
 
168
168
  ```ruby
169
- class NotifyFlow < ActiveWorkflow::Workflow
169
+ class NotifyFlow < ActiveSaga::Workflow
170
170
  task :send_email do |ctx|
171
171
  Mailers::Notice.deliver_later(ctx[:user_id])
172
172
  nil
@@ -189,7 +189,7 @@ end
189
189
  task :arrange_fulfillment, FulfillmentTask, async: true, timeout: 15.minutes
190
190
 
191
191
  # Class-level declaration
192
- class FulfillmentTask < ActiveWorkflow::Task
192
+ class FulfillmentTask < ActiveSaga::Task
193
193
  async! timeout: 15.minutes
194
194
  def call(ctx)
195
195
  job = FulfillmentAPI.create_job!(order_id: ctx[:order_id]) # initiate
@@ -209,19 +209,19 @@ end
209
209
 
210
210
  ```ruby
211
211
  # Mark an async step as completed and advance the flow
212
- ActiveWorkflow.complete_step!(execution_id, :arrange_fulfillment,
212
+ ActiveSaga.complete_step!(execution_id, :arrange_fulfillment,
213
213
  payload: { tracking: "XYZ" }, idempotency_key: params[:event_id])
214
214
 
215
215
  # Explicit failure path while waiting
216
- ActiveWorkflow.fail_step!(execution_id, :arrange_fulfillment,
216
+ ActiveSaga.fail_step!(execution_id, :arrange_fulfillment,
217
217
  error_class: "RemoteError", message: "Timeout from vendor", details: { ... },
218
218
  idempotency_key: params[:event_id])
219
219
 
220
220
  # Optional: extend waiting timeout
221
- ActiveWorkflow.extend_timeout!(execution_id, :arrange_fulfillment, by: 10.minutes)
221
+ ActiveSaga.extend_timeout!(execution_id, :arrange_fulfillment, by: 10.minutes)
222
222
 
223
223
  # Optional: heartbeat for monitoring
224
- ActiveWorkflow.heartbeat!(execution_id, :arrange_fulfillment)
224
+ ActiveSaga.heartbeat!(execution_id, :arrange_fulfillment)
225
225
  ```
226
226
 
227
227
  **Payload storage:**
@@ -261,14 +261,14 @@ ActiveWorkflow.heartbeat!(execution_id, :arrange_fulfillment)
261
261
  Pause the workflow until a signal arrives.
262
262
 
263
263
  ```ruby
264
- class ApprovalFlow < ActiveWorkflow::Workflow
264
+ class ApprovalFlow < ActiveSaga::Workflow
265
265
  task :prepare, PrepareTask
266
266
  wait_for_signal :approval, as: :approval
267
267
  task :finalize, FinalizeTask, args: ->(ctx) { [ctx[:approval]] }
268
268
  end
269
269
 
270
270
  # Controller/webhook sending a signal:
271
- ActiveWorkflow.signal!(params[:execution_id], :approval,
271
+ ActiveSaga.signal!(params[:execution_id], :approval,
272
272
  payload: { approved_by: current_user.id, decision: "approved" })
273
273
  ```
274
274
 
@@ -282,7 +282,7 @@ Stop a running workflow (for example, when the user backs out).
282
282
 
283
283
  ```ruby
284
284
  # Cancel via execution id (runs compensations, cancels remaining steps)
285
- ActiveWorkflow.cancel!(execution.id, reason: "user_request")
285
+ ActiveSaga.cancel!(execution.id, reason: "user_request")
286
286
 
287
287
  # Or on the execution instance
288
288
  execution.cancel!(reason: "user_request")
@@ -293,7 +293,7 @@ Cancellation:
293
293
  - Runs compensations for completed steps in reverse order.
294
294
  - Marks pending/running/waiting steps as `cancelled` and clears scheduled timeouts.
295
295
  - Transitions the execution to the terminal `cancelled` state (`cancelled_at` timestamp set).
296
- - Emits `active_workflow.execution.cancelled` for observability.
296
+ - Emits `active_saga.execution.cancelled` for observability.
297
297
 
298
298
  Repeated calls are safe (idempotent).
299
299
 
@@ -326,8 +326,8 @@ step :slow_call, retry: { max: 10, backoff: :fixed, delay: 5.seconds }
326
326
  ## Configuration
327
327
 
328
328
  ```ruby
329
- ActiveWorkflow.configure do |c|
330
- c.store = ActiveWorkflow::Stores::ActiveRecord.new
329
+ ActiveSaga.configure do |c|
330
+ c.store = ActiveSaga::Stores::ActiveRecord.new
331
331
  c.logger = Rails.logger
332
332
  c.serializer = :json # or a custom object responding to dump/load
333
333
  c.clock = -> { Time.now.utc }
@@ -338,25 +338,25 @@ end
338
338
 
339
339
  ## Persistence (Adapters)
340
340
 
341
- ActiveWorkflow uses a **Store** strategy.
341
+ ActiveSaga uses a **Store** strategy.
342
342
 
343
343
  **ActiveRecord store** (ships with gem):
344
344
 
345
- - Tables (prefix `aw_`):
346
- - `aw_executions`: workflow metadata (`workflow_class`, `state`, `ctx` jsonb, `cursor_step`, `idempotency_key`, `cancelled_at`, timestamps)
347
- - `aw_steps`: per-step status (`pending|running|waiting|completed|failed|timed_out|compensating|compensated`), attempts, backoff data, `init_result`, `completion_payload`, `timeout_at`, `waiting_since`, `heartbeat_at`, `correlation_id`, `completion_idempotency_key`
348
- - `aw_events`: signals etc. (`name`, `payload`, `consumed_at`)
349
- - (Optional) `aw_timers`: scheduled timeouts/backoffs (or embed in steps)
345
+ - Tables (prefix `as_`):
346
+ - `as_executions`: workflow metadata (`workflow_class`, `state`, `ctx` jsonb, `cursor_step`, `idempotency_key`, `cancelled_at`, timestamps)
347
+ - `as_steps`: per-step status (`pending|running|waiting|completed|failed|timed_out|compensating|compensated`), attempts, backoff data, `init_result`, `completion_payload`, `timeout_at`, `waiting_since`, `heartbeat_at`, `correlation_id`, `completion_idempotency_key`
348
+ - `as_events`: signals etc. (`name`, `payload`, `consumed_at`)
349
+ - (Optional) `as_timers`: scheduled timeouts/backoffs (or embed in steps)
350
350
  - Indexing: unique on `idempotency_key`; partial unique on `(execution_id, step_name, completion_idempotency_key) WHERE completion_idempotency_key IS NOT NULL`; JSONB GIN on payloads if needed.
351
351
  - Concurrency: transactional updates + row locks / SKIP LOCKED.
352
352
 
353
- You can implement other stores (e.g., Redis) by following the `ActiveWorkflow::Store` interface.
353
+ You can implement other stores (e.g., Redis) by following the `ActiveSaga::Store` interface.
354
354
 
355
355
  ---
356
356
 
357
357
  ## Active Job Integration
358
358
 
359
- - A single internal job (e.g., `ActiveWorkflow::Jobs::RunnerJob`) receives execution/step IDs and performs the next transition safely.
359
+ - A single internal job (e.g., `ActiveSaga::Jobs::RunnerJob`) receives execution/step IDs and performs the next transition safely.
360
360
  - **No dependency** on specific backends (Sidekiq, Solid Queue, etc.)—any Active Job adapter works.
361
361
  - Timers/backoffs are scheduled via `set(wait_until: ...)`.
362
362
 
@@ -368,16 +368,16 @@ Subscribe with `ActiveSupport::Notifications`:
368
368
 
369
369
  Events include:
370
370
 
371
- - `active_workflow.step.started`
372
- - `active_workflow.step.completed`
373
- - `active_workflow.step.failed`
374
- - `active_workflow.step.waiting` _(async initiation successful)_
375
- - `active_workflow.step.completed_async` _(after `complete_step!`)_
376
- - `active_workflow.step.failed_async` _(after `fail_step!`)_
377
- - `active_workflow.step.timeout`
378
- - `active_workflow.retry.scheduled`
379
- - `active_workflow.signal.received`
380
- - `active_workflow.execution.cancelled`
371
+ - `active_saga.step.started`
372
+ - `active_saga.step.completed`
373
+ - `active_saga.step.failed`
374
+ - `active_saga.step.waiting` _(async initiation successful)_
375
+ - `active_saga.step.completed_async` _(after `complete_step!`)_
376
+ - `active_saga.step.failed_async` _(after `fail_step!`)_
377
+ - `active_saga.step.timeout`
378
+ - `active_saga.retry.scheduled`
379
+ - `active_saga.signal.received`
380
+ - `active_saga.execution.cancelled`
381
381
 
382
382
  Each event carries identifiers (execution_id, workflow, step), timings, attempts, and error details (when applicable).
383
383
 
@@ -386,8 +386,8 @@ Each event carries identifiers (execution_id, workflow, step), timings, attempts
386
386
  ## Generators
387
387
 
388
388
  ```bash
389
- rails g active_workflow:install # initializer + migrations + sample
390
- rails g active_workflow:workflow CheckoutFlow
389
+ rails g active_saga:install # initializer + migrations + sample
390
+ rails g active_saga:workflow CheckoutFlow
391
391
  ```
392
392
 
393
393
  The workflow generator scaffolds a class under `app/workflows/`.
@@ -404,7 +404,7 @@ The workflow generator scaffolds a class under `app/workflows/`.
404
404
  ```
405
405
  - Simulate async completion:
406
406
  ```ruby
407
- ActiveWorkflow.complete_step!(flow.id, :export, payload: { url: "..." }, idempotency_key: "evt-1")
407
+ ActiveSaga.complete_step!(flow.id, :export, payload: { url: "..." }, idempotency_key: "evt-1")
408
408
  ```
409
409
  - Cover:
410
410
  - retries/backoff behavior,
@@ -422,7 +422,7 @@ The workflow generator scaffolds a class under `app/workflows/`.
422
422
  **Signal workflow**
423
423
 
424
424
  ```ruby
425
- class SignalWorkflow < ActiveWorkflow::Workflow
425
+ class SignalWorkflow < ActiveSaga::Workflow
426
426
  task :prepare, PrepareTask
427
427
  wait_for_signal :approval, as: :approval
428
428
  task :finalize, FinalizeTask, args: ->(ctx) { [ctx[:approval]] }
@@ -432,14 +432,14 @@ end
432
432
  **Async export + notify**
433
433
 
434
434
  ```ruby
435
- class ExportFlow < ActiveWorkflow::Workflow
435
+ class ExportFlow < ActiveSaga::Workflow
436
436
  task :request_export, ExporterTask, async: true, timeout: 30.minutes
437
437
  task :notify_user do |ctx|
438
438
  Mailers::ExportReady.deliver_later(ctx[:user_id], ctx[:request_export][:download_url])
439
439
  end
440
440
  end
441
441
 
442
- class ExporterTask < ActiveWorkflow::Task
442
+ class ExporterTask < ActiveSaga::Task
443
443
  async! timeout: 30.minutes
444
444
  def call(ctx)
445
445
  req = ExportAPI.create!(user_id: ctx[:user_id], request_id: "#{ctx[:execution_id]}:request_export")
@@ -452,7 +452,7 @@ Webhook:
452
452
 
453
453
  ```ruby
454
454
  def export_ready
455
- ActiveWorkflow.complete_step!(params[:exec_id], :request_export,
455
+ ActiveSaga.complete_step!(params[:exec_id], :request_export,
456
456
  payload: { download_url: params[:url] }, idempotency_key: params[:event_id])
457
457
  head :ok
458
458
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveWorkflow
3
+ module ActiveSaga
4
4
  module Backoff
5
5
  module_function
6
6
 
@@ -2,20 +2,20 @@
2
2
 
3
3
  require "active_support/logger"
4
4
 
5
- module ActiveWorkflow
5
+ module ActiveSaga
6
6
  # Holds gem-level configuration.
7
7
  class Configuration
8
8
  attr_accessor :store, :serializer, :logger, :clock
9
9
 
10
10
  def initialize
11
- @serializer = ActiveWorkflow::Serializers::Json.new
11
+ @serializer = ActiveSaga::Serializers::Json.new
12
12
  @logger = ActiveSupport::Logger.new($stdout, level: :info)
13
13
  @clock = -> { Time.now.utc }
14
14
  end
15
15
 
16
16
  # Ensures store is set before usage.
17
17
  def store!
18
- raise ActiveWorkflow::Errors::Configuration, "ActiveWorkflow.store is not configured" unless store
18
+ raise ActiveSaga::Errors::Configuration, "ActiveSaga.store is not configured" unless store
19
19
 
20
20
  store
21
21
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require "active_support/hash_with_indifferent_access"
4
4
 
5
- module ActiveWorkflow
5
+ module ActiveSaga
6
6
  # TODO Revisit impl.
7
7
  class Context
8
8
  include Enumerable
@@ -1,29 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveWorkflow
3
+ module ActiveSaga
4
4
  module DSL
5
5
  # Configuration helpers shared by workflows.
6
6
  module Options
7
7
  def self.extended(base)
8
- base.class_attribute :_aw_defaults, instance_writer: false, default: {}
9
- base.class_attribute :_aw_timeout, instance_writer: false, default: nil
10
- base.class_attribute :_aw_idempotency_block, instance_writer: false, default: nil
8
+ base.class_attribute :_as_defaults, instance_writer: false, default: {}
9
+ base.class_attribute :_as_timeout, instance_writer: false, default: nil
10
+ base.class_attribute :_as_idempotency_block, instance_writer: false, default: nil
11
11
  end
12
12
 
13
13
  def defaults(opts = nil)
14
14
  if opts
15
- self._aw_defaults = (_aw_defaults.deep_dup || {}).deep_merge(opts.deep_symbolize_keys)
15
+ self._as_defaults = (_as_defaults.deep_dup || {}).deep_merge(opts.deep_symbolize_keys)
16
16
  else
17
- _aw_defaults.deep_dup || {}
17
+ _as_defaults.deep_dup || {}
18
18
  end
19
19
  end
20
20
 
21
21
  def timeout(value = nil)
22
- value ? self._aw_timeout = value : _aw_timeout
22
+ value ? self._as_timeout = value : _as_timeout
23
23
  end
24
24
 
25
25
  def idempotency_key(&block)
26
- block ? self._aw_idempotency_block = block : _aw_idempotency_block
26
+ block ? self._as_idempotency_block = block : _as_idempotency_block
27
27
  end
28
28
 
29
29
  def resolve_defaults(step_options)
@@ -1,23 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveWorkflow
3
+ module ActiveSaga
4
4
  module DSL
5
5
  module Signals
6
6
  def self.extended(base)
7
- base.class_attribute :_aw_signal_handlers, instance_writer: false, default: {} # name => method
7
+ base.class_attribute :_as_signal_handlers, instance_writer: false, default: {} # name => method
8
8
  end
9
9
 
10
10
  def inherited(subclass)
11
11
  super
12
- subclass._aw_signal_handlers = _aw_signal_handlers.dup
12
+ subclass._as_signal_handlers = _as_signal_handlers.dup
13
13
  end
14
14
 
15
15
  def signal(name, to:)
16
- self._aw_signal_handlers = _aw_signal_handlers.merge(name.to_sym => to.to_sym)
16
+ self._as_signal_handlers = _as_signal_handlers.merge(name.to_sym => to.to_sym)
17
17
  end
18
18
 
19
19
  def signal_handler_for(name)
20
- _aw_signal_handlers[name.to_sym]
20
+ _as_signal_handlers[name.to_sym]
21
21
  end
22
22
  end
23
23
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveWorkflow
3
+ module ActiveSaga
4
4
  module DSL
5
5
  module Steps
6
6
  StepDefinition = Struct.new(:name, :style, :callable, :options, keyword_init: true) do
@@ -67,21 +67,21 @@ module ActiveWorkflow
67
67
  end
68
68
 
69
69
  def self.extended(base)
70
- base.class_attribute :_aw_steps, instance_writer: false, default: []
70
+ base.class_attribute :_as_steps, instance_writer: false, default: []
71
71
  end
72
72
 
73
73
  def inherited(subclass)
74
74
  super
75
- subclass._aw_steps = _aw_steps.map(&:dup)
75
+ subclass._as_steps = _as_steps.map(&:dup)
76
76
  end
77
77
 
78
78
  def steps
79
- _aw_steps
79
+ _as_steps
80
80
  end
81
81
 
82
82
  def step_definition(name)
83
83
  steps.find { |definition| definition.name == name.to_sym } ||
84
- raise(ActiveWorkflow::Errors::InvalidStep, "Unknown step: #{name}")
84
+ raise(ActiveSaga::Errors::InvalidStep, "Unknown step: #{name}")
85
85
  end
86
86
 
87
87
  def step(name, **options)
@@ -90,21 +90,21 @@ module ActiveWorkflow
90
90
 
91
91
  def task(name, handler = nil, **options, &block)
92
92
  if handler && block
93
- raise ActiveWorkflow::Errors::InvalidStep, "Provide a Task class or a block, not both"
93
+ raise ActiveSaga::Errors::InvalidStep, "Provide a Task class or a block, not both"
94
94
  end
95
95
 
96
96
  callable = handler || block
97
97
  style = if block
98
98
  :block
99
- elsif handler.is_a?(Class) && handler <= ActiveWorkflow::Task
99
+ elsif handler.is_a?(Class) && handler <= ActiveSaga::Task
100
100
  :task
101
101
  elsif handler.respond_to?(:call)
102
102
  :callable
103
103
  else
104
- raise ActiveWorkflow::Errors::InvalidStep, "Task handler must be a Task subclass, callable, or block"
104
+ raise ActiveSaga::Errors::InvalidStep, "Task handler must be a Task subclass, callable, or block"
105
105
  end
106
106
 
107
- step_options = if handler.is_a?(Class) && handler <= ActiveWorkflow::Task
107
+ step_options = if handler.is_a?(Class) && handler <= ActiveSaga::Task
108
108
  handler.async_options.deep_merge(options)
109
109
  else
110
110
  options
@@ -123,8 +123,8 @@ module ActiveWorkflow
123
123
  name = name.to_sym
124
124
  step_options = resolve_defaults((options || {}).deep_symbolize_keys)
125
125
  definition = StepDefinition.new(name:, style:, callable:, options: step_options)
126
- remaining = _aw_steps.reject { |step| step.name == name }
127
- self._aw_steps = remaining + [definition]
126
+ remaining = _as_steps.reject { |step| step.name == name }
127
+ self._as_steps = remaining + [definition]
128
128
  end
129
129
  end
130
130
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveWorkflow
3
+ module ActiveSaga
4
4
  module Errors
5
5
  class Error < StandardError; end
6
6
 
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module ActiveWorkflow
3
+ module ActiveSaga
4
4
  # Lightweight value object representing a workflow execution.
5
5
  class Execution
6
6
  attr_reader :id, :workflow_class, :state, :ctx, :cursor_step, :created_at, :updated_at, :cancelled_at
7
7
 
8
- def initialize(id:, workflow_class:, state:, ctx:, cursor_step:, created_at:, updated_at:, cancelled_at: nil, store: ActiveWorkflow.store)
8
+ def initialize(id:, workflow_class:, state:, ctx:, cursor_step:, created_at:, updated_at:, cancelled_at: nil, store: ActiveSaga.store)
9
9
  @id = id
10
10
  @workflow_class = workflow_class
11
11
  @state = state
12
- @ctx = ActiveWorkflow::Context.new(ctx)
12
+ @ctx = ActiveSaga::Context.new(ctx)
13
13
  @cursor_step = cursor_step&.to_sym
14
14
  @created_at = created_at
15
15
  @updated_at = updated_at
@@ -35,7 +35,7 @@ module ActiveWorkflow
35
35
  # @param timeout [Numeric, nil] seconds to wait, nil for indefinite
36
36
  # @param interval [Numeric] polling interval
37
37
  def await(timeout: nil, interval: 0.5)
38
- clock = ActiveWorkflow.configuration.clock
38
+ clock = ActiveSaga.configuration.clock
39
39
  deadline = timeout && clock.call + timeout
40
40
  loop do
41
41
  break if terminal?
@@ -58,7 +58,7 @@ module ActiveWorkflow
58
58
  end
59
59
 
60
60
  def cancel!(reason: nil)
61
- ActiveWorkflow.cancel!(id, reason: reason)
61
+ ActiveSaga.cancel!(id, reason: reason)
62
62
  reload!
63
63
  end
64
64