Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transition on 'final' states not picked up by transitionListener #251

Closed
laurentpierson opened this issue Nov 23, 2018 · 3 comments
Closed

Comments

@laurentpierson
Copy link

laurentpierson commented Nov 23, 2018

In the project I'm working on, we are using the transition listener to be notified of all transitions so we can store them in a history.

The problem is that when an event triggers a transition to a final state, the transition listener skips that final state and only mentions the state specified by onDone in the parent FSM.

Here's an example reproducing the behaviour:

describe('missing transitions when using onDone', () => {

    const nestedParallelFsm: MachineConfig<DefaultContext, any, EventObject> = {
        key: 'nestedFsm',
        initial: 'B',
        states: {
            B: {
                on: {
                    C_EVENT: 'C'
                }
            },
            C: {
                type: 'final'
            }
        }
    };

    const parentFsm = Machine({
        key: 'parentFsm',
        initial: 'A',
        states: {
            A: {
                onDone: {
                    target: 'D'
                },
                ...nestedParallelFsm
            },
            D: {}
        }
    });

    describe('missing transitions when using onDone', () => {
        it('missing transitions when using onDone', () => {
            const interpreter = interpret(parentFsm);
            const transitionListener = (state: State<DefaultContext, EventObject>, event: EventObject) => {
                console.log(`Event "${event.type}" is triggering transition to state ${JSON.stringify(state.value)}`);
            };
            interpreter.onTransition(transitionListener);
            interpreter.start(parentFsm.initialState);
            interpreter.send({type: 'C_EVENT'});
            interpreter.stop();
        });
    });
});

The output of this test is the following:

Event "xstate.init" is triggering transition to state {"A":"B"}
Event "C_EVENT" is triggering transition to state "D"

The transition listener is capturing C_EVENT triggering a transition to state D without any mention of state C. Is it possible to be more granular and mention the transition from B to C and then from C to `D?

@davidkpiano
Copy link
Member

davidkpiano commented Nov 23, 2018

This is the intended behavior:

When the state machine enters the <final> child of a <state> element, the SCXML Processor must generate the event done.state.id after completion of the <onentry> elements, where id is the id of the parent state.

https://www.w3.org/TR/scxml/#final

And the (pseudocode) algorithm has this:

internalQueue.enqueue(new Event("done.state." + parent.id, s.donedata))

That internalQueue indicates that the done.state.* event is a raised event, which will trigger a microstep:

[Definition: A microstep consists of the execution of the transitions in an optimal enabled transition set.]

[Definition: A macrostep is a series of one or more microsteps ending in a configuration where the internal event queue is empty and no transitions are enabled by NULL. ]

In other words, the transition from the done.state.* event (onDone in this case) is immediately taken within the same step, instead of being two discrete steps.


With that said, I've encountered this "problem" before, and for debugging/logging purposes etc., it might be useful to introduce a new .microsteps property on the State object:

nextState.value;
// 'D'
nextState.microsteps;
// [
//   State({
//     value: { A: 'C' },
//     events: ['done.state.A'],  // raised events
//     event: { type: 'C_EVENT' } // triggering event
//   },
//   State({
//     value: 'D',
//     events: [],                     // no more raised events
//     event: { type: 'done.state.A' } // triggering event
//   }
// ]

Which would represent all the intermediate states traversed in the macrostep in order to get from, e.g., { A: 'B' } to 'D'.

On the interpreter, we can also introduce .onMicrostep() to capture intermediate states/events in the same way:

service.onMicrostep(state => {
  console.log(state.value);
});

service.send('C_EVENT');
// => { A: 'C' }
// => 'D'

Would those be helpful? CC. @mogsie for more info on what we want to capture for debugging purposes.

@laurentpierson
Copy link
Author

Indeed I understand it's only useful for debugging & logging purposes.

Yes, adding microsteps would solve our issue in this case 👍

@davidkpiano
Copy link
Member

Related: #692

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants