statelyai / xstate

Actor-based state management & orchestration for complex app logic.
https://stately.ai/docs
MIT License
27.25k stars 1.26k forks source link

Notify "when all invoked services are done" #359

Closed RainerAtSpirit closed 5 years ago

RainerAtSpirit commented 5 years ago

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

davidkpiano commented 5 years ago

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: { /* ... */ }
  }
}
RainerAtSpirit commented 5 years ago

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: {}
  }
})
davidkpiano commented 5 years ago

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'
  }
}
RainerAtSpirit commented 5 years ago

Sounds fair. Thanks for sharing a different approach.

RainerAtSpirit commented 5 years ago

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
    }
  }
)
NixBiks commented 3 years ago

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'
  }
}

How would you parameterise those promises? Imagine I have parameterised fetchUser and fetchData e.g.. Now I want to use those in a Promise.all. Is that possible?

Andarist commented 3 years ago

Combinators (like Promise.all) are not supported out of the box - you'd need to handle that yourself, for example by just externalizing your promises and calling to them directly.