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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 247ae1ee6fa7beb6ac29a68dfd400ea41a3923b973d842a5adb2ef78960b3266
|
4
|
+
data.tar.gz: 7240a29d3d87740ade0d0197e9c4c0192176023f23e453d1ba659e8be905afef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26e8c0a197cdc254c4157a57e3bcfabfef87240280555ae623d993e30ccb68ba6433e911da171f60beb0e756af54aa60ff18395c9d49c6719a06b22bd895bdca
|
7
|
+
data.tar.gz: 9dfbdceba5b428f415dfeeba91995ab52640282b2b6c2fb92dea266c97677263331706e6b37f09a6280dc3f578da27dae1f5ade628a655342e46c565ddbe05db
|
data/README.md
CHANGED
@@ -36,6 +36,8 @@ Below is an example of many of the features offered by this plugin, including:
|
|
36
36
|
* Namespaced states
|
37
37
|
* Transition callbacks
|
38
38
|
* Conditional transitions
|
39
|
+
* Coordinated state management guards
|
40
|
+
* Asynchronous state machines (async: true)
|
39
41
|
* State-driven instance behavior
|
40
42
|
* Customized state values
|
41
43
|
* Parallel events
|
@@ -245,6 +247,206 @@ vehicle.alarm_state_name # => :active
|
|
245
247
|
|
246
248
|
vehicle.fire_events!(:ignite, :enable_alarm) # => StateMachines:InvalidParallelTransition: Cannot run events in parallel: ignite, enable_alarm
|
247
249
|
|
250
|
+
# Coordinated State Management
|
251
|
+
|
252
|
+
State machines can coordinate with each other using state guards, allowing transitions to depend on the state of other state machines within the same object. This enables complex system modeling where components are interdependent.
|
253
|
+
|
254
|
+
## State Guard Options
|
255
|
+
|
256
|
+
### Single State Guards
|
257
|
+
|
258
|
+
* `:if_state` - Transition only if another state machine is in a specific state.
|
259
|
+
* `:unless_state` - Transition only if another state machine is NOT in a specific state.
|
260
|
+
|
261
|
+
```ruby
|
262
|
+
class TorpedoSystem
|
263
|
+
state_machine :bay_doors, initial: :closed do
|
264
|
+
event :open do
|
265
|
+
transition closed: :open
|
266
|
+
end
|
267
|
+
|
268
|
+
event :close do
|
269
|
+
transition open: :closed
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
state_machine :torpedo_status, initial: :loaded do
|
274
|
+
event :fire_torpedo do
|
275
|
+
# Can only fire torpedo if bay doors are open
|
276
|
+
transition loaded: :fired, if_state: { bay_doors: :open }
|
277
|
+
end
|
278
|
+
|
279
|
+
event :reload do
|
280
|
+
# Can only reload if bay doors are closed (for safety)
|
281
|
+
transition fired: :loaded, unless_state: { bay_doors: :open }
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
system = TorpedoSystem.new
|
287
|
+
system.fire_torpedo # => false (bay doors are closed)
|
288
|
+
|
289
|
+
system.open_bay_doors!
|
290
|
+
system.fire_torpedo # => true (bay doors are now open)
|
291
|
+
```
|
292
|
+
|
293
|
+
### Multiple State Guards
|
294
|
+
|
295
|
+
* `:if_all_states` - Transition only if ALL specified state machines are in their respective states.
|
296
|
+
* `:unless_all_states` - Transition only if NOT ALL specified state machines are in their respective states.
|
297
|
+
* `:if_any_state` - Transition only if ANY of the specified state machines are in their respective states.
|
298
|
+
* `:unless_any_state` - Transition only if NONE of the specified state machines are in their respective states.
|
299
|
+
|
300
|
+
```ruby
|
301
|
+
class StarshipBridge
|
302
|
+
state_machine :shields, initial: :down
|
303
|
+
state_machine :weapons, initial: :offline
|
304
|
+
state_machine :warp_core, initial: :stable
|
305
|
+
|
306
|
+
state_machine :alert_status, initial: :green do
|
307
|
+
event :red_alert do
|
308
|
+
# Red alert if ANY critical system needs attention
|
309
|
+
transition green: :red, if_any_state: { warp_core: :critical, shields: :down }
|
310
|
+
end
|
311
|
+
|
312
|
+
event :battle_stations do
|
313
|
+
# Battle stations only if ALL combat systems are ready
|
314
|
+
transition green: :battle, if_all_states: { shields: :up, weapons: :armed }
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
```
|
319
|
+
|
320
|
+
## Error Handling
|
321
|
+
|
322
|
+
State guards provide comprehensive error checking:
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
# Referencing a non-existent state machine
|
326
|
+
event :invalid, if_state: { nonexistent_machine: :some_state }
|
327
|
+
# => ArgumentError: State machine 'nonexistent_machine' is not defined for StarshipBridge
|
328
|
+
|
329
|
+
# Referencing a non-existent state
|
330
|
+
event :another_invalid, if_state: { shields: :nonexistent_state }
|
331
|
+
# => ArgumentError: State 'nonexistent_state' is not defined in state machine 'shields'
|
332
|
+
```
|
333
|
+
|
334
|
+
# Asynchronous State Machines
|
335
|
+
|
336
|
+
State machines can operate asynchronously for high-performance applications. This is ideal for I/O-bound tasks, such as in web servers or other concurrent environments, where you don't want a long-running state transition (like one involving a network call) to block the entire thread.
|
337
|
+
|
338
|
+
This feature is powered by the [async](https://github.com/socketry/async) gem and uses `concurrent-ruby` for enterprise-grade thread safety.
|
339
|
+
|
340
|
+
## Platform Compatibility
|
341
|
+
|
342
|
+
**Supported Platforms:**
|
343
|
+
* MRI Ruby (CRuby) 3.2+
|
344
|
+
* Other Ruby engines with full Fiber scheduler support
|
345
|
+
|
346
|
+
**Unsupported Platforms:**
|
347
|
+
* JRuby - Falls back to synchronous mode with warnings
|
348
|
+
* TruffleRuby - Falls back to synchronous mode with warnings
|
349
|
+
|
350
|
+
## Basic Async Usage
|
351
|
+
|
352
|
+
Enable async mode by adding `async: true` to your state machine declaration:
|
353
|
+
|
354
|
+
```ruby
|
355
|
+
class AutonomousDrone < StarfleetShip
|
356
|
+
# Async-enabled state machine for autonomous operation
|
357
|
+
state_machine :status, async: true, initial: :docked do
|
358
|
+
event :launch do
|
359
|
+
transition docked: :flying
|
360
|
+
end
|
361
|
+
|
362
|
+
event :land do
|
363
|
+
transition flying: :docked
|
364
|
+
end
|
365
|
+
end
|
366
|
+
|
367
|
+
# Mixed configuration: some machines async, others sync
|
368
|
+
state_machine :teleporter_status, async: true, initial: :offline do
|
369
|
+
event :power_up do
|
370
|
+
transition offline: :charging
|
371
|
+
end
|
372
|
+
|
373
|
+
event :teleport do
|
374
|
+
transition ready: :teleporting
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
# Weapons remain synchronous for safety
|
379
|
+
state_machine :weapons, initial: :disarmed do
|
380
|
+
event :arm do
|
381
|
+
transition disarmed: :armed
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
```
|
386
|
+
|
387
|
+
## Async Event Methods
|
388
|
+
|
389
|
+
Async-enabled machines automatically generate async versions of event methods:
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
drone = AutonomousDrone.new
|
393
|
+
|
394
|
+
# Within an Async context
|
395
|
+
Async do
|
396
|
+
# Async event firing - returns Async::Task
|
397
|
+
task = drone.launch_async
|
398
|
+
result = task.wait # => true
|
399
|
+
|
400
|
+
# Bang methods for strict error handling
|
401
|
+
drone.power_up_async! # => Async::Task (raises on failure)
|
402
|
+
|
403
|
+
# Generic async event firing
|
404
|
+
drone.fire_event_async(:teleport) # => Async::Task
|
405
|
+
end
|
406
|
+
|
407
|
+
# Outside Async context - raises error
|
408
|
+
drone.launch_async # => RuntimeError: launch_async must be called within an Async context
|
409
|
+
```
|
410
|
+
|
411
|
+
## Thread Safety
|
412
|
+
|
413
|
+
Async state machines use enterprise-grade thread safety with `concurrent-ruby`:
|
414
|
+
|
415
|
+
```ruby
|
416
|
+
# Concurrent operations are automatically thread-safe
|
417
|
+
threads = []
|
418
|
+
10.times do
|
419
|
+
threads << Thread.new do
|
420
|
+
Async do
|
421
|
+
drone.launch_async.wait
|
422
|
+
drone.land_async.wait
|
423
|
+
end
|
424
|
+
end
|
425
|
+
end
|
426
|
+
threads.each(&:join)
|
427
|
+
```
|
428
|
+
|
429
|
+
## Performance Considerations
|
430
|
+
|
431
|
+
* **Thread Safety**: Uses `Concurrent::ReentrantReadWriteLock` for optimal read/write performance.
|
432
|
+
* **Memory**: Each async-enabled object gets its own mutex (lazy-loaded).
|
433
|
+
* **Marshalling**: Objects with async state machines can be serialized (mutex excluded/recreated).
|
434
|
+
* **Mixed Mode**: You can mix async and sync state machines in the same class.
|
435
|
+
|
436
|
+
## Dependencies
|
437
|
+
|
438
|
+
Async functionality requires:
|
439
|
+
|
440
|
+
```ruby
|
441
|
+
# Gemfile (automatically scoped to MRI Ruby)
|
442
|
+
platform :ruby do
|
443
|
+
gem 'async', '>= 2.25.0'
|
444
|
+
gem 'concurrent-ruby', '>= 1.3.5'
|
445
|
+
end
|
446
|
+
```
|
447
|
+
|
448
|
+
*Note: These gems are only installed on supported platforms. JRuby/TruffleRuby won't attempt installation.*
|
449
|
+
|
248
450
|
# Human-friendly names can be accessed for states/events
|
249
451
|
Vehicle.human_state_name(:first_gear) # => "first gear"
|
250
452
|
Vehicle.human_alarm_state_name(:active) # => "active"
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
module AsyncMode
|
5
|
+
# Extensions to Event class for async bang methods
|
6
|
+
module AsyncEventExtensions
|
7
|
+
# Generate async bang methods for events when async mode is enabled
|
8
|
+
def define_helper(scope, method, *args, &block)
|
9
|
+
result = super
|
10
|
+
|
11
|
+
# If this is an async-enabled machine and we're defining an event method
|
12
|
+
if scope == :instance && method !~ /_async[!]?$/ && machine.async_mode_enabled?
|
13
|
+
qualified_name = method.to_s
|
14
|
+
|
15
|
+
# Create async version that returns a task
|
16
|
+
machine.define_helper(scope, "#{qualified_name}_async") do |machine, object, *method_args, **kwargs|
|
17
|
+
# Find the machine that has this event
|
18
|
+
target_machine = object.class.state_machines.values.find { |m| m.events[name] }
|
19
|
+
|
20
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
21
|
+
raise RuntimeError, "#{qualified_name}_async must be called within an Async context"
|
22
|
+
end
|
23
|
+
|
24
|
+
Async do
|
25
|
+
target_machine.events[name].fire(object, *method_args, **kwargs)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Create async bang version that raises exceptions when awaited
|
30
|
+
machine.define_helper(scope, "#{qualified_name}_async!") do |machine, object, *method_args, **kwargs|
|
31
|
+
# Find the machine that has this event
|
32
|
+
target_machine = object.class.state_machines.values.find { |m| m.events[name] }
|
33
|
+
|
34
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
35
|
+
raise RuntimeError, "#{qualified_name}_async! must be called within an Async context"
|
36
|
+
end
|
37
|
+
|
38
|
+
Async do
|
39
|
+
# Use fire method which will raise exceptions on invalid transitions
|
40
|
+
target_machine.events[name].fire(object, *method_args, **kwargs) || raise(StateMachines::InvalidTransition.new(object, target_machine, name))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
result
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,282 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
module AsyncMode
|
5
|
+
# Async-aware event firing capabilities using the async gem
|
6
|
+
module AsyncEvents
|
7
|
+
# Fires an event asynchronously using Async
|
8
|
+
# Returns an Async::Task that can be awaited for the result
|
9
|
+
#
|
10
|
+
# Example:
|
11
|
+
# Async do
|
12
|
+
# task = vehicle.async_fire_event(:ignite)
|
13
|
+
# result = task.wait # => true/false
|
14
|
+
# end
|
15
|
+
def async_fire_event(event_name, *args)
|
16
|
+
# Find the machine that has this event
|
17
|
+
machine = self.class.state_machines.values.find { |m| m.events[event_name] }
|
18
|
+
|
19
|
+
unless machine
|
20
|
+
raise ArgumentError, "Event #{event_name} not found in any state machine"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Must be called within an Async context
|
24
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
25
|
+
raise RuntimeError, "async_fire_event must be called within an Async context. Use: Async { vehicle.async_fire_event(:event) }"
|
26
|
+
end
|
27
|
+
|
28
|
+
Async do
|
29
|
+
machine.events[event_name].fire(self, *args)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Fires multiple events asynchronously across different state machines
|
34
|
+
# Returns an array of Async::Tasks for concurrent execution
|
35
|
+
#
|
36
|
+
# Example:
|
37
|
+
# Async do
|
38
|
+
# tasks = vehicle.async_fire_events(:ignite, :buy_insurance)
|
39
|
+
# results = tasks.map(&:wait) # => [true, true]
|
40
|
+
# end
|
41
|
+
def async_fire_events(*event_names)
|
42
|
+
event_names.map { |event_name| async_fire_event(event_name) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fires an event asynchronously and waits for completion
|
46
|
+
# This is a convenience method that creates and waits for the task
|
47
|
+
#
|
48
|
+
# Example:
|
49
|
+
# result = vehicle.fire_event_async(:ignite) # => true/false
|
50
|
+
def fire_event_async(event_name, *args)
|
51
|
+
raise NoMethodError, "undefined method `fire_event_async' for #{self}" unless has_async_machines?
|
52
|
+
# Find the machine that has this event
|
53
|
+
machine = self.class.state_machines.values.find { |m| m.events[event_name] }
|
54
|
+
|
55
|
+
unless machine
|
56
|
+
raise ArgumentError, "Event #{event_name} not found in any state machine"
|
57
|
+
end
|
58
|
+
|
59
|
+
if defined?(::Async::Task) && ::Async::Task.current?
|
60
|
+
# Already in async context, just fire directly
|
61
|
+
machine.events[event_name].fire(self, *args)
|
62
|
+
else
|
63
|
+
# Create async context and wait for result
|
64
|
+
Async do
|
65
|
+
machine.events[event_name].fire(self, *args)
|
66
|
+
end.wait
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Fires multiple events asynchronously and waits for all completions
|
71
|
+
# Returns results in the same order as the input events
|
72
|
+
#
|
73
|
+
# Example:
|
74
|
+
# results = vehicle.fire_events_async(:ignite, :buy_insurance) # => [true, true]
|
75
|
+
def fire_events_async(*event_names)
|
76
|
+
raise NoMethodError, "undefined method `fire_events_async' for #{self}" unless has_async_machines?
|
77
|
+
if defined?(::Async::Task) && ::Async::Task.current?
|
78
|
+
# Already in async context, run concurrently
|
79
|
+
tasks = event_names.map { |event_name| async_fire_event(event_name) }
|
80
|
+
tasks.map(&:wait)
|
81
|
+
else
|
82
|
+
# Create async context and run concurrently
|
83
|
+
Async do
|
84
|
+
tasks = event_names.map { |event_name| async_fire_event(event_name) }
|
85
|
+
tasks.map(&:wait)
|
86
|
+
end.wait
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Fires an event asynchronously using Async and raises exception on failure
|
91
|
+
# Returns an Async::Task that raises StateMachines::InvalidTransition when awaited
|
92
|
+
#
|
93
|
+
# Example:
|
94
|
+
# Async do
|
95
|
+
# begin
|
96
|
+
# task = vehicle.async_fire_event!(:ignite)
|
97
|
+
# result = task.wait
|
98
|
+
# puts "Event fired successfully!"
|
99
|
+
# rescue StateMachines::InvalidTransition => e
|
100
|
+
# puts "Transition failed: #{e.message}"
|
101
|
+
# end
|
102
|
+
# end
|
103
|
+
def async_fire_event!(event_name, *args)
|
104
|
+
# Find the machine that has this event
|
105
|
+
machine = self.class.state_machines.values.find { |m| m.events[event_name] }
|
106
|
+
|
107
|
+
unless machine
|
108
|
+
raise ArgumentError, "Event #{event_name} not found in any state machine"
|
109
|
+
end
|
110
|
+
|
111
|
+
# Must be called within an Async context
|
112
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
113
|
+
raise RuntimeError, "async_fire_event! must be called within an Async context. Use: Async { vehicle.async_fire_event!(:event) }"
|
114
|
+
end
|
115
|
+
|
116
|
+
Async do
|
117
|
+
# Use the bang version which raises exceptions on failure
|
118
|
+
machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Fires an event asynchronously and waits for result, raising exceptions on failure
|
123
|
+
# This is a convenience method that creates and waits for the task
|
124
|
+
#
|
125
|
+
# Example:
|
126
|
+
# begin
|
127
|
+
# result = vehicle.fire_event_async!(:ignite)
|
128
|
+
# puts "Event fired successfully!"
|
129
|
+
# rescue StateMachines::InvalidTransition => e
|
130
|
+
# puts "Transition failed: #{e.message}"
|
131
|
+
# end
|
132
|
+
def fire_event_async!(event_name, *args)
|
133
|
+
raise NoMethodError, "undefined method `fire_event_async!' for #{self}" unless has_async_machines?
|
134
|
+
# Find the machine that has this event
|
135
|
+
machine = self.class.state_machines.values.find { |m| m.events[event_name] }
|
136
|
+
|
137
|
+
unless machine
|
138
|
+
raise ArgumentError, "Event #{event_name} not found in any state machine"
|
139
|
+
end
|
140
|
+
|
141
|
+
if defined?(::Async::Task) && ::Async::Task.current?
|
142
|
+
# Already in async context, just fire directly with bang behavior
|
143
|
+
machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
|
144
|
+
else
|
145
|
+
# Create async context and wait for result (may raise exception)
|
146
|
+
Async do
|
147
|
+
machine.events[event_name].fire(self, *args) || raise(StateMachines::InvalidTransition.new(self, machine, event_name))
|
148
|
+
end.wait
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Dynamically handle individual event async methods
|
153
|
+
# This provides launch_async, launch_async!, arm_weapons_async, etc.
|
154
|
+
def method_missing(method_name, *args, **kwargs, &block)
|
155
|
+
method_str = method_name.to_s
|
156
|
+
|
157
|
+
# Check if this is an async event method
|
158
|
+
if method_str.end_with?('_async!')
|
159
|
+
# Remove the _async! suffix to get the base event method
|
160
|
+
base_method = method_str.chomp('_async!').to_sym
|
161
|
+
|
162
|
+
# Check if the base method exists and this machine is async-enabled
|
163
|
+
if respond_to?(base_method) && async_method_for_event?(base_method)
|
164
|
+
return handle_individual_event_async_bang(base_method, *args, **kwargs)
|
165
|
+
end
|
166
|
+
elsif method_str.end_with?('_async')
|
167
|
+
# Remove the _async suffix to get the base event method
|
168
|
+
base_method = method_str.chomp('_async').to_sym
|
169
|
+
|
170
|
+
# Check if the base method exists and this machine is async-enabled
|
171
|
+
if respond_to?(base_method) && async_method_for_event?(base_method)
|
172
|
+
return handle_individual_event_async(base_method, *args, **kwargs)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# If not an async method, call the original method_missing
|
177
|
+
super
|
178
|
+
end
|
179
|
+
|
180
|
+
# Check if we should respond to async methods for this event
|
181
|
+
def respond_to_missing?(method_name, include_private = false)
|
182
|
+
# Only provide async methods if this object has async-enabled machines
|
183
|
+
return super unless has_async_machines?
|
184
|
+
|
185
|
+
method_str = method_name.to_s
|
186
|
+
|
187
|
+
if method_str.end_with?('_async!') || method_str.end_with?('_async')
|
188
|
+
base_method = method_str.chomp('_async!').chomp('_async').to_sym
|
189
|
+
return respond_to?(base_method) && async_method_for_event?(base_method)
|
190
|
+
end
|
191
|
+
|
192
|
+
super
|
193
|
+
end
|
194
|
+
|
195
|
+
# Check if this object has any async-enabled state machines
|
196
|
+
def has_async_machines?
|
197
|
+
self.class.state_machines.any? { |name, machine| machine.async_mode_enabled? }
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
# Check if this event method should have async versions
|
203
|
+
def async_method_for_event?(event_method)
|
204
|
+
# Find which machine contains this event
|
205
|
+
self.class.state_machines.each do |name, machine|
|
206
|
+
if machine.async_mode_enabled?
|
207
|
+
# Check if this event method belongs to this machine
|
208
|
+
machine.events.each do |event|
|
209
|
+
qualified_name = event.qualified_name
|
210
|
+
if qualified_name.to_sym == event_method || "#{qualified_name}!".to_sym == event_method
|
211
|
+
return true
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
false
|
217
|
+
end
|
218
|
+
|
219
|
+
# Handle individual event async methods (returns task)
|
220
|
+
def handle_individual_event_async(event_method, *args, **kwargs)
|
221
|
+
|
222
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
223
|
+
raise RuntimeError, "#{event_method}_async must be called within an Async context"
|
224
|
+
end
|
225
|
+
|
226
|
+
Async do
|
227
|
+
send(event_method, *args, **kwargs)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
# Handle individual event async bang methods (returns task, raises on failure)
|
232
|
+
def handle_individual_event_async_bang(event_method, *args, **kwargs)
|
233
|
+
# Extract event name from method and use bang version
|
234
|
+
bang_method = "#{event_method}!".to_sym
|
235
|
+
|
236
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
237
|
+
raise RuntimeError, "#{event_method}_async! must be called within an Async context"
|
238
|
+
end
|
239
|
+
|
240
|
+
Async do
|
241
|
+
send(bang_method, *args, **kwargs)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Extract event name from method name, handling namespaced events
|
246
|
+
def extract_event_name(method_name)
|
247
|
+
method_str = method_name.to_s
|
248
|
+
|
249
|
+
# Find the machine and event for this method
|
250
|
+
self.class.state_machines.each do |name, machine|
|
251
|
+
machine.events.each do |event|
|
252
|
+
qualified_name = event.qualified_name
|
253
|
+
if qualified_name.to_s == method_str || "#{qualified_name}!".to_s == method_str
|
254
|
+
return event.name
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Fallback: assume the method name is the event name
|
260
|
+
method_str.chomp('!').to_sym
|
261
|
+
end
|
262
|
+
|
263
|
+
public
|
264
|
+
|
265
|
+
# Fires multiple events concurrently within an async context
|
266
|
+
# This method should be called from within an Async block
|
267
|
+
#
|
268
|
+
# Example:
|
269
|
+
# Async do
|
270
|
+
# results = vehicle.fire_events_concurrent(:ignite, :buy_insurance)
|
271
|
+
# end
|
272
|
+
def fire_events_concurrent(*event_names)
|
273
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
274
|
+
raise RuntimeError, "fire_events_concurrent must be called within an Async context"
|
275
|
+
end
|
276
|
+
|
277
|
+
tasks = async_fire_events(*event_names)
|
278
|
+
tasks.map(&:wait)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateMachines
|
4
|
+
module AsyncMode
|
5
|
+
# Enhanced machine class with async capabilities
|
6
|
+
module AsyncMachine
|
7
|
+
# Thread-safe state reading for machines
|
8
|
+
def read_safely(object, attribute, ivar = false)
|
9
|
+
if object.respond_to?(:read_state_safely)
|
10
|
+
object.read_state_safely(self, attribute, ivar)
|
11
|
+
else
|
12
|
+
read(object, attribute, ivar)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Thread-safe state writing for machines
|
17
|
+
def write_safely(object, attribute, value, ivar = false)
|
18
|
+
if object.respond_to?(:write_state_safely)
|
19
|
+
object.write_state_safely(self, attribute, value, ivar)
|
20
|
+
else
|
21
|
+
write(object, attribute, value, ivar)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fires an event asynchronously on the given object
|
26
|
+
# Returns an Async::Task for concurrent execution
|
27
|
+
def async_fire_event(object, event_name, *args)
|
28
|
+
unless defined?(::Async::Task) && ::Async::Task.current?
|
29
|
+
raise RuntimeError, "async_fire_event must be called within an Async context"
|
30
|
+
end
|
31
|
+
|
32
|
+
Async do
|
33
|
+
events[event_name].fire(object, *args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates an async-aware transition collection
|
38
|
+
# Supports concurrent transition execution with proper synchronization
|
39
|
+
def create_async_transition_collection(transitions, options = {})
|
40
|
+
if defined?(AsyncTransitionCollection)
|
41
|
+
AsyncTransitionCollection.new(transitions, options)
|
42
|
+
else
|
43
|
+
# Fallback to regular collection if async collection isn't available
|
44
|
+
TransitionCollection.new(transitions, options)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Thread-safe callback execution for async operations
|
49
|
+
def run_callbacks_safely(type, object, context, transition)
|
50
|
+
if object.respond_to?(:state_machine_mutex)
|
51
|
+
object.state_machine_mutex.with_read_lock do
|
52
|
+
callbacks[type].each { |callback| callback.call(object, context, transition) }
|
53
|
+
end
|
54
|
+
else
|
55
|
+
callbacks[type].each { |callback| callback.call(object, context, transition) }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|