ultra-smart-sys 0.0.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 +7 -0
- data/state_machines-0.201.0/LICENSE.txt +23 -0
- data/state_machines-0.201.0/README.md +1024 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_event_extensions.rb +49 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_events.rb +282 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_machine.rb +60 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/async_transition_collection.rb +136 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode/thread_safe_state.rb +47 -0
- data/state_machines-0.201.0/lib/state_machines/async_mode.rb +64 -0
- data/state_machines-0.201.0/lib/state_machines/branch.rb +246 -0
- data/state_machines-0.201.0/lib/state_machines/callback.rb +223 -0
- data/state_machines-0.201.0/lib/state_machines/core.rb +43 -0
- data/state_machines-0.201.0/lib/state_machines/core_ext/class/state_machine.rb +5 -0
- data/state_machines-0.201.0/lib/state_machines/core_ext.rb +4 -0
- data/state_machines-0.201.0/lib/state_machines/error.rb +115 -0
- data/state_machines-0.201.0/lib/state_machines/eval_helpers.rb +227 -0
- data/state_machines-0.201.0/lib/state_machines/event.rb +247 -0
- data/state_machines-0.201.0/lib/state_machines/event_collection.rb +149 -0
- data/state_machines-0.201.0/lib/state_machines/extensions.rb +150 -0
- data/state_machines-0.201.0/lib/state_machines/helper_module.rb +19 -0
- data/state_machines-0.201.0/lib/state_machines/integrations/base.rb +49 -0
- data/state_machines-0.201.0/lib/state_machines/integrations.rb +111 -0
- data/state_machines-0.201.0/lib/state_machines/machine/action_hooks.rb +53 -0
- data/state_machines-0.201.0/lib/state_machines/machine/async_extensions.rb +88 -0
- data/state_machines-0.201.0/lib/state_machines/machine/callbacks.rb +333 -0
- data/state_machines-0.201.0/lib/state_machines/machine/class_methods.rb +95 -0
- data/state_machines-0.201.0/lib/state_machines/machine/configuration.rb +128 -0
- data/state_machines-0.201.0/lib/state_machines/machine/event_methods.rb +436 -0
- data/state_machines-0.201.0/lib/state_machines/machine/helper_generators.rb +125 -0
- data/state_machines-0.201.0/lib/state_machines/machine/integration.rb +92 -0
- data/state_machines-0.201.0/lib/state_machines/machine/parsing.rb +77 -0
- data/state_machines-0.201.0/lib/state_machines/machine/rendering.rb +17 -0
- data/state_machines-0.201.0/lib/state_machines/machine/scoping.rb +44 -0
- data/state_machines-0.201.0/lib/state_machines/machine/state_methods.rb +398 -0
- data/state_machines-0.201.0/lib/state_machines/machine/utilities.rb +86 -0
- data/state_machines-0.201.0/lib/state_machines/machine/validation.rb +39 -0
- data/state_machines-0.201.0/lib/state_machines/machine.rb +615 -0
- data/state_machines-0.201.0/lib/state_machines/machine_collection.rb +105 -0
- data/state_machines-0.201.0/lib/state_machines/macro_methods.rb +522 -0
- data/state_machines-0.201.0/lib/state_machines/matcher.rb +124 -0
- data/state_machines-0.201.0/lib/state_machines/matcher_helpers.rb +56 -0
- data/state_machines-0.201.0/lib/state_machines/node_collection.rb +226 -0
- data/state_machines-0.201.0/lib/state_machines/options_validator.rb +72 -0
- data/state_machines-0.201.0/lib/state_machines/path.rb +123 -0
- data/state_machines-0.201.0/lib/state_machines/path_collection.rb +91 -0
- data/state_machines-0.201.0/lib/state_machines/state.rb +314 -0
- data/state_machines-0.201.0/lib/state_machines/state_collection.rb +113 -0
- data/state_machines-0.201.0/lib/state_machines/state_context.rb +134 -0
- data/state_machines-0.201.0/lib/state_machines/stdio_renderer.rb +74 -0
- data/state_machines-0.201.0/lib/state_machines/syntax_validator.rb +54 -0
- data/state_machines-0.201.0/lib/state_machines/test_helper.rb +775 -0
- data/state_machines-0.201.0/lib/state_machines/transition.rb +593 -0
- data/state_machines-0.201.0/lib/state_machines/transition_collection.rb +310 -0
- data/state_machines-0.201.0/lib/state_machines/version.rb +5 -0
- data/state_machines-0.201.0/lib/state_machines.rb +6 -0
- data/ultra-smart-sys.gemspec +12 -0
- metadata +96 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
# Module to extend Fiber instances for pausable state tracking
|
|
5
|
+
module PausableFiber
|
|
6
|
+
attr_accessor :state_machine_fiber_pausable
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# A transition represents a state change for a specific attribute.
|
|
10
|
+
#
|
|
11
|
+
# Transitions consist of:
|
|
12
|
+
# * An event
|
|
13
|
+
# * A starting state
|
|
14
|
+
# * An ending state
|
|
15
|
+
class Transition
|
|
16
|
+
# The object being transitioned
|
|
17
|
+
attr_reader :object
|
|
18
|
+
|
|
19
|
+
# The state machine for which this transition is defined
|
|
20
|
+
attr_reader :machine
|
|
21
|
+
|
|
22
|
+
# The original state value *before* the transition
|
|
23
|
+
attr_reader :from
|
|
24
|
+
|
|
25
|
+
# The new state value *after* the transition
|
|
26
|
+
attr_reader :to
|
|
27
|
+
|
|
28
|
+
# The arguments passed in to the event that triggered the transition
|
|
29
|
+
# (does not include the +run_action+ boolean argument if specified)
|
|
30
|
+
attr_accessor :args
|
|
31
|
+
|
|
32
|
+
# The result of invoking the action associated with the machine
|
|
33
|
+
attr_reader :result
|
|
34
|
+
|
|
35
|
+
# Whether the transition is only existing temporarily for the object
|
|
36
|
+
attr_writer :transient
|
|
37
|
+
|
|
38
|
+
# Creates a new, specific transition
|
|
39
|
+
def initialize(object, machine, event, from_name, to_name, read_state = true) # :nodoc:
|
|
40
|
+
@object = object
|
|
41
|
+
@machine = machine
|
|
42
|
+
@args = []
|
|
43
|
+
@transient = false
|
|
44
|
+
@paused_fiber = nil
|
|
45
|
+
@resuming = false
|
|
46
|
+
@continuation_block = nil
|
|
47
|
+
@fiber_thread_storage = nil
|
|
48
|
+
|
|
49
|
+
@event = machine.events.fetch(event)
|
|
50
|
+
@from_state = machine.states.fetch(from_name)
|
|
51
|
+
@from = read_state ? machine.read(object, :state) : @from_state.value
|
|
52
|
+
@to_state = machine.states.fetch(to_name)
|
|
53
|
+
@to = @to_state.value
|
|
54
|
+
|
|
55
|
+
reset
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The attribute which this transition's machine is defined for
|
|
59
|
+
def attribute
|
|
60
|
+
machine.attribute
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The action that will be run when this transition is performed
|
|
64
|
+
def action
|
|
65
|
+
machine.action
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The event that triggered the transition
|
|
69
|
+
def event
|
|
70
|
+
@event.name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# The fully-qualified name of the event that triggered the transition
|
|
74
|
+
def qualified_event
|
|
75
|
+
@event.qualified_name
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The human-readable name of the event that triggered the transition
|
|
79
|
+
def human_event
|
|
80
|
+
@event.human_name(@object.class)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The state name *before* the transition
|
|
84
|
+
def from_name
|
|
85
|
+
@from_state.name
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# The fully-qualified state name *before* the transition
|
|
89
|
+
def qualified_from_name
|
|
90
|
+
@from_state.qualified_name
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# The human-readable state name *before* the transition
|
|
94
|
+
def human_from_name
|
|
95
|
+
@from_state.human_name(@object.class)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The new state name *after* the transition
|
|
99
|
+
def to_name
|
|
100
|
+
@to_state.name
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# The new fully-qualified state name *after* the transition
|
|
104
|
+
def qualified_to_name
|
|
105
|
+
@to_state.qualified_name
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The new human-readable state name *after* the transition
|
|
109
|
+
def human_to_name
|
|
110
|
+
@to_state.human_name(@object.class)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Does this transition represent a loopback (i.e. the from and to state
|
|
114
|
+
# are the same)
|
|
115
|
+
#
|
|
116
|
+
# == Example
|
|
117
|
+
#
|
|
118
|
+
# machine = StateMachine.new(Vehicle)
|
|
119
|
+
# StateMachines::Transition.new(Vehicle.new, machine, :park, :parked, :parked).loopback? # => true
|
|
120
|
+
# StateMachines::Transition.new(Vehicle.new, machine, :park, :idling, :parked).loopback? # => false
|
|
121
|
+
def loopback?
|
|
122
|
+
from_name == to_name
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Is this transition existing for a short period only? If this is set, it
|
|
126
|
+
# indicates that the transition (or the event backing it) should not be
|
|
127
|
+
# written to the object if it fails.
|
|
128
|
+
def transient?
|
|
129
|
+
@transient
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# A hash of all the core attributes defined for this transition with their
|
|
133
|
+
# names as keys and values of the attributes as values.
|
|
134
|
+
#
|
|
135
|
+
# == Example
|
|
136
|
+
#
|
|
137
|
+
# machine = StateMachine.new(Vehicle)
|
|
138
|
+
# transition = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
|
139
|
+
# transition.attributes # => {:object => #<Vehicle:0xb7d60ea4>, :attribute => :state, :event => :ignite, :from => 'parked', :to => 'idling'}
|
|
140
|
+
def attributes
|
|
141
|
+
@attributes ||= { object: object, attribute: attribute, event: event, from: from, to: to }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Runs the actual transition and any before/after callbacks associated
|
|
145
|
+
# with the transition. The action associated with the transition/machine
|
|
146
|
+
# can be skipped by passing in +false+.
|
|
147
|
+
#
|
|
148
|
+
# == Examples
|
|
149
|
+
#
|
|
150
|
+
# class Vehicle
|
|
151
|
+
# state_machine :action => :save do
|
|
152
|
+
# ...
|
|
153
|
+
# end
|
|
154
|
+
# end
|
|
155
|
+
#
|
|
156
|
+
# vehicle = Vehicle.new
|
|
157
|
+
# transition = StateMachines::Transition.new(vehicle, machine, :ignite, :parked, :idling)
|
|
158
|
+
# transition.perform # => Runs the +save+ action after setting the state attribute
|
|
159
|
+
# transition.perform(false) # => Only sets the state attribute
|
|
160
|
+
# transition.perform(run_action: false) # => Only sets the state attribute
|
|
161
|
+
# transition.perform(Time.now) # => Passes in additional arguments and runs the +save+ action
|
|
162
|
+
# transition.perform(Time.now, false) # => Passes in additional arguments and only sets the state attribute
|
|
163
|
+
# transition.perform(Time.now, run_action: false) # => Passes in additional arguments and only sets the state attribute
|
|
164
|
+
def perform(*args)
|
|
165
|
+
run_action = case args.last
|
|
166
|
+
in true | false
|
|
167
|
+
args.pop
|
|
168
|
+
in { run_action: }
|
|
169
|
+
args.last.delete(:run_action)
|
|
170
|
+
else
|
|
171
|
+
true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
self.args = args
|
|
175
|
+
|
|
176
|
+
# Run the transition
|
|
177
|
+
!!TransitionCollection.new([self], { use_transactions: machine.use_transactions, actions: run_action }).perform
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Runs a block within a transaction for the object being transitioned.
|
|
181
|
+
# By default, transactions are a no-op unless otherwise defined by the
|
|
182
|
+
# machine's integration.
|
|
183
|
+
def within_transaction(&)
|
|
184
|
+
machine.within_transaction(object, &)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Runs the before / after callbacks for this transition. If a block is
|
|
188
|
+
# provided, then it will be executed between the before and after callbacks.
|
|
189
|
+
#
|
|
190
|
+
# Configuration options:
|
|
191
|
+
# * +before+ - Whether to run before callbacks.
|
|
192
|
+
# * +after+ - Whether to run after callbacks. If false, then any around
|
|
193
|
+
# callbacks will be paused until called again with +after+ enabled.
|
|
194
|
+
# Default is true.
|
|
195
|
+
#
|
|
196
|
+
# This will return true if all before callbacks gets executed. After
|
|
197
|
+
# callbacks will not have an effect on the result.
|
|
198
|
+
def run_callbacks(options = {}, &block)
|
|
199
|
+
options = { before: true, after: true }.merge(options)
|
|
200
|
+
|
|
201
|
+
# If we have a paused fiber and we're not trying to resume (after: false),
|
|
202
|
+
# this is an idempotent call on an already-paused transition. Just return true.
|
|
203
|
+
return true if @paused_fiber&.alive? && !options[:after]
|
|
204
|
+
|
|
205
|
+
# Always use fibers for compatibility with existing pause/resume functionality
|
|
206
|
+
# The fiber argument can still be used to explicitly control fiber usage
|
|
207
|
+
pausable_options = options.key?(:fiber) ? { fiber: options[:fiber] } : {}
|
|
208
|
+
|
|
209
|
+
# Check if we're resuming from a pause
|
|
210
|
+
if @paused_fiber&.alive? && options[:after]
|
|
211
|
+
# Resume the paused fiber
|
|
212
|
+
# Don't reset @success when resuming - preserve the state from the pause
|
|
213
|
+
# Store the block for later execution
|
|
214
|
+
@continuation_block = block if block_given?
|
|
215
|
+
halted = pausable(pausable_options) { true }
|
|
216
|
+
else
|
|
217
|
+
@success = false
|
|
218
|
+
# For normal execution (not pause/resume), default to success
|
|
219
|
+
# The action block will override this if needed
|
|
220
|
+
halted = pausable(pausable_options) { before(options[:after], &block) } if options[:before]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# After callbacks are only run if:
|
|
224
|
+
# * An around callback didn't halt after yielding OR the run failed
|
|
225
|
+
# * They're enabled or the run didn't succeed
|
|
226
|
+
after if (!(@before_run && halted) || !@success) && (options[:after] || !@success)
|
|
227
|
+
|
|
228
|
+
@before_run
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Transitions the current value of the state to that specified by the
|
|
232
|
+
# transition. Once the state is persisted, it cannot be persisted again
|
|
233
|
+
# until this transition is reset.
|
|
234
|
+
#
|
|
235
|
+
# == Example
|
|
236
|
+
#
|
|
237
|
+
# class Vehicle
|
|
238
|
+
# state_machine do
|
|
239
|
+
# event :ignite do
|
|
240
|
+
# transition :parked => :idling
|
|
241
|
+
# end
|
|
242
|
+
# end
|
|
243
|
+
# end
|
|
244
|
+
#
|
|
245
|
+
# vehicle = Vehicle.new
|
|
246
|
+
# transition = StateMachines::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
|
|
247
|
+
# transition.persist
|
|
248
|
+
#
|
|
249
|
+
# vehicle.state # => 'idling'
|
|
250
|
+
def persist
|
|
251
|
+
return if @persisted
|
|
252
|
+
|
|
253
|
+
machine.write(object, :state, to)
|
|
254
|
+
@persisted = true
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Rolls back changes made to the object's state via this transition. This
|
|
258
|
+
# will revert the state back to the +from+ value.
|
|
259
|
+
#
|
|
260
|
+
# == Example
|
|
261
|
+
#
|
|
262
|
+
# class Vehicle
|
|
263
|
+
# state_machine :initial => :parked do
|
|
264
|
+
# event :ignite do
|
|
265
|
+
# transition :parked => :idling
|
|
266
|
+
# end
|
|
267
|
+
# end
|
|
268
|
+
# end
|
|
269
|
+
#
|
|
270
|
+
# vehicle = Vehicle.new # => #<Vehicle:0xb7b7f568 @state="parked">
|
|
271
|
+
# transition = StateMachines::Transition.new(vehicle, Vehicle.state_machine, :ignite, :parked, :idling)
|
|
272
|
+
#
|
|
273
|
+
# # Persist the new state
|
|
274
|
+
# vehicle.state # => "parked"
|
|
275
|
+
# transition.persist
|
|
276
|
+
# vehicle.state # => "idling"
|
|
277
|
+
#
|
|
278
|
+
# # Roll back to the original state
|
|
279
|
+
# transition.rollback
|
|
280
|
+
# vehicle.state # => "parked"
|
|
281
|
+
def rollback
|
|
282
|
+
reset
|
|
283
|
+
machine.write(object, :state, from)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Resets any tracking of which callbacks have already been run and whether
|
|
287
|
+
# the state has already been persisted
|
|
288
|
+
def reset
|
|
289
|
+
@before_run = @persisted = @after_run = false
|
|
290
|
+
@paused_fiber = nil
|
|
291
|
+
@resuming = false
|
|
292
|
+
@continuation_block = nil
|
|
293
|
+
@fiber_thread_storage = nil
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Determines equality of transitions by testing whether the object, states,
|
|
297
|
+
# and event involved in the transition are equal
|
|
298
|
+
def ==(other)
|
|
299
|
+
other.instance_of?(self.class) &&
|
|
300
|
+
other.object == object &&
|
|
301
|
+
other.machine == machine &&
|
|
302
|
+
other.from_name == from_name &&
|
|
303
|
+
other.to_name == to_name &&
|
|
304
|
+
other.event == event
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Generates a nicely formatted description of this transitions's contents.
|
|
308
|
+
#
|
|
309
|
+
# For example,
|
|
310
|
+
#
|
|
311
|
+
# transition = StateMachines::Transition.new(object, machine, :ignite, :parked, :idling)
|
|
312
|
+
# transition # => #<StateMachines::Transition attribute=:state event=:ignite from="parked" from_name=:parked to="idling" to_name=:idling>
|
|
313
|
+
def inspect
|
|
314
|
+
"#<#{self.class} #{%w[attribute event from from_name to to_name].map { |attr| "#{attr}=#{send(attr).inspect}" } * ' '}>"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Checks whether this transition is currently paused.
|
|
318
|
+
# Returns true if there is a paused fiber, false otherwise.
|
|
319
|
+
def paused?
|
|
320
|
+
@paused_fiber&.alive? || false
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Checks whether this transition has a paused fiber that can be resumed.
|
|
324
|
+
# Returns true if there is a paused fiber, false otherwise.
|
|
325
|
+
#
|
|
326
|
+
# Note: The actual resuming happens automatically when run_callbacks is called
|
|
327
|
+
# again on a transition with a paused fiber.
|
|
328
|
+
def resumable?
|
|
329
|
+
paused?
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Manually resumes the execution of a previously paused callback.
|
|
333
|
+
# Returns true if the transition was successfully resumed and completed,
|
|
334
|
+
# false if there was no paused fiber, and raises an exception if the
|
|
335
|
+
# transition was halted.
|
|
336
|
+
def resume!(&block)
|
|
337
|
+
return false unless paused?
|
|
338
|
+
|
|
339
|
+
# Store continuation block if provided
|
|
340
|
+
@continuation_block = block if block_given?
|
|
341
|
+
|
|
342
|
+
# Run the pausable block which will resume the fiber
|
|
343
|
+
halted = pausable { true }
|
|
344
|
+
|
|
345
|
+
# Return whether the transition completed successfully
|
|
346
|
+
!halted
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Completes a transition whose before phase has already run by executing
|
|
350
|
+
# its after callbacks, resuming any paused around callbacks first. Used
|
|
351
|
+
# to finish transitions that were stored for deferred completion (e.g.
|
|
352
|
+
# generated mid-action from an event attribute) once their action has
|
|
353
|
+
# succeeded.
|
|
354
|
+
def complete_deferred_after_callbacks
|
|
355
|
+
if paused?
|
|
356
|
+
run_callbacks(after: true)
|
|
357
|
+
else
|
|
358
|
+
after
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
private
|
|
363
|
+
|
|
364
|
+
# Runs a block that may get paused. If the block doesn't pause, then
|
|
365
|
+
# execution will continue as normal. If the block gets paused, then it
|
|
366
|
+
# will take care of switching the execution context when it's resumed.
|
|
367
|
+
#
|
|
368
|
+
# This will return true if the given block halts for a reason other than
|
|
369
|
+
# getting paused.
|
|
370
|
+
#
|
|
371
|
+
# Options:
|
|
372
|
+
# * :fiber - Whether to use fiber-based execution (default: true)
|
|
373
|
+
def pausable(options = {})
|
|
374
|
+
# If fiber is disabled, use simple synchronous execution
|
|
375
|
+
if options[:fiber] == false
|
|
376
|
+
halted = !catch(:halt) do
|
|
377
|
+
yield
|
|
378
|
+
true
|
|
379
|
+
end
|
|
380
|
+
return halted
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
if @paused_fiber
|
|
384
|
+
# Resume the paused fiber
|
|
385
|
+
@resuming = true
|
|
386
|
+
begin
|
|
387
|
+
result = @paused_fiber.resume
|
|
388
|
+
rescue StandardError => e
|
|
389
|
+
# Clean up on exception
|
|
390
|
+
@resuming = false
|
|
391
|
+
@paused_fiber = nil
|
|
392
|
+
raise e
|
|
393
|
+
end
|
|
394
|
+
@resuming = false
|
|
395
|
+
|
|
396
|
+
result_value = unwrap_fiber_result(result)
|
|
397
|
+
|
|
398
|
+
# Check if fiber is still alive after resume
|
|
399
|
+
if @paused_fiber.alive?
|
|
400
|
+
# Still paused, keep the fiber
|
|
401
|
+
true
|
|
402
|
+
else
|
|
403
|
+
# Fiber completed
|
|
404
|
+
@paused_fiber = nil
|
|
405
|
+
result_value == :halted
|
|
406
|
+
end
|
|
407
|
+
else
|
|
408
|
+
# Capture current fiber's Thread.current storage to preserve object identity
|
|
409
|
+
# This is needed for compatibility but has limitations with dynamic assignments
|
|
410
|
+
parent_fiber_locals = Thread.current.keys.each_with_object({}) do |key, storage|
|
|
411
|
+
storage[key] = Thread.current[key]
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
# Create a new fiber to run the block
|
|
415
|
+
fiber = Fiber.new do
|
|
416
|
+
# Restore parent's Thread.current storage with exact same object references
|
|
417
|
+
parent_fiber_locals.each do |key, value|
|
|
418
|
+
Thread.current[key] = value
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Mark that we're inside a pausable fiber
|
|
422
|
+
Fiber.current.extend(StateMachines::PausableFiber)
|
|
423
|
+
Fiber.current.state_machine_fiber_pausable = true
|
|
424
|
+
begin
|
|
425
|
+
halted = !catch(:halt) do
|
|
426
|
+
yield
|
|
427
|
+
true
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Export the final thread storage state along with the result
|
|
431
|
+
thread_storage = Thread.current.keys.each_with_object({}) do |key, storage|
|
|
432
|
+
storage[key] = Thread.current[key]
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
[halted ? :halted : :completed, thread_storage]
|
|
436
|
+
rescue StandardError => e
|
|
437
|
+
# Store the exception for re-raising
|
|
438
|
+
[:error, e]
|
|
439
|
+
ensure
|
|
440
|
+
# Clean up the flag
|
|
441
|
+
Fiber.current.state_machine_fiber_pausable = false
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Run the fiber
|
|
446
|
+
result = fiber.resume
|
|
447
|
+
|
|
448
|
+
result_value = unwrap_fiber_result(result)
|
|
449
|
+
|
|
450
|
+
# Save if paused
|
|
451
|
+
if fiber.alive?
|
|
452
|
+
@paused_fiber = fiber
|
|
453
|
+
# Return true to indicate paused (treated as halted for flow control)
|
|
454
|
+
true
|
|
455
|
+
else
|
|
456
|
+
# Fiber completed, return whether it was halted
|
|
457
|
+
result_value == :halted
|
|
458
|
+
end
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Unwraps a result returned from a pausable fiber, re-raising any exception
|
|
463
|
+
# captured inside the fiber and importing its exported thread storage
|
|
464
|
+
def unwrap_fiber_result(result)
|
|
465
|
+
return result unless result.is_a?(Array)
|
|
466
|
+
|
|
467
|
+
if result[0] == :error
|
|
468
|
+
@paused_fiber = nil
|
|
469
|
+
raise result[1]
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
@fiber_thread_storage = result[1] if result.length == 2 && result[1].is_a?(Hash)
|
|
473
|
+
result[0]
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Pauses the current callback execution. This should only occur within
|
|
477
|
+
# around callbacks when the remainder of the callback will be executed at
|
|
478
|
+
# a later point in time.
|
|
479
|
+
def pause
|
|
480
|
+
# Don't pause if we're in the middle of resuming
|
|
481
|
+
return if @resuming
|
|
482
|
+
|
|
483
|
+
# Only yield if we're actually inside a fiber created by pausable
|
|
484
|
+
# We use a module extension to track this
|
|
485
|
+
current_fiber = Fiber.current
|
|
486
|
+
return unless current_fiber.respond_to?(:state_machine_fiber_pausable) && current_fiber.state_machine_fiber_pausable
|
|
487
|
+
|
|
488
|
+
Fiber.yield
|
|
489
|
+
|
|
490
|
+
# When we resume from the pause, execute the continuation block if present
|
|
491
|
+
return unless @continuation_block && !@result
|
|
492
|
+
|
|
493
|
+
action = { success: true }.merge(@continuation_block.call)
|
|
494
|
+
@result = action[:result]
|
|
495
|
+
@success = action[:success]
|
|
496
|
+
@continuation_block = nil
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Runs the machine's +before+ callbacks for this transition. Only
|
|
500
|
+
# callbacks that are configured to match the event, from state, and to
|
|
501
|
+
# state will be invoked.
|
|
502
|
+
#
|
|
503
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
|
504
|
+
# is reset.
|
|
505
|
+
def before(complete = true, index = 0, &block)
|
|
506
|
+
return if @before_run
|
|
507
|
+
|
|
508
|
+
callback = machine.callbacks[:before][index]
|
|
509
|
+
|
|
510
|
+
if callback
|
|
511
|
+
# Check if callback matches this transition using branch
|
|
512
|
+
if callback.branch.matches?(object, context)
|
|
513
|
+
if callback.type == :around
|
|
514
|
+
# Around callback: need to handle recursively. Execution only gets
|
|
515
|
+
# paused if:
|
|
516
|
+
# * The block fails and the callback doesn't run on failures OR
|
|
517
|
+
# * The block succeeds, but after callbacks are disabled (in which
|
|
518
|
+
# case a continuation is stored for later execution)
|
|
519
|
+
callback.call(object, context, self) do
|
|
520
|
+
before(complete, index + 1, &block)
|
|
521
|
+
|
|
522
|
+
pause if @success && !complete
|
|
523
|
+
|
|
524
|
+
# If the block failed (success is false), we should halt
|
|
525
|
+
# the around callback from continuing
|
|
526
|
+
throw :halt unless @success
|
|
527
|
+
end
|
|
528
|
+
else
|
|
529
|
+
# Normal before callback
|
|
530
|
+
callback.call(object, context, self)
|
|
531
|
+
# Continue with next callback
|
|
532
|
+
before(complete, index + 1, &block)
|
|
533
|
+
end
|
|
534
|
+
else
|
|
535
|
+
# Skip to next callback if it doesn't match
|
|
536
|
+
before(complete, index + 1, &block)
|
|
537
|
+
end
|
|
538
|
+
else
|
|
539
|
+
# No more callbacks, execute the action block if at the end
|
|
540
|
+
if block_given?
|
|
541
|
+
action = { success: true }.merge(yield)
|
|
542
|
+
@result = action[:result]
|
|
543
|
+
@success = action[:success]
|
|
544
|
+
else
|
|
545
|
+
# No action block provided, default to success
|
|
546
|
+
@success = true
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
@before_run = true
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Runs the machine's +after+ callbacks for this transition. Only
|
|
554
|
+
# callbacks that are configured to match the event, from state, and to
|
|
555
|
+
# state will be invoked.
|
|
556
|
+
#
|
|
557
|
+
# Once the callbacks are run, they cannot be run again until this transition
|
|
558
|
+
# is reset.
|
|
559
|
+
#
|
|
560
|
+
# == Halting
|
|
561
|
+
#
|
|
562
|
+
# If any callback throws a <tt>:halt</tt> exception, it will be caught
|
|
563
|
+
# and the callback chain will be automatically stopped. However, this
|
|
564
|
+
# exception will not bubble up to the caller since +after+ callbacks
|
|
565
|
+
# should never halt the execution of a +perform+.
|
|
566
|
+
def after
|
|
567
|
+
return if @after_run
|
|
568
|
+
|
|
569
|
+
# Restore the fiber's thread storage to ensure consistency
|
|
570
|
+
# This preserves Thread.current state from before/around callbacks to after callbacks
|
|
571
|
+
@fiber_thread_storage.each { |key, value| Thread.current[key] = value } if @fiber_thread_storage
|
|
572
|
+
|
|
573
|
+
catch(:halt) do
|
|
574
|
+
type = @success ? :after : :failure
|
|
575
|
+
machine.callbacks[type].each { |callback| callback.call(object, context, self) }
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
@after_run = true
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
# Gets a hash of the context defining this unique transition (including
|
|
582
|
+
# event, from state, and to state).
|
|
583
|
+
#
|
|
584
|
+
# == Example
|
|
585
|
+
#
|
|
586
|
+
# machine = StateMachine.new(Vehicle)
|
|
587
|
+
# transition = StateMachines::Transition.new(Vehicle.new, machine, :ignite, :parked, :idling)
|
|
588
|
+
# transition.context # => {:on => :ignite, :from => :parked, :to => :idling}
|
|
589
|
+
def context
|
|
590
|
+
@context ||= { on: event, from: from_name, to: to_name }
|
|
591
|
+
end
|
|
592
|
+
end
|
|
593
|
+
end
|