Xstate: Raising events for a parallel state causes no transition to occur in initial state.

Created on 20 Jun 2018  路  3Comments  路  Source: davidkpiano/xstate

Bug or feature request?

Bug

Description:

I'm on xstate 3.3.2, given this code:

const {
  Machine,
  actions: { raise }
} = require("xstate");

const t = (state, event) => {
  state = testMachine.transition(state, event);
  console.log(`[${event}]: ${JSON.stringify(state.value)}`);
  return state;
};

const testMachine = Machine({
  strict: true,
  parallel: true,
  states: {
    OUTER1: {
      initial: "C",
      states: {
        A: {
          onEntry: [raise("TURN_OFF")],
          on: {
            EVENT_OUTER1_B: "B",
            EVENT_OUTER1_C: "C"
          }
        },
        B: {
          onEntry: [raise("TURN_ON")],
          on: {
            EVENT_OUTER1_A: "A",
            EVENT_OUTER1_C: "C"
          }
        },
        C: {
          onEntry: [raise("CLEAR")],
          on: {
            EVENT_OUTER1_A: "A",
            EVENT_OUTER1_B: "B"
          }
        }
      }
    },
    OUTER2: {
      parallel: true,
      states: {
        INNER1: {
          initial: "ON",
          states: {
            OFF: {
              on: {
                TURN_ON: "ON"
              }
            },
            ON: {
              on: {
                CLEAR: "OFF"
              }
            }
          }
        },
        INNER2: {
          initial: "OFF",
          states: {
            OFF: {
              on: {
                TURN_ON: "ON"
              }
            },
            ON: {
              on: {
                TURN_OFF: "OFF"
              }
            }
          }
        }
      }
    }
  }
});

let state = testMachine.initialState;
console.log(`INITIAL: ${JSON.stringify(state.value)}`);
state = t(state, "EVENT_OUTER1_B");
state = t(state, "EVENT_OUTER1_A");
state = t(state, "EVENT_OUTER1_C");

(Bug) Expected result:

INITIAL: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_B]: {"OUTER1":"B","OUTER2":{"INNER1":"ON","INNER2":"ON"}}
[EVENT_OUTER1_A]: {"OUTER1":"A","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_C]: {"OUTER1":"C","OUTER2":{"INNER1":"OFF","INNER2":"OFF"}}

(Bug) Actual result:

The OUTER1 state sticks on C. However the events raised by the onEntry are fired off, as shown by the OUTER2 state.

After EVENT_OUTER1_C, the OUTER2 state is mismatched from the expected result due to the C state not having the EVENT_OUTER1_C event.

INITIAL: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_B]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"ON"}}
[EVENT_OUTER1_A]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_C]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}

Commenting out either the INNER1 or INNER2 state stops the OUTER1 state from sticking on C.
Alternatively, commenting out B's onEntry also has the same effect for the OUTER1 state.

Link to reproduction or proof-of-concept:

CodePen Debugger

bug

Most helpful comment

Great catch!

