chrono_forge 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f65c15313d0538a84fe916c5d3fd1966e593364d00e777aa2d1693a5bc801102
4
- data.tar.gz: 9a3a1c025dd348d46c9169bb03a3fc26a975b5c0eb5411d032410c9f8a70e564
3
+ metadata.gz: 8c9315ad74de7245484119f385f2eb9706d1e3e532e142696d03d204e37f81ad
4
+ data.tar.gz: 4ea5ef5858c6d5fc8903f5432dd1ddc643d3cb6d26e65693c8a30664224a51c5
5
5
  SHA512:
6
- metadata.gz: 31c16f81bbfefd72f6ad4799a2ab7eeac2ffe11a8a8f2d12606632b36d843a8dc518c27a0e2f5a992184619004c7de5f3e60c4b19f55ab63b38f49e67ee32e79
7
- data.tar.gz: 233e0badc22ad43f4084b2acfc83782d41da6b0949d6b7ee9b050b89ee4a9fbbbac46dd74029470af58dd4293767f1200ee46b3ed19dfa7eaba89c3901e8b3f8
6
+ metadata.gz: 4713568fd2d32ddc70737fb5eb2f91db90aa41ff80d446afcd91f79719938e427f87b06da69dae18fb253c751aec07a70954e2f941053cb748db7fed04304ff0
7
+ data.tar.gz: caed47e85300d073b9ee1309d33b05047f2b0e6f60e95612ca247c86f233a32c823f9d68307e77384573c250b1779407e630a2f7ecd3557d2c033482f4326263
data/README.md CHANGED
@@ -1,21 +1,26 @@
1
1
  # ChronoForge
2
2
 
