Xstate: Notify "when all invoked services are done"

Created on 8 Feb 2019  路  5Comments  路  Source: davidkpiano/xstate

Is there a way to wait till all invoked services have reached their final state before moving on to the next state?
In the example below I'd like all childMachines be in their final state before moving on to the success state. Putting target: "success" into the onDone configuration doesn't do the job as the first response will move to success ignoring all other responses that are still in flight.

states: {
    pending: {
      invoke: [
        {
          id: "topTenProjects",
          src: topTenProjects,
          onDone: {
            // target: "success",
            actions: assign((ctx, event) =>
              produce(ctx, draft => {
                draft.topTenProjects = event.data
              })
            )
          }
        },
        {
          id: "fundingTypes",
          src: fundingTypes,
          onDone: {
            // target: "success",
            actions: assign((ctx, event) =>
              produce(ctx, draft => {
                draft.fundingTypes = event.data
              })
            )
          }
        }
      ]
    },
    success: {}
  }

_Originally posted by @RainerAtSpirit in https://github.com/davidkpiano/xstate/issues/321#issuecomment-461802330_

documentation question

All 5 comments

Copying the reply here:

I haven't seen anything in SCXML that would notify "when all invoked services are done" although you can accomplish this with "when all parallel states are done" - xstate.js.org/docs/guides/final.html#parallel-states

That might be a bit verbose, but it's for good reason - parallel states can have invoked services on each of them, but they can also be normal states without invoked services.

You can create a helper function that cuts down on the extra config, something like:

states: {
  pending: {
    ...parallelServices(svc1, svc2, svc3),
    onDone: { /* ... */ }
  }
}

I was able to implement that feature in userland by adding an invokeCount and a transient transition.
Would it be possible that xstate support this scenario OOTB?

export const serviceMachine = Machine<
  IServiceMachineContext,
  IServiceMachineSchema,
  EventObject
>({
  id: "serviceMachine",
  initial: "pending",
  context: {
    invokeCount: 0,
    topTenProjects: undefined,
    fundingTypes: undefined
  },
  states: {
    pending: {
      on: {
        "": [
          {
            target: "final",
           cond: (ctx, event) => ctx.invokeCount === 2
          }
        ]
      },
      invoke: [
        {
          id: "topTenProjects",
          src: topTenProjects,
          onDone: {
            actions: assign((ctx, event) =>
              produce(ctx, draft => {
                draft.topTenProjects = event.data
                draft.invokeCount = ctx.invokeCount + 1
              })
            )
          }
        },
        {
          id: "fundingTypes",
          src: fundingTypes,
          onDone: {
            actions: assign((ctx, event) =>
              produce(ctx, draft => {
                draft.fundingTypes = event.data
                draft.invokeCount = ctx.invokeCount + 1
              })
            )
          }
        }
      ]
    },
    final: {}
  }
})

Would it be possible that xstate support this scenario OOTB?

As much as I'd love to, every single API design decision must be fully 1-1 compatible with SCXML: https://www.w3.org/TR/scxml/ . Remember: XState is _nothing new invented_.

I would tackle this a different way:

pending: {
  invoke: {
    src: (ctx, event) => Promise.all([
      /* all services */
    ]),
    onDone: 'final'
  }
}

Sounds fair. Thanks for sharing a different approach.

After reading up a bit on invoke your suggestion of using parallel machines with a single invoke per state is favorable to any "workarounds" to make invoke arrays work the way I'd expect them to work.
@davidkpiano Would it make sense to add that limitation the invoke array documentation?

For sake of completeness here's the above refactored example.

export const serviceMachine = Machine<
  IServiceMachineContext,
  IServiceMachineSchema,
  EventObject
>(
  {
    id: "serviceMachine",
    initial: "pending",
    context: {
      topTenProjects: undefined,
      fundingTypes: undefined
    },
    states: {
      pending: {
        type: "parallel",
        states: {
          topTenProjects: {
            initial: "loading",
            states: {
              loading: {
                invoke: {
                  src: "topTenProjects",
                  onDone: {
                    target: "done",
                    actions: assign((ctx: IServiceMachineContext, event) =>
                      produce(ctx, draft => {
                        draft.topTenProjects = event.data
                      })
                    )
                  }
                }
              },
              done: {
                type: "final"
              }
            }
          },
          fundingTypes: {
            initial: "loading",
            states: {
              loading: {
                invoke: {
                  src: "fundingTypes",
                  onDone: {
                    target: "done",
                    actions: assign((ctx: IServiceMachineContext, event) =>
                      produce(ctx, draft => {
                        draft.fundingTypes = event.data
                      })
                    )
                  }
                }
              },
              done: {
                type: "final"
              }
            }
          }
        },
        onDone: {
          target: "final"
        }
      },
      final: {}
    }
  },
  {
    services: {
      topTenProjects,
      fundingTypes
    }
  }
)
Was this page helpful?
0 / 5 - 0 ratings