chrono_forge 0.2.0 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f65c15313d0538a84fe916c5d3fd1966e593364d00e777aa2d1693a5bc801102
4
- data.tar.gz: 9a3a1c025dd348d46c9169bb03a3fc26a975b5c0eb5411d032410c9f8a70e564
3
+ metadata.gz: b16c027867c5aca8d3f168ccb252125feef563731af33f40a129bfa9bfb781c5
4
+ data.tar.gz: 0ebd43e1896ea8df4ceb00c9c9af49c57a1e74d309149e817e43f9acfdfd5b8a
5
5
  SHA512:
6
- metadata.gz: 31c16f81bbfefd72f6ad4799a2ab7eeac2ffe11a8a8f2d12606632b36d843a8dc518c27a0e2f5a992184619004c7de5f3e60c4b19f55ab63b38f49e67ee32e79
7
- data.tar.gz: 233e0badc22ad43f4084b2acfc83782d41da6b0949d6b7ee9b050b89ee4a9fbbbac46dd74029470af58dd4293767f1200ee46b3ed19dfa7eaba89c3901e8b3f8
6
+ metadata.gz: c94edc3ed7339bf3c7304c54e547bb1f86bc098dda586ffa049cfaf2fc29efcc7b44cce9429789ee1c3c5559dd3dd75859b75e74a27587c2a804160fee600f42
7
+ data.tar.gz: f7350954cace2c26ca57c7c0253a81d098a2eb655a5f283e6627eedec9d2b4acc11e0f58d3bebe47cd8a4c82907e1c4410d3655c43695ec8f02b8cc0af6a52c2
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,7 @@ $ rails generate chrono_forge:install
40
45
  $ rails db:migrate
41
46
  ```
42
47
 
43
- ## Usage
48
+ ## 📋 Usage
44
49
 
45
50
  ### Basic Workflow Example
46
51
 
@@ -52,7 +57,7 @@ class OrderProcessingWorkflow < ApplicationJob
52
57
 
53
58
  def perform
54
59
  # Context can be used to pass and store data between executions
55
- context["order_id"] = SecureRandom.hex
60
+ context.set_once "order_id", SecureRandom.hex
56
61
 
57
62
  # Wait until payment is confirmed
58
63
  wait_until :payment_confirmed?
@@ -74,22 +79,22 @@ class OrderProcessingWorkflow < ApplicationJob
74
79
  end
75
80
 
76
81
  def process_order
77
- context["processed_at"] = Time.current.iso8601
78
82
  OrderProcessor.process(context["order_id"])
83
+ context["processed_at"] = Time.current.iso8601
79
84
  end
80
85
 
81
86
  def complete_order
82
- context["completed_at"] = Time.current.iso8601
83
87
  OrderCompletionService.complete(context["order_id"])
88
+ context["completed_at"] = Time.current.iso8601
84
89
  end
85
90
  end
86
91
  ```
87
92
 
88
- ### Workflow Features
93
+ ### Core Workflow Features
89
94
 
90
- #### Durable Execution
95
+ #### Durable Execution
91
96
 
92
- The `durably_execute` method ensures operations are executed exactly once:
97
+ The `durably_execute` method ensures operations are executed exactly once, even if the job fails and is retried:
93
98
 
94
99
  ```ruby
95
100
  # Execute a method
@@ -101,7 +106,7 @@ durably_execute -> (ctx) {
101
106
  }
102
107
  ```
103
108
 
104
- #### Wait States
109
+ #### ⏱️ Wait States
105
110
 
106
111
  ChronoForge supports both time-based and condition-based waits:
107
112
 
@@ -115,9 +120,9 @@ wait_until :payment_processed,
115
120
  check_interval: 5.minutes
116
121
  ```
117
122
 
118
- #### Workflow Context
123
+ #### 🔄 Workflow Context
119
124
 
120
- ChronoForge provides a persistent context that survives job restarts:
125
+ ChronoForge provides a persistent context that survives job restarts. The context behaves like a Hash but with additional capabilities:
121
126
 
122
127
  ```ruby
