Skip to content

Commit 5fcbbd7

Browse files
committed
feat: add async support with declarative async: true parameter (#144)
release-as: 0.40.0
1 parent 073012d commit 5fcbbd7

21 files changed

Lines changed: 1603 additions & 23 deletions

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
22
".": "0.31.0"
3-
}
3+
}

Gemfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,11 @@ gem 'rubocop', require: false
1212
gem 'rubocop-minitest', require: false
1313

1414
gem 'rubocop-rake', require: false
15+
16+
# Async support dependencies (MRI Ruby only)
17+
# These gems are required for StateMachines::AsyncMode functionality
18+
# and are loaded conditionally based on Ruby engine compatibility
19+
platform :ruby do
20+
gem 'async', require: false
21+
gem 'concurrent-ruby', require: false
22+
end

coss.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,4 @@ state_machines-activemodel = "https://github.com/state-machines/state_machines-a
139139
state_machines-activerecord = "https://github.com/state-machines/state_machines-activerecord"
140140
state_machines-audit_trail = "https://github.com/state-machines/state_machines-audit_trail"
141141
state_machines-graphviz = "https://github.com/state-machines/state_machines-graphviz"
142-
state_machines-yard = "https://github.com/state-machines/state_machines-yard"
142+
state_machines-yard = "https://github.com/state-machines/state_machines-yard"

lib/state_machines/async_mode.rb

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)