3
- ChronoForge is a Ruby gem that provides a robust framework for building durable, distributed workflows in Ruby on Rails applications. It offers a reliable way to handle long-running processes, state management, and error recovery.
3
+ ![Version](https://img.shields.io/badge/version-0.3.0-blue.svg)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
5
 
5
- ## Features
6
+ > A robust framework for building durable, distributed workflows in Ruby on Rails applications
7
+
8
+ ChronoForge provides a powerful solution for handling long-running processes, managing state, and recovering from failures in your Rails applications. Built on top of ActiveJob, it ensures your critical business processes remain resilient and traceable.
9
+
10
+ ## 🌟 Features
6
11
 
7
12
  - **Durable Execution**: Automatically tracks and recovers from failures during workflow execution
8
- - **State Management**: Built-in workflow state tracking with support for custom contexts
9
- - **Concurrency Control**: Advanced locking mechanisms to prevent concurrent execution of the same workflow
10
- - **Error Handling**: Comprehensive error tracking and retry strategies
11
- - **Execution Logging**: Detailed logging of workflow execution steps and errors
13
+ - **State Management**: Built-in workflow state tracking with persistent context storage
14
+ - **Concurrency Control**: Advanced locking mechanisms to prevent parallel execution of the same workflow
15
+ - **Error Handling**: Comprehensive error tracking with configurable retry strategies
16
+ - **Execution Logging**: Detailed logging of workflow steps and errors for visibility
12
17
  - **Wait States**: Support for time-based waits and condition-based waiting
13
- - **Database-Backed**: All workflow state is persisted to the database for durability
14
- - **ActiveJob Integration**: Seamlessly works with any ActiveJob backend
18
+ - **Database-Backed**: All workflow state is persisted to ensure durability
19
+ - **ActiveJob Integration**: Compatible with all ActiveJob backends, though database-backed processors (like Solid Queue) provide the most reliable experience for long-running workflows
15
20
 
16
- ## Installation
21
+ ## 📦 Installation
17
22
 
18
- Add this line to your application's Gemfile:
23
+ Add to your application's Gemfile:
19
24
 
20
25
  ```ruby
21
26
  gem 'chrono_forge'
@@ -27,7 +32,7 @@ Then execute:
27
32
  $ bundle install
28
33
  ```
29
34
 
30
- Or install it directly:
35
+ Or install directly:
31
36
 
32
37
  ```bash
33
38
  $ gem install chrono_forge
@@ -40,7 +45,33 @@ $ rails generate chrono_forge:install
40
45
  $ rails db:migrate
41
46
  ```
42
47
 
43
- ## Usage
48
+ ## 📋 Usage
49
+
50
+ ### Creating and Executing Workflows
51
+
52
+ ChronoForge workflows are ActiveJob classes that prepend the `ChronoForge::Executor` module. Each workflow can **only** accept keyword arguments:
53
+
54
+ ```ruby
55
+ # Define your workflow class
56
+ class OrderProcessingWorkflow < ApplicationJob
57
+ prepend ChronoForge::Executor
58
+
59
+ def perform(order_id:, customer_id:)
60
+ # Workflow steps...
61
+ end
62
+ end
63
+ ```
64
+
65
+ All workflows require a unique identifier when executed. This identifier is used to track and manage the workflow:
66
+
67
+ ```ruby
68
+ # Execute the workflow
69
+ OrderProcessingWorkflow.perform_later(
70
+ "order-123", # Unique workflow key
71
+ order_id: "order-134", # Custom kwargs
72
+ customer_id: "customer-456" # More custom kwargs
73
+ )
74
+ ```
44
75
 
45
76
  ### Basic Workflow Example
46
77
 
@@ -48,11 +79,13 @@ Here's a complete example of a durable order processing workflow:
48
79
 
49
80
  ```ruby
50
81
  class OrderProcessingWorkflow < ApplicationJob
51
- include ChronoForge::Executor
82
+ prepend ChronoForge::Executor
83
+
84
+ def perform(order_id:)
85
+ @order_id = order_id
52
86
 
53
- def perform
54
87
  # Context can be used to pass and store data between executions
55
- context["order_id"] = SecureRandom.hex
88
+ context.set_once "execution_id", SecureRandom.hex
56
89
 
57
90
  # Wait until payment is confirmed
58
91
  wait_until :payment_confirmed?
@@ -70,26 +103,48 @@ class OrderProcessingWorkflow < ApplicationJob
70
103
  private
71
104
 
72
105
  def payment_confirmed?
73
- PaymentService.confirmed?(context["order_id"])
106
+ PaymentService.confirmed?(@order_id, context["execution_id"])
74
107
  end
75
108
 
76
109
  def process_order
110
+ OrderProcessor.process(@order_id, context["execution_id"])
77
111
  context["processed_at"] = Time.current.iso8601
78
- OrderProcessor.process(context["order_id"])
79
112
  end
80
113
 
81
114
  def complete_order
115
+ OrderCompletionService.complete(@order_id, context["execution_id"])
82
116
  context["completed_at"] = Time.current.iso8601
83
- OrderCompletionService.complete(context["order_id"])
84
117
  end
85
118
  end
86
119
  ```
87
120
 
88
- ### Workflow Features
121
+ ### Core Workflow Features
89
122
 
90
- #### Durable Execution
123
+ #### 🚀 Executing Workflows
91
124
 
92
- The `durably_execute` method ensures operations are executed exactly once:
125
+ ChronoForge workflows are executed through ActiveJob's standard interface with a specific parameter structure:
126
+
127
+ ```ruby
128
+ # Perform the workflow immediately
129
+ OrderProcessingWorkflow.perform_now(
130
+ "order-123", # Unique workflow key
131
+ order_id: "O-123", # Custom parameter
132
+ customer_id: "C-456" # Another custom parameter
133
+ )
134
+
135
+ # Or queue it for background processing
136
+ OrderProcessingWorkflow.perform_later(
137
+ "order-123-async", # Unique workflow key
138
+ order_id: "O-124",
139
+ customer_id: "C-457"
140
+ )
141
+ ```
142
+
143
+ **Important:** Workflows must use keyword arguments only, not positional arguments.
144
+
145
+ #### ⚡ Durable Execution
146
+
147
+ The `durably_execute` method ensures operations are executed exactly once, even if the job fails and is retried:
93
148
 
94
149
  ```ruby
95
150
  # Execute a method
@@ -101,7 +156,7 @@ durably_execute -> (ctx) {
101
156
  }
102
157
  ```
103
158
 
104
- #### Wait States
159
+ #### ⏱️ Wait States
105
160
 
106
161
  ChronoForge supports both time-based and condition-based waits:
107
162
 
@@ -115,9 +170,9 @@ wait_until :payment_processed,
115
170
  check_interval: 5.minutes
116
171
  ```
117
172
 
118
- #### Workflow Context
173
+ #### 🔄 Workflow Context
119
174
 
120
- ChronoForge provides a persistent context that survives job restarts:
175
+ ChronoForge provides a persistent context that survives job restarts. The context behaves like a Hash but with additional capabilities:
121
176
 
122
177
  ```ruby
123
178
  # Set context values
@@ -126,34 +181,50 @@ context[:status] = "processing"
126
181
 
127
182
  # Read context values
128
183
  user_name = context[:user_name]
184
+
185
+ # Using the fetch method (returns default if key doesn't exist)
186
+ status = context.fetch(:status, "pending")
187
+
188
+ # Set a value with the set method (alias for []=)
189
+ context.set(:total_amount, 99.99)
190
+
191
+ # Set a value only if the key doesn't already exist
192
+ context.set_once(:created_at, Time.current.iso8601)
193
+
194
+ # Check if a key exists
195
+ if context.key?(:user_id)
196
+ # Do something with the user ID
197
+ end
129
198
  ```
130
199
 
131
- ### Error Handling
200
+ The context supports serializable Ruby objects (Hash, Array, String, Integer, Float, Boolean, and nil) and validates types automatically.
201
+
202
+ ### 🛡️ Error Handling
132
203
 
133
- ChronoForge automatically tracks errors and provides retry capabilities:
204
+ ChronoForge automatically tracks errors and provides configurable retry capabilities:
134
205
 
135
206
  ```ruby
136
207
  class MyWorkflow < ApplicationJob
137
- include ChronoForge::Executor
208
+ prepend ChronoForge::Executor
138
209
 
139
210
  private
140
211
 
141
212
  def should_retry?(error, attempt_count)
142
213
  case error
143
214
  when NetworkError
144
- attempt_count < 5
215
+ attempt_count < 5 # Retry network errors up to 5 times
145
216
  when ValidationError
146
217
  false # Don't retry validation errors
147
218
  else
148
- attempt_count < 3
219
+ attempt_count < 3 # Default retry policy
149
220
  end
150
221
  end
151
222
  end
152
223
  ```
153
224
 
154
- ## Testing
225
+ ## 🧪 Testing
155
226
 
156
- ChronoForge is designed to be easily testable using [ChaoticJob](https://github.com/fractaledmind/chaotic_job), a testing framework that makes it simple to test complex job workflows. Here's how to set up your test environment:
227
+ ChronoForge is designed to be easily testable using [ChaoticJob](https://github.com/fractaledmind/chaotic_job), a testing framework that makes it simple to test complex job workflows:
157
228
 
158
229
  1. Add ChaoticJob to your Gemfile's test group:
159
230
 
@@ -179,14 +250,18 @@ class WorkflowTest < ActiveJob::TestCase
179
250
  include ChaoticJob::Helpers
180
251
 
181
252
  def test_workflow_completion
182
- # Enqueue the job
183
- OrderProcessingWorkflow.perform_later("order_123")
253
+ # Enqueue the job with a unique key and custom parameters
254
+ OrderProcessingWorkflow.perform_later(
255
+ "order-test-123",
256
+ order_id: "O-123",
257
+ customer_id: "C-456"
258
+ )
184
259
 
185
260
  # Perform all enqueued jobs
186
261
  perform_all_jobs
187
262
 
188
263
  # Assert workflow completed successfully
189
- workflow = ChronoForge::Workflow.last
264
+ workflow = ChronoForge::Workflow.find_by(key: "order-test-123")
190
265
  assert workflow.completed?
191
266
 
192
267
  # Check workflow context
@@ -196,22 +271,24 @@ class WorkflowTest < ActiveJob::TestCase
196
271
  end
197
272
  ```
198
273
 
199
- ChaoticJob provides several helpful methods for testing workflows:
274
+ ## 🗄️ Database Schema
200
275
 
201
- - `perform_all_jobs`: Processes all enqueued jobs, including those enqueued during job execution
202
-
203
- For testing with specific job processing libraries like Sidekiq or Delayed Job, you can still use their respective testing modes, but ChaoticJob is recommended for testing ChronoForge workflows as it better handles the complexities of nested job scheduling and wait states.
276
+ ChronoForge creates three main tables:
204
277
 
278
+ 1. **chrono_forge_workflows**: Stores workflow state and context
279
+ 2. **chrono_forge_execution_logs**: Tracks individual execution steps
280
+ 3. **chrono_forge_error_logs**: Records detailed error information
205
281
 
206
- ## Database Schema
282
+ ## 🔍 When to Use ChronoForge
207
283
 
208
- ChronoForge creates three main tables:
284
+ ChronoForge is ideal for:
209
285
 
210
- 1. `chrono_forge_workflows`: Stores workflow state and context
211
- 2. `chrono_forge_execution_logs`: Tracks individual execution steps
212
- 3. `chrono_forge_error_logs`: Records detailed error information
286
+ - **Long-running business processes** - Order processing, account registration flows
287
+ - **Processes requiring durability** - Financial transactions, data migrations
288
+ - **Multi-step workflows** - Onboarding flows, approval processes, multi-stage jobs
289
+ - **State machines with time-based transitions** - Document approval, subscription lifecycle
213
290
 
214
- ## Development
291
+ ## 🚀 Development
215
292
 
216
293
  After checking out the repo, run:
217
294
 
@@ -227,7 +304,7 @@ The test suite uses SQLite by default and includes:
227
304
  - Integration tests with ActiveJob
228
305
  - Example workflow implementations
229
306
 
230
- ## Contributing
307
+ ## 👥 Contributing
231
308
 
232
309
  1. Fork the repository
233
310
  2. Create your feature branch (`git checkout -b feature/my-new-feature`)
@@ -237,6 +314,6 @@ The test suite uses SQLite by default and includes:
237
314
 
238
315
  Please include tests for any new features or bug fixes.
239
316
 
240
- ## License
317
+ ## 📜 License
241
318
 
242
319
  This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/export.json CHANGED
@@ -5,7 +5,7 @@
5
5
  },