Just to explain the statechart quickly to @davidkpiano:

  • OUTER1 and OUTER2 are two regions.
  • OUTER1 has A B and C, that have events to go to any of those three (e.g. A defines EVENT_OUTER1_B to go to B.
  • OUTER2 is itself a parallel state (directly nested in another parallel state, although I tried moving it to a substate of another region but it didn't change the behaviour)
  • OUTER2 has two regions INNER1 and INNER2, each with substates ON and OFF
  • ON and OFF are controlled via raised events from the onEntry of the A B C states

So controlling the machine using OUTER_A; _B and _C the bug seems to be observing some _side effects_ of _entering B_ (Entering B raises TURN_ON which should cause both OFF states to go to ON) but without actually entering B itself.

Removing the raised events causes the normal state transitions to happen.

  1. Shouldn't the initial state be OUTER1: C, INNER1: OFF, INNER2: OFF? The initial state of OUTER1.C should be raising the CLEAR event, which should cause INNER1.ON to go to INNER1.OFF. Maybe a separate bug.

  2. There is some strange things going on. It seems to be stuck in the initial state of the substate somehow. If I change OUTER1's initial state to A, then it 'sticks' to A instead of C.

All 3 comments

Great catch!

Just to explain the statechart quickly to @davidkpiano:

  • OUTER1 and OUTER2 are two regions.
  • OUTER1 has A B and C, that have events to go to any of those three (e.g. A defines EVENT_OUTER1_B to go to B.
  • OUTER2 is itself a parallel state (directly nested in another parallel state, although I tried moving it to a substate of another region but it didn't change the behaviour)
  • OUTER2 has two regions INNER1 and INNER2, each with substates ON and OFF
  • ON and OFF are controlled via raised events from the onEntry of the A B C states

So controlling the machine using OUTER_A; _B and _C the bug seems to be observing some _side effects_ of _entering B_ (Entering B raises TURN_ON which should cause both OFF states to go to ON) but without actually entering B itself.

Removing the raised events causes the normal state transitions to happen.

  1. Shouldn't the initial state be OUTER1: C, INNER1: OFF, INNER2: OFF? The initial state of OUTER1.C should be raising the CLEAR event, which should cause INNER1.ON to go to INNER1.OFF. Maybe a separate bug.

  2. There is some strange things going on. It seems to be stuck in the initial state of the substate somehow. If I change OUTER1's initial state to A, then it 'sticks' to A instead of C.

Good point on 1, I was too focussed on the sticking to see that!

Codepen debugger

I found another interesting case with this particular machine.
So with that definition above, but with OUTER1.B's onEntry commented out (to prevent the OUTER1 state from sticking)

const {
  Machine,
  actions: { raise }
} = require("xstate");

const t = (state, event) => {
  state = testMachine.transition(state, event);
  console.log(`[${event}]: ${JSON.stringify(state.value)}`);
  return state;
};

const testMachine = Machine({
  strict: true,
  parallel: true,
  states: {
    OUTER1: {
      initial: "C",
      states: {
        A: {
          onEntry: [raise("TURN_OFF")],
          on: {
            EVENT_OUTER1_B: "B",
            EVENT_OUTER1_C: "C"
          }
        },
        B: {
          // onEntry: [raise("TURN_ON")],
          on: {
            EVENT_OUTER1_A: "A",
            EVENT_OUTER1_C: "C"
          }
        },
        C: {
          onEntry: [raise("CLEAR")],
          on: {
            EVENT_OUTER1_A: "A",
            EVENT_OUTER1_B: "B"
          }
        }
      }
    },
    OUTER2: {
      parallel: true,
      states: {
        INNER1: {
          initial: "ON",
          states: {
            OFF: {
              on: {
                TURN_ON: "ON"
              }
            },
            ON: {
              on: {
                CLEAR: "OFF"
              }
            }
          }
        },
        INNER2: {
          initial: "OFF",
          states: {
            OFF: {
              on: {
                TURN_ON: "ON"
              }
            },
            ON: {
              on: {
                TURN_OFF: "OFF"
              }
            }
          }
        }
      }
    }
  }
});

If we were to perform these transitions:

let state = testMachine.initialState;
console.log(`INITIAL: ${JSON.stringify(state.value)}`);
state = t(state, "EVENT_OUTER1_B");
state = t(state, "TURN_ON");
state = t(state, "EVENT_OUTER1_A");
state = t(state, "EVENT_OUTER1_C");

What happens is the OUTER1 state transitions to B on EVENT_OUTER1_B, but goes back to C on the TURN_ON event. I would expect OUTER1 to stay on B.

Expected result

                                        // INNER1: OFF from point 1.
INITIAL:          {"OUTER1":"C","OUTER2":{"INNER1":"OFF","INNER2":"OFF"}}
[EVENT_OUTER1_B]: {"OUTER1":"B","OUTER2":{"INNER1":"OFF","INNER2":"OFF"}}
[TURN_ON]:        {"OUTER1":"B","OUTER2":{"INNER1":"ON","INNER2":"ON"}}
[EVENT_OUTER1_A]: {"OUTER1":"A","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_C]: {"OUTER1":"C","OUTER2":{"INNER1":"OFF","INNER2":"OFF"}}

Actual result

INITIAL:          {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_B]: {"OUTER1":"B","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[TURN_ON]:        {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"ON"}}
[EVENT_OUTER1_A]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}
[EVENT_OUTER1_C]: {"OUTER1":"C","OUTER2":{"INNER1":"ON","INNER2":"OFF"}}

I'm not sure if this would be classed as a separate bug..

Can you do me a favor and turn this into a PR containing the failing test case? (Should be just a matter of copy-pasting this into the correct test file, probably parallel.test.ts)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

doup picture doup  路  3Comments

pke picture pke  路  3Comments

bradwoods picture bradwoods  路  3Comments

amelon picture amelon  路  3Comments

mattiamanzati picture mattiamanzati  路  3Comments