onepub-dev/fsm2

InitialState for nested states doesnt seem to work

canastro opened this issue · 8 comments

Given the following state machine
When I transition to a nested state
Then transition to this state's initial state

In reality it the statemachine is currently ignoring the initial state and it just transitions to the root of the nested state.

Example:

Given the following statemachine:

  final machine = StateMachine.create(
    (g) => g
      ..initialState<Start>()
      ..state<Start>((g) => g..on<OnKickStart, Main>())
      ..state<Main>(
        (g) => g
          ..on<OnTickFirst, First>()
          ..on<OnTickSecond, Second>()
          ..coregion<First>(
            (g) => g
              ..initialState<One>()
              ..state<One>((g) => g..on<OnToggle, Two>())
              ..state<Two>((g) => g..on<OnToggle, One>()),
          )
          ..coregion<Second>(
            (g) => g
              ..initialState<Three>()
              ..state<Three>((g) => g..on<OnToggle, Four>())
              ..state<Four>((g) => g..on<OnToggle, Three>()),
          ),
      ),
  );

When: applying a OnKickStart event
Reality: the statemachine transitions to Main->VirtualRoot"
Expected: the statemachine transitions to Main->First->One->VirtualRoot and Main->Second->Three->VirtualRoot.

In this XState visualizer you can see that by triggering "Kickstart" the machine transitions to all initial states of the parallel machines.

In this PR you can see a failing test for this scenario.

Opened the following PR #13 with a failing test case to help out understanding the issue.

Hi, it seems we're hitting the same issue with nested states (no coregions defined). This GH issue suggests it's a bug in fsm2, can you confirm?

I think the problem is the transition attempts to transition to a parent state which isn't allowed. You need to transition or fork to one or more leaf states.

The validator should probably flag the state machine definition as being in error.

A mobile app can be in the foreground or in the background.
When it is in the foreground there are a number of activities allowed/possible.
During one of these activities, the end user may press the home button, moving the app to the background.
On such occasion we may want to gracefully pause or cancel ongoing activities.
I believe an FSM can help.

For example:

final fsm = StateMachine.create((b) => b
  ..initialState<Foreground>()
  ..state<Foreground>((b) => b
    ..initialState<Idle>()
    ..state<Idle>((b) => b
      ..on<StartActivity1, DoingActivity1>()
    )
    ..state<DoingActivity1>((b) => b
      ..on<EndActivity1, Idle>()
    )
    ..on<GoToBackground, Backgrounded>() //are you saying this is not allowed? how would you do it?
  )
  ..state<Backgrounded>((b) => b)
);

the on<GoToBackground... is fine as it targets a leaf state Background.

However, I don't think I would model it that way.
Instead, I would have two (or more) states for each activity. The activities would switch between states based on GoToBackground, GotoForeground events.

.state<Music>((b) => b
 ..initialState<Paused>()
     ..state<Paused>((b) => b
          .. on<OnForeground, Playing>
      ..state<Playing>((b) => b
          .. on<OnBackground, Paused>

You may also want to consider having a separate state machine for each activity as it can make the model easier to understand.

Alternatively, have a look at the doco example of the human life cycle. The living/dead states are closer to your original idea.

Following is an updated state machine where the Backgrounded state contains nest state. I assume that's no longer what you call a "leaf state".

There, you're saying that the ..on<GoToBackground, Backgrounded>() line is no longer valid?

That example is quite close to our app's behaviour. Are you suggesting that we should migrate this fsm design to one where we have "two (or more) states for each activity"?

final fsm = StateMachine.create((b) => b
  ..initialState<Foreground>()
  ..state<Foreground>((b) => b
    ..initialState<Idle>()
    ..state<Idle>((b) => b
      ..on<StartActivity1, DoingActivity1>()
    )
    ..state<DoingActivity1>((b) => b
      ..on<EndActivity1, Idle>()
    )
    ..on<GoToBackground, Backgrounded>() 
  )
  ..state<Backgrounded>((b) => b
    ..initialState<GracefulShutdown>()
    ..state<GracefulShutdown>((b) => b
      ..onEnter(...)
      ..on<GracefulShutdownComplete, Terminated>()
    )
    ..state<Terminated>((b) => b)
  )
);

Also, you wrote earlier:

I think the problem is the transition attempts to transition to a parent state which isn't allowed.

It sounds arbitrary and I can't justify it, could you explain why?
I've seen it too in xstate and they created "final states" which, when transitioned to, transition to their parent state. I haven't found the equivalent in fsm2.

close and stale.
The equivalent of 'final states' (as I understand it) is leaf states. You can only transition to a leaf state, the tree of parent states is then implied by the path to the leaf state.