finite_machine 0.11.3 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +564 -569
  4. data/Rakefile +5 -1
  5. data/benchmarks/memory_profile.rb +11 -0
  6. data/benchmarks/memory_usage.rb +16 -9
  7. data/finite_machine.gemspec +10 -3
  8. data/lib/finite_machine.rb +34 -46
  9. data/lib/finite_machine/async_call.rb +5 -21
  10. data/lib/finite_machine/callable.rb +4 -4
  11. data/lib/finite_machine/catchable.rb +4 -2
  12. data/lib/finite_machine/choice_merger.rb +19 -19
  13. data/lib/finite_machine/const.rb +16 -0
  14. data/lib/finite_machine/definition.rb +2 -2
  15. data/lib/finite_machine/dsl.rb +66 -149
  16. data/lib/finite_machine/env.rb +4 -2
  17. data/lib/finite_machine/event_definition.rb +7 -15
  18. data/lib/finite_machine/{events_chain.rb → events_map.rb} +39 -51
  19. data/lib/finite_machine/hook_event.rb +60 -61
  20. data/lib/finite_machine/hooks.rb +44 -36
  21. data/lib/finite_machine/listener.rb +2 -2
  22. data/lib/finite_machine/logger.rb +5 -4
  23. data/lib/finite_machine/message_queue.rb +39 -30
  24. data/lib/finite_machine/observer.rb +55 -37
  25. data/lib/finite_machine/safety.rb +12 -10
  26. data/lib/finite_machine/state_definition.rb +3 -5
  27. data/lib/finite_machine/state_machine.rb +83 -64
  28. data/lib/finite_machine/state_parser.rb +51 -79
  29. data/lib/finite_machine/subscribers.rb +1 -1
  30. data/lib/finite_machine/threadable.rb +3 -1
  31. data/lib/finite_machine/transition.rb +30 -31
  32. data/lib/finite_machine/transition_builder.rb +23 -32
  33. data/lib/finite_machine/transition_event.rb +12 -11
  34. data/lib/finite_machine/two_phase_lock.rb +3 -1
  35. data/lib/finite_machine/undefined_transition.rb +5 -6
  36. data/lib/finite_machine/version.rb +2 -2
  37. data/spec/integration/system_spec.rb +36 -38
  38. data/spec/performance/benchmark_spec.rb +13 -21
  39. data/spec/unit/alias_target_spec.rb +22 -41
  40. data/spec/unit/async_callbacks_spec.rb +8 -13
  41. data/spec/unit/auto_methods_spec.rb +44 -0
  42. data/spec/unit/callable/call_spec.rb +1 -3
  43. data/spec/unit/callbacks_spec.rb +372 -463
  44. data/spec/unit/can_spec.rb +13 -23
  45. data/spec/unit/cancel_callbacks_spec.rb +46 -0
  46. data/spec/unit/choice_spec.rb +105 -141
  47. data/spec/unit/define_spec.rb +31 -31
  48. data/spec/unit/definition_spec.rb +24 -41
  49. data/spec/unit/event_names_spec.rb +6 -10
  50. data/spec/unit/events_map/add_spec.rb +23 -0
  51. data/spec/unit/events_map/choice_transition_spec.rb +25 -0
  52. data/spec/unit/events_map/clear_spec.rb +13 -0
  53. data/spec/unit/events_map/events_spec.rb +16 -0
  54. data/spec/unit/events_map/inspect_spec.rb +22 -0
  55. data/spec/unit/{events_chain → events_map}/match_transition_spec.rb +12 -14
  56. data/spec/unit/{events_chain → events_map}/move_to_spec.rb +14 -17
  57. data/spec/unit/events_map/states_for_spec.rb +17 -0
  58. data/spec/unit/events_spec.rb +91 -160
  59. data/spec/unit/handlers_spec.rb +34 -66
  60. data/spec/unit/hook_event/any_state_or_event_spec.rb +13 -0
  61. data/spec/unit/hook_event/build_spec.rb +1 -3
  62. data/spec/unit/hook_event/eql_spec.rb +1 -3
  63. data/spec/unit/hook_event/initialize_spec.rb +2 -4
  64. data/spec/unit/hook_event/notify_spec.rb +2 -4
  65. data/spec/unit/hooks/clear_spec.rb +1 -1
  66. data/spec/unit/hooks/{call_spec.rb → find_spec.rb} +4 -9
  67. data/spec/unit/hooks/inspect_spec.rb +16 -8
  68. data/spec/unit/hooks/register_spec.rb +4 -9
  69. data/spec/unit/if_unless_spec.rb +76 -115
  70. data/spec/unit/initial_spec.rb +50 -82
  71. data/spec/unit/inspect_spec.rb +14 -9
  72. data/spec/unit/is_spec.rb +12 -18
  73. data/spec/unit/log_transitions_spec.rb +4 -10
  74. data/spec/unit/logger_spec.rb +1 -3
  75. data/spec/unit/{event_queue_spec.rb → message_queue_spec.rb} +15 -8
  76. data/spec/unit/new_spec.rb +50 -0
  77. data/spec/unit/respond_to_spec.rb +2 -6
  78. data/spec/unit/state_parser/parse_spec.rb +9 -12
  79. data/spec/unit/states_spec.rb +12 -18
  80. data/spec/unit/subscribers_spec.rb +1 -3
  81. data/spec/unit/target_spec.rb +60 -93
  82. data/spec/unit/terminated_spec.rb +15 -25
  83. data/spec/unit/transition/check_conditions_spec.rb +16 -15
  84. data/spec/unit/transition/inspect_spec.rb +6 -6
  85. data/spec/unit/transition/matches_spec.rb +5 -7
  86. data/spec/unit/transition/states_spec.rb +5 -7
  87. data/spec/unit/transition/to_state_spec.rb +5 -13
  88. data/spec/unit/trigger_spec.rb +5 -9
  89. data/spec/unit/undefined_transition/eql_spec.rb +1 -3
  90. metadata +86 -49
  91. data/.gitignore +0 -18
  92. data/.rspec +0 -5
  93. data/.travis.yml +0 -27
  94. data/Gemfile +0 -16
  95. data/assets/finite_machine_logo.png +0 -0
  96. data/lib/finite_machine/async_proxy.rb +0 -55
  97. data/spec/unit/async_events_spec.rb +0 -107
  98. data/spec/unit/events_chain/add_spec.rb +0 -25
  99. data/spec/unit/events_chain/cancel_transitions_spec.rb +0 -22
  100. data/spec/unit/events_chain/choice_transition_spec.rb +0 -28
  101. data/spec/unit/events_chain/clear_spec.rb +0 -15
  102. data/spec/unit/events_chain/events_spec.rb +0 -18
  103. data/spec/unit/events_chain/inspect_spec.rb +0 -24
  104. data/spec/unit/events_chain/states_for_spec.rb +0 -17
  105. data/spec/unit/hook_event/infer_default_name_spec.rb +0 -13
  106. data/spec/unit/state_parser/inspect_spec.rb +0 -25
@@ -1,92 +1,93 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
3
+ require 'concurrent/map'
4
+
5
+ require_relative 'hook_event'
2
6
 
3
7
  module FiniteMachine
4
8
  # A class reponsible for registering callbacks
5
9
  class Hooks
6
- include Threadable
7
-
8
- attr_threadsafe :collection
10
+ attr_reader :hooks_map
9
11
 
10
- # Initialize a collection of hoooks
12
+ # Initialize a hooks_map of hooks
11
13
  #
12
14
  # @example
13
15
  # Hoosk.new(machine)
14
16
  #
15
17
  # @api public
16
18
  def initialize
17
- @collection = Hash.new do |events_hash, event_type|
18
- events_hash[event_type] = Hash.new do |state_hash, name|
19
+ @hooks_map = Concurrent::Map.new do |events_hash, hook_event|
20
+ events_hash[hook_event] = Concurrent::Map.new do |state_hash, name|
19
21
  state_hash[name] = []
20
22
  end
21
23
  end
22
24
  end
23
25
 
24
- # Register callback
26
+ # Finds all hooks for the event type
25
27
  #
26
- # @param [String] event_type
27
- # @param [String] name
28
- # @param [Proc] callback
28
+ # @param [Symbol] name
29
29
  #
30
30
  # @example
31
- # hooks.register HookEvent::Enter, :green do ... end
31
+ # hooks[HookEvent::Enter][:go] # => [-> { }]
32
32
  #
33
- # @example
34
- # hooks.register HookEvent::Before, :any do ... end
35
- #
36
- # @return [Hash]
33
+ # @return [Array[Transition]]
34
+ # the transitions matching event name
37
35
  #
38
36
  # @api public
39
- def register(event_type, name, callback)
40
- collection[event_type][name] << callback
37
+ def find(name)
38
+ @hooks_map[name]
41
39
  end
40
+ alias [] find
42
41
 
43
- # Unregister callback
42
+ # Register callback
44
43
  #
45
- # @param [String] event_type
44
+ # @param [String] hook_event
46
45
  # @param [String] name
47
46
  # @param [Proc] callback
48
47
  #
49
48
  # @example
50
- # hooks.unregister HookEvent::Enter, :green do ... end
49
+ # hooks.register HookEvent::Enter, :green do ... end
50
+ #
51
+ # @example
52
+ # hooks.register HookEvent::Before, any_state do ... end
51
53
  #
52
54
  # @return [Hash]
53
55
  #
54
56
  # @api public
55
- def unregister(event_type, name, callback)
56
- callbacks = collection[event_type][name]
57
- callbacks.delete(callback)
57
+ def register(hook_event, name, callback)
58
+ @hooks_map[hook_event][name] << callback
58
59
  end
59
60
 
60
- # Return all hooks matching event and state
61
+ # Unregister callback
61
62
  #
62
- # @param [String] event_type
63
- # @param [String] event_state
64
- # @param [Event] event
63
+ # @param [String] hook_event
64
+ # @param [String] name
65
+ # @param [Proc] callback
65
66
  #
66
67
  # @example
67
- # hooks.call(HookEvent::Enter, :green, Event.new)
68
+ # hooks.unregister HookEvent::Enter, :green do ... end
68
69
  #
69
70
  # @return [Hash]
70
71
  #
71
72
  # @api public
72
- def call(event_type, event_state, &block)
73
- collection[event_type][event_state].each(&block)
73
+ def unregister(hook_event, name, callback)
74
+ @hooks_map[hook_event][name].delete(callback)
74
75
  end
75
76
 
76
- # Check if collection has any elements
77
+ # Check if hooks_map has any elements
77
78
  #
78
79
  # @return [Boolean]
79
80
  #
80
81
  # @api public
81
82
  def empty?
82
- collection.empty?
83
+ @hooks_map.empty?
83
84
  end
84
85
 
85
86
  # Remove all callbacks
86
87
  #
87
88
  # @api public
88
89
  def clear
89
- collection.clear
90
+ @hooks_map.clear
90
91
  end
91
92
 
92
93
  # String representation
@@ -95,7 +96,14 @@ module FiniteMachine
95
96
  #
96
97
  # @api public
97
98
  def to_s
98
- self.inspect
99
+ hash = {}
100
+ @hooks_map.each_pair do |hook_event, nested_hash|
101
+ hash[hook_event] = {}
102
+ nested_hash.each_pair do |name, callbacks|
103
+ hash[hook_event][name] = callbacks
104
+ end
105
+ end
106
+ hash.to_s
99
107
  end
100
108
 
101
109
  # String representation
@@ -104,7 +112,7 @@ module FiniteMachine
104
112
  #
105
113
  # @api public
106
114
  def inspect
107
- "<##{self.class}:0x#{object_id.to_s(16)} @collection=#{collection.inspect}>"
115
+ "<##{self.class}:0x#{object_id.to_s(16)} @hooks_map=#{self}>"
108
116
  end
109
117
  end # Hooks
110
118
  end # FiniteMachine
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  module FiniteMachine
4
4
  # A generic listener interface
@@ -24,6 +24,6 @@ module FiniteMachine
24
24
  def call(*args)
25
25
  @on_delivery.call(*args) if @on_delivery
26
26
  end
27
- alias_method :handle_delivery, :call
27
+ alias handle_delivery call
28
28
  end # Listener
29
29
  end # FiniteMachine
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  module FiniteMachine
4
4
  module Logger
@@ -21,21 +21,22 @@ module FiniteMachine
21
21
  end
22
22
 
23
23
  def format_error(error)
24
- message = "#{error.class}: #{error.message}\n\t"
24
+ message = ["#{error.class}: #{error.message}\n\t"]
25
25
  if error.backtrace
26
26
  message << "occured at #{error.backtrace.join("\n\t")}"
27
27
  else
28
28
  message << "EMPTY BACKTRACE\n\t"
29
29
  end
30
+ message.join
30
31
  end
31
32
 
32
33
  def report_transition(name, from, to, *args)
33
- message = "Transition: @event=#{name} "
34
+ message = ["Transition: @event=#{name} "]
34
35
  unless args.empty?
35
36
  message << "@with=[#{args.join(',')}] "
36
37
  end
37
38
  message << "#{from} -> #{to}"
38
- info(message)
39
+ info(message.join)
39
40
  end
40
41
  end # Logger
41
42
  end # FiniteMachine
@@ -1,4 +1,7 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'listener'
4
+ require 'thread'
2
5
 
3
6
  module FiniteMachine
4
7
  # Allows for storage of asynchronous messages such as events
@@ -8,15 +11,15 @@ module FiniteMachine
8
11
  #
9
12
  # @api private
10
13
  class MessageQueue
11
- include Threadable
12
-
13
- # Initialize an event queue
14
+ # Initialize an event queue in separate thread
14
15
  #
15
16
  # @example
16
17
  # MessageQueue.new
17
18
  #
18
19
  # @api public
19
20
  def initialize
21
+ @not_empty = ConditionVariable.new
22
+ @mutex = Mutex.new
20
23
  @queue = Queue.new
21
24
  @dead = false
22
25
  @listeners = []
@@ -28,7 +31,13 @@ module FiniteMachine
28
31
  # @api private
29
32
  def start
30
33
  return if running?
34
+ @mutex.synchronize { spawn_thread }
35
+ end
31
36
 
37
+ # Spawn new background thread
38
+ #
39
+ # @api private
40
+ def spawn_thread
32
41
  @thread = Thread.new do
33
42
  Thread.current.abort_on_exception = true
34
43
  process_events
@@ -39,16 +48,7 @@ module FiniteMachine
39
48
  !@thread.nil? && alive?
40
49
  end
41
50
 
42
- # Retrieve the next event
43
- #
44
- # @return [AsyncCall]
45
- #
46
- # @api private
47
- def next_event
48
- sync_shared { @queue.pop }
49
- end
50
-
51
- # Add asynchronous event to the event queue
51
+ # Add asynchronous event to the event queue to process
52
52
  #
53
53
  # @example
54
54
  # event_queue << AsyncCall.build(...)
@@ -59,21 +59,21 @@ module FiniteMachine
59
59
  #
60
60
  # @api public
61
61
  def <<(event)
62
- sync_exclusive do
62
+ @mutex.synchronize do
63
63
  if @dead
64
64
  discard_message(event)
65
65
  else
66
66
  @queue << event
67
+ @not_empty.signal
67
68
  end
68
69
  end
69
- self
70
70
  end
71
71
 
72
72
  # Add listener to the queue to receive messages
73
73
  #
74
74
  # @api public
75
75
  def subscribe(*args, &block)
76
- sync_exclusive do
76
+ @mutex.synchronize do
77
77
  listener = Listener.new(*args)
78
78
  listener.on_delivery(&block)
79
79
  @listeners << listener
@@ -87,7 +87,7 @@ module FiniteMachine
87
87
  #
88
88
  # @api public
89
89
  def empty?
90
- sync_shared { @queue.empty? }
90
+ @mutex.synchronize { @queue.empty? }
91
91
  end
92
92
 
93
93
  # Check if the event queue is alive
@@ -99,7 +99,7 @@ module FiniteMachine
99
99
  #
100
100
  # @api public
101
101
  def alive?
102
- sync_shared { !@dead }
102
+ @mutex.synchronize { !@dead }
103
103
  end
104
104
 
105
105
  # Join the event queue from current thread
@@ -129,10 +129,12 @@ module FiniteMachine
129
129
  fail EventQueueDeadError, 'event queue already dead' if @dead
130
130
 
131
131
  queue = []
132
- sync_exclusive do
132
+ @mutex.synchronize do
133
+ @dead = true
134
+ @not_empty.broadcast
135
+
133
136
  queue = @queue
134
137
  @queue.clear
135
- @dead = true
136
138
  end
137
139
  while !queue.empty?
138
140
  discard_message(queue.pop)
@@ -149,24 +151,24 @@ module FiniteMachine
149
151
  #
150
152
  # @api public
151
153
  def size
152
- sync_shared { @queue.size }
154
+ @mutex.synchronize { @queue.size }
153
155
  end
154
156
 
155
157
  def inspect
156
- "#<#{self.class}:#{object_id.to_s(16)} @size=#{size}, @dead=#{@dead}>"
158
+ @mutex.synchronize do
159
+ "#<#{self.class}:#{object_id.to_s(16)} @size=#{size}, @dead=#{@dead}>"
160
+ end
157
161
  end
158
162
 
159
163
  private
160
164
 
161
165
  # Notify consumers about process event
162
166
  #
163
- # @param [FiniteMachine::AsyncCall] event
167
+ # @param [AsyncCall] event
164
168
  #
165
169
  # @api private
166
170
  def notify_listeners(event)
167
- sync_shared do
168
- @listeners.each { |listener| listener.handle_delivery(event) }
169
- end
171
+ @listeners.each { |listener| listener.handle_delivery(event) }
170
172
  end
171
173
 
172
174
  # Process all the events
@@ -176,9 +178,16 @@ module FiniteMachine
176
178
  # @api private
177
179
  def process_events
178
180
  until @dead
179
- event = next_event
180
- notify_listeners(event)
181
- event.dispatch
181
+ @mutex.synchronize do
182
+ while @queue.empty?
183
+ break if @dead
184
+ @not_empty.wait(@mutex)
185
+ end
186
+ event = @queue.pop
187
+ break unless event
188
+ notify_listeners(event)
189
+ event.dispatch
190
+ end
182
191
  end
183
192
  rescue Exception => ex
184
193
  Logger.error "Error while running event: #{Logger.format_error(ex)}"
@@ -1,18 +1,37 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
- require 'finite_machine/hooks'
3
+ require_relative 'async_call'
4
+ require_relative 'callable'
5
+ require_relative 'hook_event'
6
+ require_relative 'hooks'
7
+ require_relative 'message_queue'
8
+ require_relative 'safety'
9
+ require_relative 'transition_event'
4
10
 
5
11
  module FiniteMachine
6
12
  # A class responsible for observing state changes
7
- class Observer
8
- include Threadable
13
+ class Observer < GenericDSL
9
14
  include Safety
10
15
 
16
+ # Clean up callback queue
17
+ #
18
+ # @api private
19
+ def self.cleanup_callback_queue
20
+ proc do
21
+ begin
22
+ if callback_queue.alive?
23
+ callback_queue.shutdown
24
+ end
25
+ rescue MessageQueueDeadError
26
+ end
27
+ end
28
+ end
29
+
11
30
  # The current state machine
12
- attr_threadsafe :machine
31
+ attr_reader :machine
13
32
 
14
33
  # The hooks to trigger around the transition lifecycle.
15
- attr_threadsafe :hooks
34
+ attr_reader :hooks
16
35
 
17
36
  # Initialize an Observer
18
37
  #
@@ -21,12 +40,15 @@ module FiniteMachine
21
40
  #
22
41
  # @api public
23
42
  def initialize(machine)
24
- @machine = machine
25
- @hooks = FiniteMachine::Hooks.new
26
- @callback_queue = MessageQueue.new
43
+ @machine = machine
44
+ @hooks = Hooks.new
27
45
 
28
46
  @machine.subscribe(self)
29
- ObjectSpace.define_finalizer(self, self.class.cleanup(@callback_queue))
47
+ ObjectSpace.define_finalizer(self, self.class.cleanup_callback_queue)
48
+ end
49
+
50
+ def callback_queue
51
+ @callback_queue ||= MessageQueue.new
30
52
  end
31
53
 
32
54
  # Evaluate in current context
@@ -49,7 +71,7 @@ module FiniteMachine
49
71
  def on(hook_type, state_or_event_name = nil, async = nil, &callback)
50
72
  sync_exclusive do
51
73
  if state_or_event_name.nil?
52
- state_or_event_name = HookEvent.infer_default_name(hook_type)
74
+ state_or_event_name = HookEvent.any_state_or_event(hook_type)
53
75
  end
54
76
  async = false if async.nil?
55
77
  ensure_valid_callback_name!(hook_type, state_or_event_name)
@@ -124,8 +146,9 @@ module FiniteMachine
124
146
  def emit(event, *data)
125
147
  sync_exclusive do
126
148
  [event.type].each do |hook_type|
127
- [event.name, ANY_STATE, ANY_EVENT].each do |event_name|
128
- hooks.call(hook_type, event_name) do |hook|
149
+ any_state_or_event = HookEvent.any_state_or_event(hook_type)
150
+ [any_state_or_event, event.name].each do |event_name|
151
+ hooks[hook_type][event_name].each do |hook|
129
152
  handle_callback(hook, event, *data)
130
153
  off(hook_type, event_name, &hook) if hook.is_a?(Once)
131
154
  end
@@ -134,6 +157,19 @@ module FiniteMachine
134
157
  end
135
158
  end
136
159
 
160
+ # Cancel the current event
161
+ #
162
+ # This should be called inside a on_before or on_exit callbacks
163
+ # to prevent event transition.
164
+ #
165
+ # @param [String] msg
166
+ # the message used for failure
167
+ #
168
+ # @api public
169
+ def cancel_event(msg = nil)
170
+ raise CallbackError.new(msg)
171
+ end
172
+
137
173
  private
138
174
 
139
175
  # Handle callback and decide if run synchronously or asynchronously
@@ -148,20 +184,14 @@ module FiniteMachine
148
184
  #
149
185
  # @api private
150
186
  def handle_callback(hook, event, *data)
151
- to = machine.events_chain.move_to(event.event_name, event.from, *data)
152
- trans_event = TransitionEvent.new(event, to)
187
+ to = machine.events_map.move_to(event.event_name, event.from, *data)
188
+ trans_event = TransitionEvent.new(event.event_name, event.from, to)
153
189
  callable = create_callable(hook)
154
190
 
155
191
  if hook.is_a?(Async)
156
192
  defer(callable, trans_event, *data)
157
- result = nil
158
193
  else
159
- result = callable.call(trans_event, *data)
160
- end
161
-
162
- if result == CANCELLED
163
- hooks.clear
164
- machine.events_chain.cancel_transitions(event.event_name)
194
+ callable.(trans_event, *data)
165
195
  end
166
196
  end
167
197
 
@@ -170,8 +200,8 @@ module FiniteMachine
170
200
  # @api private
171
201
  def defer(callable, trans_event, *data)
172
202
  async_call = AsyncCall.new(machine, callable, trans_event, *data)
173
- @callback_queue.start unless @callback_queue.running?
174
- @callback_queue << async_call
203
+ callback_queue.start unless callback_queue.running?
204
+ callback_queue << async_call
175
205
  end
176
206
 
177
207
  # Create callable instance
@@ -191,7 +221,7 @@ module FiniteMachine
191
221
  #
192
222
  # @api private
193
223
  def callback_names
194
- machine.states + machine.event_names + [ANY_EVENT, ANY_STATE]
224
+ machine.states + machine.events + [ANY_EVENT, ANY_STATE]
195
225
  end
196
226
 
197
227
  # Forward the message to observer
@@ -225,17 +255,5 @@ module FiniteMachine
225
255
  *_, callback_name = *method_name.to_s.match(/^(\w*?on_\w+?)_(\w+)$/)
226
256
  callback_name && callback_names.include?(:"#{callback_name}")
227
257
  end
228
-
229
- # Clean up callback queue
230
- #
231
- # @api private
232
- def self.cleanup(queue)
233
- proc do
234
- begin
235
- queue && queue.shutdown
236
- rescue MessageQueueDeadError
237
- end
238
- end
239
- end
240
258
  end # Observer
241
259
  end # FiniteMachine