6
6
  {
7
7
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/README.md",
8
- "contents": "# ChronoForge\n\nChronoForge is a Ruby gem that provides a robust framework for building durable, distributed workflows in Ruby on Rails applications. It offers a reliable way to handle long-running processes, state management, and error recovery.\n\n## Features\n\n- **Durable Execution**: Automatically tracks and recovers from failures during workflow execution\n- **State Management**: Built-in workflow state tracking with support for custom contexts\n- **Concurrency Control**: Advanced locking mechanisms to prevent concurrent execution of the same workflow\n- **Error Handling**: Comprehensive error tracking and retry strategies\n- **Execution Logging**: Detailed logging of workflow execution steps and errors\n- **Wait States**: Support for time-based waits and condition-based waiting\n- **Database-Backed**: All workflow state is persisted to the database for durability\n- **ActiveJob Integration**: Seamlessly works with any ActiveJob backend\n\n## Installation\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'chrono_forge'\n```\n\nThen execute:\n\n```bash\n$ bundle install\n```\n\nOr install it directly:\n\n```bash\n$ gem install chrono_forge\n```\n\nAfter installation, run the generator to create the necessary database migrations:\n\n```bash\n$ rails generate chrono_forge:install\n$ rails db:migrate\n```\n\n## Usage\n\n### Basic Workflow Example\n\nHere's a complete example of a durable order processing workflow:\n\n```ruby\nclass OrderProcessingWorkflow < ApplicationJob\n include ChronoForge::Executor\n\n def perform\n # Context can be used to pass and store data between executions\n context[\"order_id\"] = SecureRandom.hex\n\n # Wait until payment is confirmed\n wait_until :payment_confirmed?\n\n # Wait for potential fraud check\n wait 1.minute, :fraud_check_delay\n\n # Durably execute order processing\n durably_execute :process_order\n\n # Final steps\n complete_order\n end\n\n private\n\n def payment_confirmed?\n PaymentService.confirmed?(context[\"order_id\"])\n end\n\n def process_order\n context[\"processed_at\"] = Time.current.iso8601\n OrderProcessor.process(context[\"order_id\"])\n end\n\n def complete_order\n context[\"completed_at\"] = Time.current.iso8601\n OrderCompletionService.complete(context[\"order_id\"])\n end\nend\n```\n\n### Workflow Features\n\n#### Durable Execution\n\nThe `durably_execute` method ensures operations are executed exactly once:\n\n```ruby\n# Execute a method\ndurably_execute(:process_payment, max_attempts: 3)\n\n# Or with a block\ndurably_execute -> (ctx) {\n Payment.process(ctx[:payment_id])\n}\n```\n\n#### Wait States\n\nChronoForge supports both time-based and condition-based waits:\n\n```ruby\n# Wait for a specific duration\nwait 1.hour, :cooling_period\n\n# Wait until a condition is met\nwait_until :payment_processed, \n timeout: 1.hour,\n check_interval: 5.minutes\n```\n\n#### Workflow Context\n\nChronoForge provides a persistent context that survives job restarts:\n\n```ruby\n# Set context values\ncontext[:user_name] = \"John Doe\"\ncontext[:status] = \"processing\"\n\n# Read context values\nuser_name = context[:user_name]\n```\n\n### Error Handling\n\nChronoForge automatically tracks errors and provides retry capabilities:\n\n```ruby\nclass MyWorkflow < ApplicationJob\n include ChronoForge::Executor\n\n private\n\n def should_retry?(error, attempt_count)\n case error\n when NetworkError\n attempt_count < 5\n when ValidationError\n false # Don't retry validation errors\n else\n attempt_count < 3\n end\n end\nend\n```\n\n## Testing\n\nChronoForge is designed to be easily testable using [ChaoticJob](https://github.com/fractaledmind/chaotic_job), a testing framework that makes it simple to test complex job workflows. Here's how to set up your test environment:\n\n1. Add ChaoticJob to your Gemfile's test group:\n\n```ruby\ngroup :test do\n gem 'chaotic_job'\nend\n```\n\n2. Set up your test helper:\n\n```ruby\n# test_helper.rb\nrequire 'chrono_forge'\nrequire 'minitest/autorun'\nrequire 'chaotic_job'\n```\n\nExample test:\n\n```ruby\nclass WorkflowTest < ActiveJob::TestCase\n include ChaoticJob::Helpers\n\n def test_workflow_completion\n # Enqueue the job\n OrderProcessingWorkflow.perform_later(\"order_123\")\n \n # Perform all enqueued jobs\n perform_all_jobs\n \n # Assert workflow completed successfully\n workflow = ChronoForge::Workflow.last\n assert workflow.completed?\n \n # Check workflow context\n assert workflow.context[\"processed_at\"].present?\n assert workflow.context[\"completed_at\"].present?\n end\nend\n```\n\nChaoticJob provides several helpful methods for testing workflows:\n\n- `perform_all_jobs`: Processes all enqueued jobs, including those enqueued during job execution\n\nFor testing with specific job processing libraries like Sidekiq or Delayed Job, you can still use their respective testing modes, but ChaoticJob is recommended for testing ChronoForge workflows as it better handles the complexities of nested job scheduling and wait states.\n\n\n## Database Schema\n\nChronoForge creates three main tables:\n\n1. `chrono_forge_workflows`: Stores workflow state and context\n2. `chrono_forge_execution_logs`: Tracks individual execution steps\n3. `chrono_forge_error_logs`: Records detailed error information\n\n## Development\n\nAfter checking out the repo, run:\n\n```bash\n$ bin/setup # Install dependencies\n$ bundle exec rake test # Run the tests\n$ bin/appraise # Run the full suite of appraisals\n$ bin/console # Start an interactive console\n```\n\nThe test suite uses SQLite by default and includes:\n- Unit tests for core functionality\n- Integration tests with ActiveJob\n- Example workflow implementations\n\n## Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin feature/my-new-feature`)\n5. Create a new Pull Request\n\nPlease include tests for any new features or bug fixes.\n\n## License\n\nThis gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n"
8
+ "contents": "# ChronoForge\n\n![Version](https://img.shields.io/badge/version-0.3.0-blue.svg)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\n> A robust framework for building durable, distributed workflows in Ruby on Rails applications\n\nChronoForge provides a powerful solution for handling long-running processes, managing state, and recovering from failures in your Rails applications. Built on top of ActiveJob, it ensures your critical business processes remain resilient and traceable.\n\n## 🌟 Features\n\n- **Durable Execution**: Automatically tracks and recovers from failures during workflow execution\n- **State Management**: Built-in workflow state tracking with persistent context storage\n- **Concurrency Control**: Advanced locking mechanisms to prevent parallel execution of the same workflow\n- **Error Handling**: Comprehensive error tracking with configurable retry strategies\n- **Execution Logging**: Detailed logging of workflow steps and errors for visibility\n- **Wait States**: Support for time-based waits and condition-based waiting\n- **Database-Backed**: All workflow state is persisted to ensure durability\n- **ActiveJob Integration**: Compatible with all ActiveJob backends, though database-backed processors (like Solid Queue) provide the most reliable experience for long-running workflows\n\n## 📦 Installation\n\nAdd to your application's Gemfile:\n\n```ruby\ngem 'chrono_forge'\n```\n\nThen execute:\n\n```bash\n$ bundle install\n```\n\nOr install directly:\n\n```bash\n$ gem install chrono_forge\n```\n\nAfter installation, run the generator to create the necessary database migrations:\n\n```bash\n$ rails generate chrono_forge:install\n$ rails db:migrate\n```\n\n## 📋 Usage\n\n### Creating and Executing Workflows\n\nChronoForge workflows are ActiveJob classes that prepend the `ChronoForge::Executor` module. Each workflow can **only** accept keyword arguments:\n\n```ruby\n# Define your workflow class\nclass OrderProcessingWorkflow < ApplicationJob\n prepend ChronoForge::Executor\n \n def perform(order_id:, customer_id:)\n # Workflow steps...\n end\nend\n```\n\nAll workflows require a unique identifier when executed. This identifier is used to track and manage the workflow:\n\n```ruby\n# Execute the workflow\nOrderProcessingWorkflow.perform_later(\n \"order-123\", # Unique workflow key\n order_id: \"order-134\", # Custom kwargs\n customer_id: \"customer-456\" # More custom kwargs\n)\n```\n\n### Basic Workflow Example\n\nHere's a complete example of a durable order processing workflow:\n\n```ruby\nclass OrderProcessingWorkflow < ApplicationJob\n prepend ChronoForge::Executor\n\n def perform(order_id:)\n @order_id = order_id\n\n # Context can be used to pass and store data between executions\n context.set_once \"execution_id\", SecureRandom.hex\n\n # Wait until payment is confirmed\n wait_until :payment_confirmed?\n\n # Wait for potential fraud check\n wait 1.minute, :fraud_check_delay\n\n # Durably execute order processing\n durably_execute :process_order\n\n # Final steps\n complete_order\n end\n\n private\n\n def payment_confirmed?\n PaymentService.confirmed?(@order_id, context[\"execution_id\"])\n end\n\n def process_order\n OrderProcessor.process(@order_id, context[\"execution_id\"])\n context[\"processed_at\"] = Time.current.iso8601\n end\n\n def complete_order\n OrderCompletionService.complete(@order_id, context[\"execution_id\"])\n context[\"completed_at\"] = Time.current.iso8601\n end\nend\n```\n\n### Core Workflow Features\n\n#### 🚀 Executing Workflows\n\nChronoForge workflows are executed through ActiveJob's standard interface with a specific parameter structure:\n\n```ruby\n# Perform the workflow immediately\nOrderProcessingWorkflow.perform_now(\n \"order-123\", # Unique workflow key\n order_id: \"O-123\", # Custom parameter\n customer_id: \"C-456\" # Another custom parameter\n)\n\n# Or queue it for background processing\nOrderProcessingWorkflow.perform_later(\n \"order-123-async\", # Unique workflow key\n order_id: \"O-124\",\n customer_id: \"C-457\"\n)\n```\n\n**Important:** Workflows must use keyword arguments only, not positional arguments.\n\n#### Durable Execution\n\nThe `durably_execute` method ensures operations are executed exactly once, even if the job fails and is retried:\n\n```ruby\n# Execute a method\ndurably_execute(:process_payment, max_attempts: 3)\n\n# Or with a block\ndurably_execute -> (ctx) {\n Payment.process(ctx[:payment_id])\n}\n```\n\n#### ⏱️ Wait States\n\nChronoForge supports both time-based and condition-based waits:\n\n```ruby\n# Wait for a specific duration\nwait 1.hour, :cooling_period\n\n# Wait until a condition is met\nwait_until :payment_processed, \n timeout: 1.hour,\n check_interval: 5.minutes\n```\n\n#### 🔄 Workflow Context\n\nChronoForge provides a persistent context that survives job restarts. The context behaves like a Hash but with additional capabilities:\n\n```ruby\n# Set context values\ncontext[:user_name] = \"John Doe\"\ncontext[:status] = \"processing\"\n\n# Read context values\nuser_name = context[:user_name]\n\n# Using the fetch method (returns default if key doesn't exist)\nstatus = context.fetch(:status, \"pending\")\n\n# Set a value with the set method (alias for []=)\ncontext.set(:total_amount, 99.99)\n\n# Set a value only if the key doesn't already exist\ncontext.set_once(:created_at, Time.current.iso8601)\n\n# Check if a key exists\nif context.key?(:user_id)\n # Do something with the user ID\nend\n```\n\nThe context supports serializable Ruby objects (Hash, Array, String, Integer, Float, Boolean, and nil) and validates types automatically.\n\n### 🛡️ Error Handling\n\nChronoForge automatically tracks errors and provides configurable retry capabilities:\n\n```ruby\nclass MyWorkflow < ApplicationJob\n prepend ChronoForge::Executor\n\n private\n\n def should_retry?(error, attempt_count)\n case error\n when NetworkError\n attempt_count < 5 # Retry network errors up to 5 times\n when ValidationError\n false # Don't retry validation errors\n else\n attempt_count < 3 # Default retry policy\n end\n end\nend\n```\n\n## 🧪 Testing\n\nChronoForge is designed to be easily testable using [ChaoticJob](https://github.com/fractaledmind/chaotic_job), a testing framework that makes it simple to test complex job workflows:\n\n1. Add ChaoticJob to your Gemfile's test group:\n\n```ruby\ngroup :test do\n gem 'chaotic_job'\nend\n```\n\n2. Set up your test helper:\n\n```ruby\n# test_helper.rb\nrequire 'chrono_forge'\nrequire 'minitest/autorun'\nrequire 'chaotic_job'\n```\n\nExample test:\n\n```ruby\nclass WorkflowTest < ActiveJob::TestCase\n include ChaoticJob::Helpers\n\n def test_workflow_completion\n # Enqueue the job with a unique key and custom parameters\n OrderProcessingWorkflow.perform_later(\n \"order-test-123\",\n order_id: \"O-123\",\n customer_id: \"C-456\"\n )\n \n # Perform all enqueued jobs\n perform_all_jobs\n \n # Assert workflow completed successfully\n workflow = ChronoForge::Workflow.find_by(key: \"order-test-123\")\n assert workflow.completed?\n \n # Check workflow context\n assert workflow.context[\"processed_at\"].present?\n assert workflow.context[\"completed_at\"].present?\n end\nend\n```\n\n## 🗄️ Database Schema\n\nChronoForge creates three main tables:\n\n1. **chrono_forge_workflows**: Stores workflow state and context\n2. **chrono_forge_execution_logs**: Tracks individual execution steps\n3. **chrono_forge_error_logs**: Records detailed error information\n\n## 🔍 When to Use ChronoForge\n\nChronoForge is ideal for:\n\n- **Long-running business processes** - Order processing, account registration flows\n- **Processes requiring durability** - Financial transactions, data migrations\n- **Multi-step workflows** - Onboarding flows, approval processes, multi-stage jobs\n- **State machines with time-based transitions** - Document approval, subscription lifecycle\n\n## 🚀 Development\n\nAfter checking out the repo, run:\n\n```bash\n$ bin/setup # Install dependencies\n$ bundle exec rake test # Run the tests\n$ bin/appraise # Run the full suite of appraisals\n$ bin/console # Start an interactive console\n```\n\nThe test suite uses SQLite by default and includes:\n- Unit tests for core functionality\n- Integration tests with ActiveJob\n- Example workflow implementations\n\n## 👥 Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin feature/my-new-feature`)\n5. Create a new Pull Request\n\nPlease include tests for any new features or bug fixes.\n\n## 📜 License\n\nThis gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).\n"
9
9
  },
10
10
  {
11
11
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/chrono_forge.gemspec",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  {
15
15
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/export.rb",
16
- "contents": "require \"json\"\nrequire \"find\"\n\ndef export_files_to_json(directory, extensions, output_file, exceptions = [])\n # Convert extensions to lowercase for case-insensitive matching\n extensions = extensions.map(&:downcase)\n\n # Array to store file data\n files_data = []\n\n # Find all files in directory and subdirectories\n Find.find(directory) do |path|\n # Skip if not a file\n next unless File.file?(path)\n next if exceptions.any? { |exception| path.include?(exception) }\n\n # Check if file extension matches any in our list\n ext = File.extname(path).downcase[1..-1] # Remove the leading dot\n next unless extensions.include?(ext)\n\n puts path\n\n begin\n # Read file contents\n contents = File.read(path)\n\n # Add to our array\n files_data << {\n \"path\" => path,\n \"contents\" => contents\n }\n rescue => e\n puts \"Error reading file #{path}: #{e.message}\"\n end\n end\n\n # Write to JSON file\n File.write(output_file, JSON.pretty_generate(files_data))\n\n puts \"Successfully exported #{files_data.length} files to #{output_file}\"\nend\n\n# Example usage (uncomment and modify as needed):\ndirectory = \"/Users/stefan/Documents/plutonium/chrono_forge\"\nexceptions = [\"/.github/\", \"/.vscode/\", \"gemfiles\", \"pkg\", \"node_modules\"]\nextensions = [\"rb\", \"md\", \"yml\", \"yaml\", \"gemspec\"]\noutput_file = \"export.json\"\nexport_files_to_json(directory, extensions, output_file, exceptions)\n"
16
+ "contents": "require \"json\"\nrequire \"find\"\n\ndef export_files_to_json(directory, extensions, output_file, exceptions = [])\n # Convert extensions to lowercase for case-insensitive matching\n extensions = extensions.map(&:downcase)\n\n # Array to store file data\n files_data = []\n\n # Find all files in directory and subdirectories\n Find.find(directory) do |path|\n # Skip if not a file\n next unless File.file?(path)\n next if exceptions.any? { |exception| path.include?(exception) }\n\n # Check if file extension matches any in our list\n ext = File.extname(path).downcase[1..] # Remove the leading dot\n next unless extensions.include?(ext)\n\n puts path\n\n begin\n # Read file contents\n contents = File.read(path)\n\n # Add to our array\n files_data << {\n \"path\" => path,\n \"contents\" => contents\n }\n rescue => e\n puts \"Error reading file #{path}: #{e.message}\"\n end\n end\n\n # Write to JSON file\n File.write(output_file, JSON.pretty_generate(files_data))\n\n puts \"Successfully exported #{files_data.length} files to #{output_file}\"\nend\n\n# Example usage (uncomment and modify as needed):\ndirectory = \"/Users/stefan/Documents/plutonium/chrono_forge\"\nexceptions = [\"/.github/\", \"/.vscode/\", \"gemfiles\", \"pkg\", \"node_modules\"]\nextensions = [\"rb\", \"md\", \"yml\", \"yaml\", \"gemspec\"]\noutput_file = \"export.json\"\nexport_files_to_json(directory, extensions, output_file, exceptions)\n"
17
17
  },
