state_machines 0.31.0 → 0.50.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 +4 -4
- data/README.md +202 -0
- data/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
- data/lib/state_machines/async_mode/async_events.rb +282 -0
- data/lib/state_machines/async_mode/async_machine.rb +60 -0
- data/lib/state_machines/async_mode/async_transition_collection.rb +141 -0
- data/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
- data/lib/state_machines/async_mode.rb +64 -0
- data/lib/state_machines/branch.rb +55 -14
- data/lib/state_machines/callback.rb +2 -1
- data/lib/state_machines/event.rb +7 -5
- data/lib/state_machines/event_collection.rb +21 -13
- data/lib/state_machines/machine/async_extensions.rb +88 -0
- data/lib/state_machines/machine/class_methods.rb +4 -0
- data/lib/state_machines/machine/configuration.rb +11 -1
- data/lib/state_machines/machine.rb +1 -0
- data/lib/state_machines/state.rb +6 -5
- data/lib/state_machines/test_helper.rb +328 -0
- data/lib/state_machines/transition.rb +7 -6
- data/lib/state_machines/transition_collection.rb +15 -1
- data/lib/state_machines/version.rb +1 -1
- metadata +8 -1
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
module AsyncMode
|
5
|
+
# Error class for async-specific transition failures
|
6
|
+
class AsyncTransitionError < StateMachines::Error
|
7
|
+
def initialize(object, machines, failed_events)
|
8
|
+
@object = object
|
9
|
+
@machines = machines
|
10
|
+
@failed_events = failed_events
|
11
|
+
super("Failed to perform async transitions: #{failed_events.join(', ')}")
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :object, :machines, :failed_events
|
15
|
+
end
|
16
|
+
|
17
|
+
# Async-aware transition collection that can execute transitions concurrently
|
18
|
+
class AsyncTransitionCollection < TransitionCollection
|
19
|
+
# Performs transitions asynchronously using Async
|
20
|
+
# Provides better concurrency for I/O-bound operations
|
21
|
+
def perform_async(&block)
|
22
|
+
reset
|
23
|
+
|
24
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
25
|
+
return Async do
|
26
|
+
perform_async(&block)
|
27
|
+
end.wait
|
28
|
+
end
|
29
|
+
|
30
|
+
if valid?
|
31
|
+
# Create async tasks for each transition
|
32
|
+
tasks = map do |transition|
|
33
|
+
Async do
|
34
|
+
if use_event_attributes? && !block_given?
|
35
|
+
transition.transient = true
|
36
|
+
transition.machine.write_safely(object, :event_transition, transition)
|
37
|
+
run_actions
|
38
|
+
transition
|
39
|
+
else
|
40
|
+
within_transaction do
|
41
|
+
catch(:halt) { run_callbacks(&block) }
|
42
|
+
rollback unless success?
|
43
|
+
end
|
44
|
+
transition
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Wait for all tasks to complete
|
50
|
+
completed_transitions = []
|
51
|
+
tasks.each do |task|
|
52
|
+
begin
|
53
|
+
result = task.wait
|
54
|
+
completed_transitions << result if result
|
55
|
+
rescue StandardError => e
|
56
|
+
# Handle individual transition failures
|
57
|
+
rollback
|
58
|
+
raise AsyncTransitionError.new(object, map(&:machine), [e.message])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Check if all transitions succeeded
|
63
|
+
@success = completed_transitions.length == length
|
64
|
+
end
|
65
|
+
|
66
|
+
success?
|
67
|
+
end
|
68
|
+
|
69
|
+
# Performs transitions concurrently using threads
|
70
|
+
# Better for CPU-bound operations but requires more careful synchronization
|
71
|
+
def perform_threaded(&block)
|
72
|
+
reset
|
73
|
+
|
74
|
+
if valid?
|
75
|
+
# Use basic thread approach
|
76
|
+
threads = []
|
77
|
+
results = []
|
78
|
+
results_mutex = Concurrent::ReentrantReadWriteLock.new
|
79
|
+
|
80
|
+
each do |transition|
|
81
|
+
threads << Thread.new do
|
82
|
+
begin
|
83
|
+
result = if use_event_attributes? && !block_given?
|
84
|
+
transition.transient = true
|
85
|
+
transition.machine.write_safely(object, :event_transition, transition)
|
86
|
+
run_actions
|
87
|
+
transition
|
88
|
+
else
|
89
|
+
within_transaction do
|
90
|
+
catch(:halt) { run_callbacks(&block) }
|
91
|
+
rollback unless success?
|
92
|
+
end
|
93
|
+
transition
|
94
|
+
end
|
95
|
+
|
96
|
+
results_mutex.with_write_lock { results << result }
|
97
|
+
rescue StandardError => e
|
98
|
+
# Handle individual transition failures
|
99
|
+
rollback
|
100
|
+
raise AsyncTransitionError.new(object, [transition.machine], [e.message])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# Wait for all threads to complete
|
106
|
+
threads.each(&:join)
|
107
|
+
@success = results.length == length
|
108
|
+
end
|
109
|
+
|
110
|
+
success?
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
# Override run_actions to be thread-safe when needed
|
116
|
+
def run_actions(&block)
|
117
|
+
catch_exceptions do
|
118
|
+
@success = if block_given?
|
119
|
+
result = yield
|
120
|
+
actions.each { |action| results[action] = result }
|
121
|
+
!!result
|
122
|
+
else
|
123
|
+
actions.compact.each do |action|
|
124
|
+
next if skip_actions
|
125
|
+
|
126
|
+
# Use thread-safe write for results
|
127
|
+
if object.respond_to?(:state_machine_mutex)
|
128
|
+
object.state_machine_mutex.with_write_lock do
|
129
|
+
results[action] = object.send(action)
|
130
|
+
end
|
131
|
+
else
|
132
|
+
results[action] = object.send(action)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
results.values.all?
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
module AsyncMode
|
5
|
+
# Thread-safe state operations for async-enabled state machines
|
6
|
+
# Uses concurrent-ruby for enterprise-grade thread safety
|
7
|
+
module ThreadSafeState
|
8
|
+
# Gets or creates a reentrant mutex for thread-safe state operations on an object
|
9
|
+
# Each object gets its own mutex to avoid global locking
|
10
|
+
# Uses Concurrent::ReentrantReadWriteLock for better performance
|
11
|
+
def state_machine_mutex
|
12
|
+
@_state_machine_mutex ||= Concurrent::ReentrantReadWriteLock.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Thread-safe version of state reading
|
16
|
+
# Ensures atomic read operations across concurrent threads
|
17
|
+
def read_state_safely(machine, attribute, ivar = false)
|
18
|
+
state_machine_mutex.with_read_lock do
|
19
|
+
machine.read(self, attribute, ivar)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Thread-safe version of state writing
|
24
|
+
# Ensures atomic write operations across concurrent threads
|
25
|
+
def write_state_safely(machine, attribute, value, ivar = false)
|
26
|
+
state_machine_mutex.with_write_lock do
|
27
|
+
machine.write(self, attribute, value, ivar)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Handle marshalling by excluding the mutex (will be recreated when needed)
|
32
|
+
def marshal_dump
|
33
|
+
# Get instance variables excluding the mutex
|
34
|
+
vars = instance_variables.reject { |var| var == :@_state_machine_mutex }
|
35
|
+
vars.map { |var| [var, instance_variable_get(var)] }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Restore marshalled object, mutex will be lazily recreated when needed
|
39
|
+
def marshal_load(data)
|
40
|
+
data.each do |var, value|
|
41
|
+
instance_variable_set(var, value)
|
42
|
+
end
|
43
|
+
# Don't set @_state_machine_mutex - let it be lazily created
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Ruby Engine Compatibility Check
|
4
|
+
# The async gem requires native extensions and Fiber scheduler support
|
5
|
+
# which are not available on JRuby or TruffleRuby
|
6
|
+
if RUBY_ENGINE == 'jruby' || RUBY_ENGINE == 'truffleruby'
|
7
|
+
raise LoadError, <<~ERROR
|
8
|
+
StateMachines::AsyncMode is not available on #{RUBY_ENGINE}.
|
9
|
+
|
10
|
+
The async gem requires native extensions (io-event) and Fiber scheduler support
|
11
|
+
which are not implemented in #{RUBY_ENGINE}. AsyncMode is only supported on:
|
12
|
+
|
13
|
+
• MRI Ruby (CRuby) 3.2+
|
14
|
+
• Other Ruby engines with full Fiber scheduler support
|
15
|
+
|
16
|
+
If you need async support on #{RUBY_ENGINE}, consider using:
|
17
|
+
• java.util.concurrent classes (JRuby)
|
18
|
+
• Native threading libraries for your platform
|
19
|
+
• Or stick with synchronous state machines
|
20
|
+
ERROR
|
21
|
+
end
|
22
|
+
|
23
|
+
# Load required gems with version constraints
|
24
|
+
gem 'async', '>= 2.25.0'
|
25
|
+
gem 'concurrent-ruby', '>= 1.3.5' # Security is not negotiable - enterprise-grade thread safety required
|
26
|
+
|
27
|
+
require 'async'
|
28
|
+
require 'concurrent-ruby'
|
29
|
+
|
30
|
+
# Load all async mode components
|
31
|
+
require_relative 'async_mode/thread_safe_state'
|
32
|
+
require_relative 'async_mode/async_events'
|
33
|
+
require_relative 'async_mode/async_event_extensions'
|
34
|
+
require_relative 'async_mode/async_machine'
|
35
|
+
require_relative 'async_mode/async_transition_collection'
|
36
|
+
|
37
|
+
module StateMachines
|
38
|
+
# AsyncMode provides asynchronous state machine capabilities using the async gem
|
39
|
+
# This module enables concurrent, thread-safe state operations for high-performance applications
|
40
|
+
#
|
41
|
+
# @example Basic usage
|
42
|
+
# class AutonomousDrone < StarfleetShip
|
43
|
+
# state_machine :teleporter_status, async: true do
|
44
|
+
# event :power_up do
|
45
|
+
# transition offline: :charging
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# drone = AutonomousDrone.new
|
51
|
+
# Async do
|
52
|
+
# result = drone.fire_event_async(:power_up) # => true
|
53
|
+
# task = drone.power_up_async! # => Async::Task
|
54
|
+
# end
|
55
|
+
#
|
56
|
+
module AsyncMode
|
57
|
+
# All components are loaded from separate files:
|
58
|
+
# - ThreadSafeState: Mutex-based thread safety
|
59
|
+
# - AsyncEvents: Async event firing methods
|
60
|
+
# - AsyncEventExtensions: Event method generation
|
61
|
+
# - AsyncMachine: Machine-level async capabilities
|
62
|
+
# - AsyncTransitionCollection: Concurrent transition execution
|
63
|
+
end
|
64
|
+
end
|
@@ -34,6 +34,12 @@ module StateMachines
|
|
34
34
|
# Build conditionals
|
35
35
|
@if_condition = options.delete(:if)
|
36
36
|
@unless_condition = options.delete(:unless)
|
37
|
+
@if_state_condition = options.delete(:if_state)
|
38
|
+
@unless_state_condition = options.delete(:unless_state)
|
39
|
+
@if_all_states_condition = options.delete(:if_all_states)
|
40
|
+
@unless_all_states_condition = options.delete(:unless_all_states)
|
41
|
+
@if_any_state_condition = options.delete(:if_any_state)
|
42
|
+
@unless_any_state_condition = options.delete(:unless_any_state)
|
37
43
|
|
38
44
|
# Build event requirement
|
39
45
|
@event_requirement = build_matcher(options, :on, :except_on)
|
@@ -182,21 +188,56 @@ module StateMachines
|
|
182
188
|
# Verifies that the conditionals for this branch evaluate to true for the
|
183
189
|
# given object. Event arguments are passed to guards that accept multiple parameters.
|
184
190
|
def matches_conditions?(object, query, event_args = [])
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
191
|
+
return true if query[:guard] == false
|
192
|
+
|
193
|
+
# Evaluate original if/unless conditions
|
194
|
+
if_passes = !if_condition || Array(if_condition).all? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
|
195
|
+
unless_passes = !unless_condition || Array(unless_condition).none? { |condition| evaluate_method_with_event_args(object, condition, event_args) }
|
196
|
+
|
197
|
+
return false unless if_passes && unless_passes
|
198
|
+
|
199
|
+
# Consolidate all state guards
|
200
|
+
state_guards = {
|
201
|
+
if_state: @if_state_condition,
|
202
|
+
unless_state: @unless_state_condition,
|
203
|
+
if_all_states: @if_all_states_condition,
|
204
|
+
unless_all_states: @unless_all_states_condition,
|
205
|
+
if_any_state: @if_any_state_condition,
|
206
|
+
unless_any_state: @unless_any_state_condition
|
207
|
+
}.compact
|
208
|
+
|
209
|
+
return true if state_guards.empty?
|
210
|
+
|
211
|
+
validate_and_check_state_guards(object, state_guards)
|
212
|
+
end
|
213
|
+
|
214
|
+
private
|
215
|
+
|
216
|
+
def validate_and_check_state_guards(object, guards)
|
217
|
+
guards.all? do |guard_type, conditions|
|
218
|
+
case guard_type
|
219
|
+
when :if_state, :if_all_states
|
220
|
+
conditions.all? { |machine, state| check_state(object, machine, state) }
|
221
|
+
when :unless_state
|
222
|
+
conditions.none? { |machine, state| check_state(object, machine, state) }
|
223
|
+
when :if_any_state
|
224
|
+
conditions.any? { |machine, state| check_state(object, machine, state) }
|
225
|
+
when :unless_all_states
|
226
|
+
!conditions.all? { |machine, state| check_state(object, machine, state) }
|
227
|
+
when :unless_any_state
|
228
|
+
conditions.none? { |machine, state| check_state(object, machine, state) }
|
229
|
+
end
|
199
230
|
end
|
200
231
|
end
|
232
|
+
|
233
|
+
def check_state(object, machine_name, state_name)
|
234
|
+
machine = object.class.state_machines[machine_name]
|
235
|
+
raise ArgumentError, "State machine '#{machine_name}' is not defined for #{object.class.name}" unless machine
|
236
|
+
|
237
|
+
state = machine.states[state_name]
|
238
|
+
raise ArgumentError, "State '#{state_name}' is not defined in state machine '#{machine_name}'" unless state
|
239
|
+
|
240
|
+
state.matches?(object.send(machine_name))
|
241
|
+
end
|
201
242
|
end
|
202
243
|
end
|
@@ -177,7 +177,8 @@ module StateMachines
|
|
177
177
|
# order. The callback will only halt if the resulting value from the
|
178
178
|
# method passes the terminator.
|
179
179
|
def run_methods(object, context = {}, index = 0, *args, &block)
|
180
|
-
|
180
|
+
case type
|
181
|
+
when :around
|
181
182
|
current_method = @methods[index]
|
182
183
|
if current_method
|
183
184
|
yielded = false
|
data/lib/state_machines/event.rb
CHANGED
@@ -35,15 +35,17 @@ module StateMachines
|
|
35
35
|
# * <tt>:human_name</tt> - The human-readable version of this event's name
|
36
36
|
def initialize(machine, name, options = nil, human_name: nil, **extra_options) # :nodoc:
|
37
37
|
# Handle both old hash style and new kwargs style for backward compatibility
|
38
|
-
|
38
|
+
case options
|
39
|
+
in Hash
|
39
40
|
# Old style: initialize(machine, name, {human_name: 'Custom Name'})
|
40
41
|
StateMachines::OptionsValidator.assert_valid_keys!(options, :human_name)
|
41
42
|
human_name = options[:human_name]
|
42
|
-
|
43
|
+
in nil
|
43
44
|
# New style: initialize(machine, name, human_name: 'Custom Name')
|
44
|
-
raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
|
45
|
-
|
46
45
|
StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :human_name) unless extra_options.empty?
|
46
|
+
else
|
47
|
+
# Handle unexpected options
|
48
|
+
raise ArgumentError, "Unexpected positional argument in Event initialize: #{options.inspect}"
|
47
49
|
end
|
48
50
|
|
49
51
|
@machine = machine
|
@@ -102,7 +104,7 @@ module StateMachines
|
|
102
104
|
|
103
105
|
# Only a certain subset of explicit options are allowed for transition
|
104
106
|
# requirements
|
105
|
-
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless) if (options.keys - %i[from to on except_from except_to except_on if unless]).empty?
|
107
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :from, :to, :except_from, :except_to, :if, :unless, :if_state, :unless_state, :if_all_states, :unless_all_states, :if_any_state, :unless_any_state) if (options.keys - %i[from to on except_from except_to except_on if unless if_state unless_state if_all_states unless_all_states if_any_state unless_any_state]).empty?
|
106
108
|
|
107
109
|
branches << branch = Branch.new(options.merge(on: name))
|
108
110
|
@known_states |= branch.known_states
|
@@ -117,19 +117,27 @@ module StateMachines
|
|
117
117
|
return unless machine.action
|
118
118
|
|
119
119
|
# TODO, simplify
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
120
|
+
# First try the regular event_transition
|
121
|
+
transition = machine.read(object, :event_transition)
|
122
|
+
|
123
|
+
# If not found and we have stored transitions by machine (issue #91)
|
124
|
+
if !transition && (transitions_by_machine = object.instance_variable_get(:@_state_machine_event_transitions))
|
125
|
+
transition = transitions_by_machine[machine.name]
|
126
|
+
end
|
127
|
+
|
128
|
+
transition || if event_name = machine.read(object, :event)
|
129
|
+
if event = self[event_name.to_sym, :name]
|
130
|
+
event.transition_for(object) || begin
|
131
|
+
# No valid transition: invalidate
|
132
|
+
machine.invalidate(object, :event, :invalid_event, [[:state, machine.states.match!(object).human_name(object.class)]]) if invalidate
|
133
|
+
false
|
134
|
+
end
|
135
|
+
else
|
136
|
+
# Event is unknown: invalidate
|
137
|
+
machine.invalidate(object, :event, :invalid) if invalidate
|
138
|
+
false
|
139
|
+
end
|
140
|
+
end
|
133
141
|
end
|
134
142
|
|
135
143
|
private
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# This file provides optional async extensions for the Machine class.
|
4
|
+
# It should only be loaded when async functionality is explicitly requested.
|
5
|
+
|
6
|
+
module StateMachines
|
7
|
+
class Machine
|
8
|
+
# AsyncMode extensions for the Machine class
|
9
|
+
# Provides async-aware methods while maintaining backward compatibility
|
10
|
+
module AsyncExtensions
|
11
|
+
# Instance methods added to Machine for async support
|
12
|
+
|
13
|
+
# Configure this specific machine instance for async mode
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
# class Vehicle
|
17
|
+
# state_machine initial: :parked do
|
18
|
+
# configure_async_mode! # Enable async for this machine
|
19
|
+
#
|
20
|
+
# event :ignite do
|
21
|
+
# transition parked: :idling
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
def configure_async_mode!(enabled = true)
|
26
|
+
if enabled
|
27
|
+
begin
|
28
|
+
require 'state_machines/async_mode'
|
29
|
+
@async_mode_enabled = true
|
30
|
+
|
31
|
+
owner_class.include(StateMachines::AsyncMode::ThreadSafeState)
|
32
|
+
owner_class.include(StateMachines::AsyncMode::AsyncEvents)
|
33
|
+
self.extend(StateMachines::AsyncMode::AsyncMachine)
|
34
|
+
|
35
|
+
# Extend events to generate async versions
|
36
|
+
events.each do |event|
|
37
|
+
event.extend(StateMachines::AsyncMode::AsyncEventExtensions)
|
38
|
+
end
|
39
|
+
rescue LoadError => e
|
40
|
+
# Fallback to sync mode with warning (only once per class)
|
41
|
+
unless owner_class.instance_variable_get(:@async_fallback_warned)
|
42
|
+
warn <<~WARNING
|
43
|
+
⚠️ #{owner_class.name}: Async mode requested but not available on #{RUBY_ENGINE}.
|
44
|
+
|
45
|
+
#{e.message}
|
46
|
+
|
47
|
+
⚠️ Falling back to synchronous mode. Results may be unpredictable due to engine limitations.
|
48
|
+
For production async support, use MRI Ruby (CRuby) 3.2+
|
49
|
+
WARNING
|
50
|
+
owner_class.instance_variable_set(:@async_fallback_warned, true)
|
51
|
+
end
|
52
|
+
|
53
|
+
@async_mode_enabled = false
|
54
|
+
end
|
55
|
+
else
|
56
|
+
@async_mode_enabled = false
|
57
|
+
end
|
58
|
+
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
# Check if this specific machine instance has async mode enabled
|
63
|
+
def async_mode_enabled?
|
64
|
+
@async_mode_enabled || false
|
65
|
+
end
|
66
|
+
|
67
|
+
# Thread-safe version of state reading
|
68
|
+
def read_safely(object, attribute, ivar = false)
|
69
|
+
object.read_state_safely(self, attribute, ivar)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Thread-safe version of state writing
|
73
|
+
def write_safely(object, attribute, value, ivar = false)
|
74
|
+
object.write_state_safely(self, attribute, value, ivar)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Thread-safe callback execution for async operations
|
78
|
+
def run_callbacks_safely(type, object, context, transition)
|
79
|
+
object.state_machine_mutex.with_write_lock do
|
80
|
+
callbacks[type].each { |callback| callback.call(object, context, transition) }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Include async extensions by default (but only load AsyncMode when requested)
|
86
|
+
include AsyncExtensions
|
87
|
+
end
|
88
|
+
end
|
@@ -30,6 +30,10 @@ module StateMachines
|
|
30
30
|
machine = machine.clone
|
31
31
|
machine.initial_state = options[:initial] if options.include?(:initial)
|
32
32
|
machine.owner_class = owner_class
|
33
|
+
# Configure async mode if requested in options
|
34
|
+
if options.include?(:async)
|
35
|
+
machine.configure_async_mode!(options[:async])
|
36
|
+
end
|
33
37
|
end
|
34
38
|
|
35
39
|
# Evaluate DSL
|
@@ -6,7 +6,7 @@ module StateMachines
|
|
6
6
|
# Initializes a new state machine with the given configuration.
|
7
7
|
def initialize(owner_class, *args, &)
|
8
8
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
9
|
-
StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions)
|
9
|
+
StateMachines::OptionsValidator.assert_valid_keys!(options, :attribute, :initial, :initialize, :action, :plural, :namespace, :integration, :messages, :use_transactions, :async)
|
10
10
|
|
11
11
|
# Find an integration that matches this machine's owner class
|
12
12
|
@integration = if options.include?(:integration)
|
@@ -35,6 +35,8 @@ module StateMachines
|
|
35
35
|
@use_transactions = options[:use_transactions]
|
36
36
|
@initialize_state = options[:initialize]
|
37
37
|
@action_hook_defined = false
|
38
|
+
@async_requested = options[:async]
|
39
|
+
|
38
40
|
self.owner_class = owner_class
|
39
41
|
|
40
42
|
# Merge with sibling machine configurations
|
@@ -47,6 +49,12 @@ module StateMachines
|
|
47
49
|
|
48
50
|
# Evaluate DSL
|
49
51
|
instance_eval(&) if block_given?
|
52
|
+
|
53
|
+
# Configure async mode if requested, after owner_class is set and DSL is evaluated
|
54
|
+
if @async_requested
|
55
|
+
configure_async_mode!(true)
|
56
|
+
end
|
57
|
+
|
50
58
|
self.initial_state = options[:initial] unless sibling_machines.any?
|
51
59
|
end
|
52
60
|
|
@@ -61,6 +69,8 @@ module StateMachines
|
|
61
69
|
@states = @states.dup
|
62
70
|
@states.machine = self
|
63
71
|
@callbacks = { before: @callbacks[:before].dup, after: @callbacks[:after].dup, failure: @callbacks[:failure].dup }
|
72
|
+
@async_requested = orig.instance_variable_get(:@async_requested)
|
73
|
+
@async_mode_enabled = orig.instance_variable_get(:@async_mode_enabled)
|
64
74
|
end
|
65
75
|
|
66
76
|
# Sets the class which is the owner of this state machine. Any methods
|
@@ -14,6 +14,7 @@ require_relative 'machine/event_methods'
|
|
14
14
|
require_relative 'machine/callbacks'
|
15
15
|
require_relative 'machine/rendering'
|
16
16
|
require_relative 'machine/integration'
|
17
|
+
require_relative 'machine/async_extensions'
|
17
18
|
require_relative 'syntax_validator'
|
18
19
|
|
19
20
|
module StateMachines
|
data/lib/state_machines/state.rb
CHANGED
@@ -55,7 +55,8 @@ module StateMachines
|
|
55
55
|
# * <tt>:human_name</tt> - The human-readable version of this state's name
|
56
56
|
def initialize(machine, name, options = nil, initial: false, value: :__not_provided__, cache: nil, if: nil, human_name: nil, **extra_options) # :nodoc:
|
57
57
|
# Handle both old hash style and new kwargs style for backward compatibility
|
58
|
-
|
58
|
+
case options
|
59
|
+
in Hash
|
59
60
|
# Old style: initialize(machine, name, {initial: true, value: 'foo'})
|
60
61
|
StateMachines::OptionsValidator.assert_valid_keys!(options, :initial, :value, :cache, :if, :human_name)
|
61
62
|
initial = options.fetch(:initial, false)
|
@@ -63,13 +64,13 @@ module StateMachines
|
|
63
64
|
cache = options[:cache]
|
64
65
|
if_condition = options[:if]
|
65
66
|
human_name = options[:human_name]
|
66
|
-
|
67
|
+
in nil
|
67
68
|
# New style: initialize(machine, name, initial: true, value: 'foo')
|
68
|
-
# options parameter should be nil in this case
|
69
|
-
raise ArgumentError, "Unexpected positional argument: #{options.inspect}" unless options.nil?
|
70
|
-
|
71
69
|
StateMachines::OptionsValidator.assert_valid_keys!(extra_options, :initial, :value, :cache, :if, :human_name) unless extra_options.empty?
|
72
70
|
if_condition = binding.local_variable_get(:if) # 'if' is a keyword, need special handling
|
71
|
+
else
|
72
|
+
# Handle unexpected options
|
73
|
+
raise ArgumentError, "Unexpected positional argument in State initialize: #{options.inspect}"
|
73
74
|
end
|
74
75
|
|
75
76
|
@machine = machine
|