123
128
  # Set context values
@@ -126,11 +131,25 @@ context[:status] = "processing"
126
131
 
127
132
  # Read context values
128
133
  user_name = context[:user_name]
134
+
135
+ # Using the fetch method (returns default if key doesn't exist)
136
+ status = context.fetch(:status, "pending")
137
+
138
+ # Set a value with the set method (alias for []=)
139
+ context.set(:total_amount, 99.99)
140
+
141
+ # Set a value only if the key doesn't already exist
142
+ context.set_once(:created_at, Time.current.iso8601)
143
+
144
+ # Check if a key exists
145
+ if context.key?(:user_id)
146
+ # Do something with the user ID
147
+ end
129
148
  ```
130
149
 
131
- ### Error Handling
150
+ ### 🛡️ Error Handling
132
151
 
133
- ChronoForge automatically tracks errors and provides retry capabilities:
152
+ ChronoForge automatically tracks errors and provides configurable retry capabilities:
134
153
 
135
154
  ```ruby
136
155
  class MyWorkflow < ApplicationJob
@@ -141,19 +160,19 @@ class MyWorkflow < ApplicationJob
141
160
  def should_retry?(error, attempt_count)
142
161
  case error
143
162
  when NetworkError
144
- attempt_count < 5
163
+ attempt_count < 5 # Retry network errors up to 5 times
145
164
  when ValidationError
146
165
  false # Don't retry validation errors
147
166
  else
148
- attempt_count < 3
167
+ attempt_count < 3 # Default retry policy
149
168
  end
150
169
  end
151
170
  end
152
171
  ```
153
172
 
154
- ## Testing
173
+ ## 🧪 Testing
155
174
 
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:
175
+ 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
176
 
158
177
  1. Add ChaoticJob to your Gemfile's test group:
159
178
 
@@ -196,22 +215,24 @@ class WorkflowTest < ActiveJob::TestCase
196
215
  end
197
216
  ```
198
217
 
199
- ChaoticJob provides several helpful methods for testing workflows:
218
+ ## 🗄️ Database Schema
200
219
 
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.
220
+ ChronoForge creates three main tables:
204
221
 
222
+ 1. **chrono_forge_workflows**: Stores workflow state and context
223
+ 2. **chrono_forge_execution_logs**: Tracks individual execution steps
224
+ 3. **chrono_forge_error_logs**: Records detailed error information
205
225
 
206
- ## Database Schema
226
+ ## 🔍 When to Use ChronoForge
207
227
 
208
- ChronoForge creates three main tables:
228
+ ChronoForge is ideal for:
209
229
 
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
230
+ - **Long-running business processes** - Order processing, account registration flows
231
+ - **Processes requiring durability** - Financial transactions, data migrations
232
+ - **Multi-step workflows** - Onboarding flows, approval processes, multi-stage jobs
233
+ - **State machines with time-based transitions** - Document approval, subscription lifecycle
213
234
 
214
- ## Development
235
+ ## 🚀 Development
215
236
 
216
237
  After checking out the repo, run:
217
238
 
@@ -227,7 +248,7 @@ The test suite uses SQLite by default and includes:
227
248
  - Integration tests with ActiveJob
228
249
  - Example workflow implementations
229
250
 
230
- ## Contributing
251
+ ## 👥 Contributing
231
252
 
232
253
  1. Fork the repository
233
254
  2. Create your feature branch (`git checkout -b feature/my-new-feature`)
@@ -237,6 +258,6 @@ The test suite uses SQLite by default and includes:
237
258
 
238
259
  Please include tests for any new features or bug fixes.
239
260
 
240
- ## License
261
+ ## 📜 License
241
262
 
242
263
  This gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/export.json CHANGED
@@ -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,7 +61,7 @@
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.2.0\"\nend\n"
65
65
  },
66
66
  {
67
67
  "path": "/Users/stefan/Documents/plutonium/chrono_forge/lib/chrono_forge/workflow.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.0"
5
5
  end
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.0
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