2020from ..event_data import TriggerData
2121from ..exceptions import InvalidDefinition
2222from ..exceptions import TransitionNotAllowed
23- from ..invoke import InvokeManager
2423from ..orderedset import OrderedSet
2524from ..state import HistoryState
2625from ..state import State
2726from ..transition import Transition
2827
2928if TYPE_CHECKING :
29+ from ..invoke import InvokeManager
3030 from ..statemachine import StateChart
3131
3232logger = logging .getLogger (__name__ )
@@ -95,8 +95,7 @@ def __init__(self, sm: "StateChart"):
9595 self .running = True
9696 self ._processing = Lock ()
9797 self ._cache : Dict = {} # Cache for _get_args_kwargs results
98- self .invoke_manager = InvokeManager (self )
99- self .states_to_invoke : "OrderedSet[State]" = OrderedSet ()
98+ self ._invoke : "InvokeManager | None" = None # set by StateChart if needed
10099
101100 def empty (self ): # pragma: no cover
102101 return self .external_queue .is_empty ()
@@ -138,33 +137,6 @@ def cancel_event(self, send_id: str):
138137 """Cancel the event with the given send_id."""
139138 self .external_queue .remove (send_id )
140139
141- def _forward_to_target (self , trigger_data : TriggerData ):
142- """Forward an event to a cross-session target instead of processing it.
143-
144- Called by the processing loop when ``trigger_data.forward_target`` is set.
145- This supports delayed cross-session sends: the event sits on the child's
146- queue with a delay, and when it expires the processing loop forwards it
147- to the named target.
148- """
149- target = trigger_data .forward_target
150- event_name = str (trigger_data .event )
151- if target in ("#_parent" , "parent" ):
152- parent_sm = getattr (self .sm , "_parent_sm" , None )
153- if parent_sm is not None :
154- child_invokeid = getattr (self .sm , "_invokeid" , None )
155- parent_sm .send (
156- event_name ,
157- * trigger_data .args ,
158- invokeid = child_invokeid ,
159- ** trigger_data .kwargs ,
160- )
161- else :
162- self .sm .send ("error.communication" , internal = True )
163- elif target == "#_child" :
164- self .invoke_manager .send_to_child (event_name , ** trigger_data .kwargs )
165- else :
166- logger .warning ("Unknown forward_target: %s" , target )
167-
168140 def _on_error_handler (self ) -> "Callable[[Exception], None] | None" :
169141 """Return a per-block error handler, or ``None``.
170142
@@ -528,12 +500,8 @@ def _exit_states(
528500 for info in ordered_states :
529501 args , kwargs = self ._get_args_kwargs (info .transition , trigger_data )
530502
531- # Cancel invocations for states being exited
532- if info .state is not None and getattr (info .state , "invocations" , None ):
533- self .invoke_manager .cancel_for_state (info .state )
534-
535- # Remove from states_to_invoke if we exit before invoking
536- self .states_to_invoke .discard (info .state )
503+ if self ._invoke is not None :
504+ self ._invoke .on_state_exiting (info .state )
537505
538506 # Execute `onexit` handlers — same per-block error isolation as onentry.
539507 if info .state is not None : # pragma: no branch
@@ -602,16 +570,16 @@ def _add_state_to_configuration(self, target: State):
602570 if not self .sm .atomic_configuration_update :
603571 self .sm .configuration |= {target }
604572
605- def _terminate_machine (self ):
606- """SCXML termination: exit all active states in exit order, firing onexit handlers.
573+ def _terminate_child_session (self ):
574+ """Exit all active states ( firing onexit handlers) and stop the engine .
607575
608- Per SCXML spec, when a top-level final state is reached, the machine
609- terminates. All active states have their ``onexit`` handlers fired
610- (in reverse document order), but the final configuration is preserved
611- so that observers can see which final state was reached.
576+ Called when a child session (one with ``_invoke_session``) reaches a
577+ top-level final state. Per SCXML spec, all active states have their
578+ ``onexit`` handlers fired (in reverse document order), but the final
579+ configuration is preserved so that observers can see which final state
580+ was reached.
612581 """
613582 on_error = self ._on_error_handler ()
614- # Sort active states by document_order (reverse) for exit order
615583 active_states = sorted (
616584 self .sm .configuration ,
617585 key = lambda s : s .document_order ,
@@ -621,24 +589,24 @@ def _terminate_machine(self):
621589 event_data = EventData (trigger_data = trigger_data , transition = None )
622590 args , kwargs = event_data .args , event_data .extended_kwargs
623591 for state in active_states :
624- if getattr ( state , "invocations" , None ) :
625- self .invoke_manager . cancel_for_state (state )
592+ if self . _invoke is not None :
593+ self ._invoke . on_state_exiting (state )
626594 self .sm ._callbacks .call (state .exit .key , * args , on_error = on_error , ** kwargs )
627- # Note: we intentionally do NOT clear configuration — the final state
628- # must remain visible for assertions and done.invoke reporting.
629- self .invoke_manager .cancel_all ()
595+ if self ._invoke is not None :
596+ self ._invoke .on_terminate ()
630597 self .running = False
631598
632599 def _handle_final_state (self , target : State , on_entry_result : list ):
633600 """Handle final state entry: queue done events. No direct callback dispatch."""
634601 if target .parent is None :
635- if getattr (self .sm , "_parent_sm " , None ) is not None :
602+ if getattr (self .sm , "_invoke_session " , None ) is not None :
636603 # Child session: fire onexit on all active states before terminating
637604 # so that #_parent sends in <onexit> of final states are delivered
638- self ._terminate_machine ()
605+ self ._terminate_child_session ()
639606 else :
640607 self .running = False
641- self .invoke_manager .cancel_all ()
608+ if self ._invoke is not None :
609+ self ._invoke .on_terminate ()
642610 else :
643611 parent = target .parent
644612 grandparent = parent .parent
@@ -729,8 +697,8 @@ def _enter_states( # noqa: C901
729697 )
730698
731699 # Track states with invocations for post-macrostep spawning
732- if getattr ( target , "invocations" , None ) :
733- self .states_to_invoke . add (target )
700+ if self . _invoke is not None :
701+ self ._invoke . on_state_entered (target )
734702
735703 # Handle final states
736704 if target .final :
0 commit comments