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

Bug: data not passed to parentFsm 'onDone' when using parallel FSM #335

Closed
laurentpierson opened this issue Jan 28, 2019 · 4 comments
Closed
Labels

Comments

@laurentpierson
Copy link

laurentpierson commented Jan 28, 2019

Bug or feature request?

Bug

Description:

As explained by @davidkpiano in #224, it is possible to pass data to a parent FSM by putting a data property in the final state of the child FSM. The content of that data property can then be accessed in the parent FSM's onDone transition.

This is working as expected for non-parallel FSMs but not with parallel FSMs where no data is received in parent FSM.

Link to reproduction or proof-of-concept:

Successful unit test with non-parallel FSM:

describe('passing data using fsm & onDone', () => {

    const nestedParallelFsm: MachineConfig<DefaultContext, any, EventObject> = {
        key: 'nestedFsm',
        initial: 'C',
        states: {
            C: {
                on: {
                    D_EVENT: 'D'
                }
            },
            D: {
                type: 'final',
                data: {status: 'success'}
            }
        }
    };

    const parentFsm = Machine({
        key: 'parentFsm',
        initial: 'A',
        states: {
            A: {
                onDone: [
                    {
                        target: 'B', cond: (_, e) => {
                            console.log('event during condition check:', JSON.stringify(e));
                            assert.strictEqual(e.data.status, 'success');
                            return e.status === 'success';
                        }
                    }
                ],
                ...nestedParallelFsm
            },
            B: {}
        }
    });

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

As you can see if you run the test, the event during condition check in parent FSM contains the data property set in child FSM:

event during condition check: {"type":"done.state.parentFsm.A","data":{"status":"success"}}

Now the failing unit test when using parallel FSMs:

describe('passing data using parallel fsm & onDone', () => {

    const nestedParallelFsm: MachineConfig<DefaultContext, any, EventObject> = {
        key: 'nestedParallelFsm',
        type: 'parallel',
        states: {
            parallel1: {
                initial: 'C1',
                states: {
                    C1: {
                        on: {
                            D1_EVENT: 'D1'
                        }
                    },
                    D1: {
                        type: 'final',
                        data: {status: 'success'}
                    }
                }
            },
            parallel2: {
                initial: 'C2',
                states: {
                    C2: {
                        on: {
                            D2_EVENT: 'D2'
                        }
                    },
                    D2: {
                        type: 'final',
                        data: {status: 'success'}
                    }
                }
            }
        }
    };

    const parentFsm = Machine({
        key: 'parentFsm',
        initial: 'A',
        states: {
            A: {
                onDone: [
                    {
                        target: 'B', cond: (_, e) => {
                            console.log('event during condition check:', JSON.stringify(e));
                            assert.strictEqual(e.data.status, 'success');
                            return e.status === 'success';
                        }
                    }
                ],
                ...nestedParallelFsm
            },
            B: {}
        }
    });

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

The test will fail because the event doesn't contain the data property:

event during condition check: {"type":"done.state.parentFsm.A"}

An additional question follows: how do you plan on providing data coming from a parallel FSM? Will data be an array in such case? Indeed, parallel FSMs (in this case, parallel1 and parallel2) could return different data which might be interesting to the parent FSM.

@jens-vc
Copy link

jens-vc commented Feb 21, 2019

I am facing the same problem: for parallel machines, the metadata of a final state is not available onDone.

@laurentpierson
Copy link
Author

Do you plan on fixing this bug? We plan on using xstate for a Production application soon and this bug is preventing us from using parallel FSMs.

@davidkpiano
Copy link
Member

@laurentpierson Yes, I'm prioritizing this.

@davidkpiano
Copy link
Member

Since we don't want to make assumptions on what a "done" event would return for parallel states (what would this look like to you?), you can do this already by using assign and keeping an aggregation of data from each individual parallel state once they're each done:

import { Machine, interpret, assign } from "xstate";

const parallelDoneMachine = Machine({
  id: "parallelDone",
  type: "parallel",
  context: {
    allData: []
  },
  states: {
    one: {
      initial: "foo",
      states: {
        foo: {
          on: { ONE: "bar" }
        },
        bar: {
          type: "final",
          data: "one-bar"
        }
      },
      onDone: {
        actions: assign({
          allData: (ctx, e) => ctx.allData.concat(e.data)
        })
      }
    },
    two: {
      initial: "foo",
      states: {
        foo: {
          on: { TWO: "bar" }
        },
        bar: {
          type: "final",
          data: "two-bar"
        }
      },
      onDone: {
        actions: assign({
          allData: (ctx, e) => ctx.allData.concat(e.data)
        })
      }
    }
  },
  onDone: {
    actions: ctx => {
      console.log(ctx);
    }
  }
});

const parallelDoneService = interpret(parallelDoneMachine);

parallelDoneService.start();
parallelDoneService.send("ONE");
parallelDoneService.send("TWO");
// => ['one-bar', 'two-bar']

See this pattern in action here: https://codesandbox.io/s/p52nxqzw4m

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

No branches or pull requests

3 participants