18
18
  {
19
19
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/error_log.rb",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  {
27
27
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/context.rb",
28
- "contents": "module ChronoForge\n module Executor\n class Context\n class ValidationError < Error; end\n\n ALLOWED_TYPES = [\n String,\n Integer,\n Float,\n TrueClass,\n FalseClass,\n NilClass,\n Hash,\n Array\n ]\n\n def initialize(workflow)\n @workflow = workflow\n @context = workflow.context || {}\n @dirty = false\n end\n\n def []=(key, value)\n # Type and size validation\n validate_value!(value)\n\n @context[key.to_s] =\n if value.is_a?(Hash) || value.is_a?(Array)\n deep_dup(value)\n else\n value\n end\n\n @dirty = true\n end\n\n def [](key)\n @context[key.to_s]\n end\n\n def save!\n return unless @dirty\n\n @workflow.update_column(:context, @context)\n @dirty = false\n end\n\n private\n\n def validate_value!(value)\n unless ALLOWED_TYPES.any? { |type| value.is_a?(type) }\n raise ValidationError, \"Unsupported context value type: #{value.inspect}\"\n end\n\n # Optional: Add size constraints\n if value.is_a?(String) && value.size > 64.kilobytes\n raise ValidationError, \"Context value too large\"\n end\n end\n\n def deep_dup(obj)\n JSON.parse(JSON.generate(obj))\n rescue\n obj.dup\n end\n end\n end\nend\n"
28
+ "contents": "module ChronoForge\n module Executor\n class Context\n class ValidationError < Error; end\n\n ALLOWED_TYPES = [\n String,\n Integer,\n Float,\n TrueClass,\n FalseClass,\n NilClass,\n Hash,\n Array\n ]\n\n def initialize(workflow)\n @workflow = workflow\n @context = workflow.context || {}\n @dirty = false\n end\n\n def []=(key, value)\n set_value(key, value)\n end\n\n def [](key)\n get_value(key)\n end\n\n # Fetches a value from the context\n # Returns the value if the key exists, otherwise returns the default value\n def fetch(key, default = nil)\n key?(key) ? get_value(key) : default\n end\n\n # Sets a value in the context\n # Alias for the []= method\n def set(key, value)\n set_value(key, value)\n end\n\n # Sets a value in the context only if the key doesn't already exist\n # Returns true if the value was set, false otherwise\n def set_once(key, value)\n return false if key?(key)\n\n set_value(key, value)\n true\n end\n\n def key?(key)\n @context.key?(key.to_s)\n end\n\n def save!\n return unless @dirty\n\n @workflow.update_column(:context, @context)\n @dirty = false\n end\n\n private\n\n def set_value(key, value)\n validate_value!(value)\n\n @context[key.to_s] =\n if value.is_a?(Hash) || value.is_a?(Array)\n deep_dup(value)\n else\n value\n end\n\n @dirty = true\n end\n\n def get_value(key)\n @context[key.to_s]\n end\n\n def validate_value!(value)\n unless ALLOWED_TYPES.any? { |type| value.is_a?(type) }\n raise ValidationError, \"Unsupported context value type: #{value.inspect}\"\n end\n\n # Optional: Add size constraints\n if value.is_a?(String) && value.size > 64.kilobytes\n raise ValidationError, \"Context value too large\"\n end\n end\n\n def deep_dup(obj)\n JSON.parse(JSON.generate(obj))\n rescue\n obj.dup\n end\n end\n end\nend\n"
29
29
  },
30
30
  {
31
31
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/execution_tracker.rb",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  {
39
39
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/durably_execute.rb",
40
- "contents": "module ChronoForge\n module Executor\n module Methods\n module DurablyExecute\n def durably_execute(method, **options)\n # Create execution log\n step_name = \"durably_execute$#{method}\"\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: @workflow,\n step_name: step_name\n ) do |log|\n log.started_at = Time.current\n end\n\n # Return if already completed\n return if execution_log.completed?\n\n # Execute with error handling\n begin\n # Update execution log with attempt\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n # Execute the method\n if method.is_a?(Symbol)\n send(method)\n else\n method.call(@context)\n end\n\n # Complete the execution\n execution_log.update!(\n state: :completed,\n completed_at: Time.current\n )\n result\n rescue HaltExecutionFlow\n raise\n rescue => e\n # Log the error\n Rails.logger.error { \"Error while durably executing #{method}: #{e.message}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n\n # Optional retry logic\n if execution_log.attempts < (options[:max_attempts] || 3)\n # Reschedule with exponential backoff\n backoff = (2**[execution_log.attempts || 1, 5].min).seconds\n\n self.class\n .set(wait: backoff)\n .perform_later(\n @workflow.key,\n retry_method: method\n )\n\n # Halt current execution\n halt_execution!\n else\n # Max attempts reached\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise ExecutionFailedError, \"#{step_name} failed after maximum attempts\"\n end\n end\n end\n end\n end\n end\nend\n"
40
+ "contents": "module ChronoForge\n module Executor\n module Methods\n module DurablyExecute\n def durably_execute(method, **options)\n # Create execution log\n step_name = \"durably_execute$#{method}\"\n execution_log = ExecutionLog.create_or_find_by!(\n workflow: @workflow,\n step_name: step_name\n ) do |log|\n log.started_at = Time.current\n end\n\n # Return if already completed\n return if execution_log.completed?\n\n # Execute with error handling\n begin\n # Update execution log with attempt\n execution_log.update!(\n attempts: execution_log.attempts + 1,\n last_executed_at: Time.current\n )\n\n # Execute the method\n if method.is_a?(Symbol)\n send(method)\n else\n method.call(@context)\n end\n\n # Complete the execution\n execution_log.update!(\n state: :completed,\n completed_at: Time.current\n )\n \n # return nil\n nil\n rescue HaltExecutionFlow\n raise\n rescue => e\n # Log the error\n Rails.logger.error { \"Error while durably executing #{method}: #{e.message}\" }\n self.class::ExecutionTracker.track_error(workflow, e)\n\n # Optional retry logic\n if execution_log.attempts < (options[:max_attempts] || 3)\n # Reschedule with exponential backoff\n backoff = (2**[execution_log.attempts || 1, 5].min).seconds\n\n self.class\n .set(wait: backoff)\n .perform_later(\n @workflow.key,\n retry_method: method\n )\n\n # Halt current execution\n halt_execution!\n else\n # Max attempts reached\n execution_log.update!(\n state: :failed,\n error_message: e.message,\n error_class: e.class.name\n )\n raise ExecutionFailedError, \"#{step_name} failed after maximum attempts\"\n end\n end\n end\n end\n end\n end\nend\n"
41
41
  },
42
42
  {
43
43
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/executor/methods/wait.rb",
@@ -61,11 +61,11 @@
61
61
  },
62
62
  {
63
63
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/version.rb",
64
- "contents": "# frozen_string_literal: true\n\nmodule ChronoForge\n VERSION = \"0.1.1\"\nend\n"
64
+ "contents": "# frozen_string_literal: true\n\nmodule ChronoForge\n VERSION = \"0.3.1\"\nend\n"
65
65
  },
66
66
  {
67
67
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/workflow.rb",
68
- "contents": "# frozen_string_literal: true\n\n# == Schema Information\n#\n# Table name: chrono_forge_workflows\n#\n# id :integer not null, primary key\n# completed_at :datetime\n# context :json not null\n# job_class :string not null\n# key :string not null\n# kwargs :json not null\n# options :json not null\n# locked_at :datetime\n# started_at :datetime\n# state :integer default(\"idle\"), not null\n# created_at :datetime not null\n# updated_at :datetime not null\n#\n# Indexes\n#\n# index_chrono_forge_workflows_on_key (key) UNIQUE\n#\nmodule ChronoForge\n class Workflow < ActiveRecord::Base\n self.table_name = \"chrono_forge_workflows\"\n\n has_many :execution_logs, -> { order(id: :asc) }\n has_many :error_logs, -> { order(id: :asc) }\n\n enum :state, %i[\n idle\n running\n completed\n failed\n stalled\n ]\n\n # Serialization for metadata\n serialize :metadata, coder: JSON\n\n def executable?\n idle? || running?\n end\n\n def job_klass\n job_class.constantize\n end\n end\nend\n"
68
+ "contents": "# frozen_string_literal: true\n\n# == Schema Information\n#\n# Table name: chrono_forge_workflows\n#\n# id :integer not null, primary key\n# completed_at :datetime\n# context :json not null\n# job_class :string not null\n# key :string not null\n# kwargs :json not null\n# options :json not null\n# locked_at :datetime\n# started_at :datetime\n# state :integer default(\"idle\"), not null\n# created_at :datetime not null\n# updated_at :datetime not null\n#\n# Indexes\n#\n# index_chrono_forge_workflows_on_key (key) UNIQUE\n#\nmodule ChronoForge\n class Workflow < ActiveRecord::Base\n self.table_name = \"chrono_forge_workflows\"\n\n has_many :execution_logs, -> { order(id: :asc) }, dependent: :destroy\n has_many :error_logs, -> { order(id: :asc) }, dependent: :destroy\n\n enum :state, %i[\n idle\n running\n completed\n failed\n stalled\n ]\n\n # Serialization for metadata\n serialize :metadata, coder: JSON\n\n def executable?\n idle? || running?\n end\n\n def job_klass\n job_class.constantize\n end\n end\nend\n"
69
69
  },
70
70
  {
71
71
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge.rb",
data/export.rb CHANGED
@@ -15,7 +15,7 @@ def export_files_to_json(directory, extensions, output_file, exceptions = [])
15
15
  next if exceptions.any? { |exception| path.include?(exception) }
16
16
 
17
17
  # Check if file extension matches any in our list
18
- ext = File.extname(path).downcase[1..-1] # Remove the leading dot
18
+ ext = File.extname(path).downcase[1..] # Remove the leading dot
19
19
  next unless extensions.include?(ext)
20
20
 
21
21
  puts path
@@ -21,21 +21,36 @@ module ChronoForge
21
21
  end
22
22
 
23
23
  def []=(key, value)
24
- # Type and size validation
25
- validate_value!(value)
24
+ set_value(key, value)
25
+ end
26
26
 
27
- @context[key.to_s] =
28
- if value.is_a?(Hash) || value.is_a?(Array)
29
- deep_dup(value)
30
- else
31
- value
32
- end
27
+ def [](key)
28
+ get_value(key)
29
+ end
33
30
 
34
- @dirty = true
31
+ # Fetches a value from the context
32
+ # Returns the value if the key exists, otherwise returns the default value
33
+ def fetch(key, default = nil)
34
+ key?(key) ? get_value(key) : default
35
35
  end
36
36
 
37
- def [](key)
38
- @context[key.to_s]
37
+ # Sets a value in the context
38
+ # Alias for the []= method
39
+ def set(key, value)
40
+ set_value(key, value)
41
+ end
42
+
43
+ # Sets a value in the context only if the key doesn't already exist
44
+ # Returns true if the value was set, false otherwise
45
+ def set_once(key, value)
46
+ return false if key?(key)
47
+
48
+ set_value(key, value)
49
+ true
50
+ end
51
+
52
+ def key?(key)
53
+ @context.key?(key.to_s)
39
54
  end
40
55
 
41
56
  def save!
@@ -47,6 +62,23 @@ module ChronoForge
47
62
 
48
63
  private
49
64
 
65
+ def set_value(key, value)
66
+ validate_value!(value)
67
+
68
+ @context[key.to_s] =
69
+ if value.is_a?(Hash) || value.is_a?(Array)
70
+ deep_dup(value)
71
+ else
72
+ value
73
+ end
74
+
75
+ @dirty = true
76
+ end
77
+
78
+ def get_value(key)
79
+ @context[key.to_s]
80
+ end
81
+
50
82
  def validate_value!(value)
51
83
  unless ALLOWED_TYPES.any? { |type| value.is_a?(type) }
52
84
  raise ValidationError, "Unsupported context value type: #{value.inspect}"
@@ -35,7 +35,9 @@ module ChronoForge
35
35
  state: :completed,
36
36
  completed_at: Time.current
37
37
  )
38
- result
38
+
39
+ # return nil
40
+ nil
39
41
  rescue HaltExecutionFlow
40
42
  raise
41
43
  rescue => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ChronoForge
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.1"
5
5
  end
@@ -25,8 +25,8 @@ module ChronoForge
25
25
  class Workflow < ActiveRecord::Base
26
26
  self.table_name = "chrono_forge_workflows"
27
27
 
28
- has_many :execution_logs, -> { order(id: :asc) }
29
- has_many :error_logs, -> { order(id: :asc) }
28
+ has_many :execution_logs, -> { order(id: :asc) }, dependent: :destroy
29
+ has_many :error_logs, -> { order(id: :asc) }, dependent: :destroy
30
30
 
31
31
  enum :state, %i[
32
32
  idle
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chrono_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Froelich
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-04-27 00:00:00.000000000 Z
11
+ date: 2025-04-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord