protofsm: change Receive to use AskEvent and return outbox

In this commit, we modify the Receive method that implements the
actor.ActorBehavior interface to use the AskEvent pattern instead of
the fire-and-forget SendEvent approach. This fundamental change enables
proper event propagation through actor hierarchies when state machines
are composed.

Previously, Receive would send events directly to the events channel
and return a boolean indicating success. This provided no visibility
into what happened during event processing or what events were emitted
as a result. The fire-and-forget nature meant that parent actors had
no way to receive outbox events from nested state machines.

With this change, Receive now delegates to AskEvent and awaits the
Future, returning fn.Result[[]Event] instead of fn.Result[bool]. This
means the actor system can now propagate events upward through the
actor hierarchy. When a state machine is embedded as an actor and
processes a message, any outbox events it emits are returned to the
calling actor for further processing.

The implementation becomes remarkably simple as AskEvent already handles
all the complexity of context cancellation, shutdown detection, and
promise completion. We simply invoke AskEvent with the actor's context
and the incoming event, then await and return the result directly.

This change completes the outbox pattern implementation by ensuring
that state machines used as actors can participate in event propagation,
enabling clean composition of state machines in actor-based systems.
This commit is contained in:
Olaoluwa Osuntokun
2025-11-07 15:51:08 -08:00
parent eb118f8ab6
commit 11d0375eb0

View File

@@ -340,24 +340,25 @@ func (s *StateMachine[Event, Env]) AskEvent(ctx context.Context,
return promise.Future()
}
// Receive processes a message and returns a Result. The provided context is the
// actor's internal context, which can be used to detect actor shutdown
// requests.
// Receive processes a message and returns a Result containing the accumulated
// outbox events from the state machine. The provided context is the actor's
// internal context, which can be used to detect actor shutdown requests.
//
// This method uses the AskEvent pattern to wait for the event to be fully
// processed and collect any outbox events emitted during state transitions.
// This enables the actor system to propagate events from nested state machines
// up through the actor hierarchy.
//
// NOTE: This implements the actor.ActorBehavior interface.
func (s *StateMachine[Event, Env]) Receive(ctx context.Context,
e ActorMessage[Event]) fn.Result[bool] {
e ActorMessage[Event]) fn.Result[[]Event] {
select {
case s.events <- e.Event:
return fn.Ok(true)
// Use AskEvent to process the event and get the outbox events back.
future := s.AskEvent(ctx, e.Event)
case <-ctx.Done():
return fn.Err[bool](ctx.Err())
case <-s.quit:
return fn.Err[bool](ErrStateMachineShutdown)
}
// Await the result which will contain the accumulated outbox events
// from all state transitions triggered by this event.
return future.Await(ctx)
}
// CanHandle returns true if the target message can be routed to the state