javascriptparallel-processingstate-managementstatechartxstate

Resolving Multiple Promises with Parallel state (XState)


I have an xstate state machine that has 3 states, idle, waitingForA and waitingForB. waitingForB, in turn is a type of parallel state. I would like to return to initial state of 'idle' on waitingForB is done. How do I accomplish this?

idle: {
  id: 'initialState'
},
waitingForA: {
  invoke: { /*Promise*/ },
  onDone: { target: 'idle' },
  onError: { alert(); }
},
waitingForB: {
  type: 'parallel',
  states: {
    waitingForC: {
      invoke: { /*Promise*/ },
      onDone: {
        target: 'success',
        actions: assign({ CreturnCode: (context, event) => event.data, })
      }
    },
    waitingForD: {
      invoke: { /*Promise*/ },
      onDone: {
        target: 'success',
        actions: assign({ DreturnCode: (context, event) => event.data, })
      }
    },
    success: {
      // here, I would like ot go back to initial state of 'idle'
      // based on both CreturnCode and DreturnCode 
    }
  }
}

Solution

  • I assume you want to transition out of the parallel state waitingForB into the idle state once both promise C and D are done.

    There are two ways to accomplish this behavior:

    1. Using Promise.all() to wrap both promises in the all() function.

    Using Promise.all() (mdn docs), you can get rid of the parallel state altogether. Instead, you would only have a normal state called sth. like waiting For C and D that invokes both promises at once with Promise.all([promiseC, promiseD]).

    This approach can be used if you don't need to differentiate between individual errors that each promise might create. Error handling is performed for both promises in one onError event.

    2. Transitioning both child states of the parallel state into their own final state.

    In your case, the parallel state has two nested states which each invoke a promise. When the promise of a nested state is resolved, you can use the onDone event to transition the nested state to a final state. Once both nested states have transitioned to their respective final state, an onDone event is automatically raised for the parallel state. There you can define a transition to the idle state of the parent machine.

    The second approach lets you handle the errors of each promise individually which might be preferable if you want to give more specific feedback in the UI for each error.

    const dummyFetch = function (fakeResolveValue, delayInMs) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          if (Math.random() <= 0.01) {
            reject('some error')
          }
          resolve(fakeResolveValue)
    
        }, delayInMs)
      })
    }
    
    waitingForB: {
      type: 'parallel',
      states: {
        waitingForC: {
          initial: 'loading',
          states: {
            loading: {
              invoke: {
                "id": "getStuffC",
                "src": (context, event) => dummyFetch('C', 1000),
                onDone: {
                  target: 'complete',
                  "actions": (ctx, ev) => {
                    console.log('C done')
                  },
                  actions: assign({ CreturnCode: (context, event) => event.data, })
                },
                onError: {
                  actions: send('handle fetch C error')
                }
              },
            },
            complete: {
              type: "final",
            },
          }
        },
        waitingForD: {
          initial: 'loading',
          states: {
            loading: {
              invoke: {
                "id": "getStuffD",
                "src": (context, event) => dummyFetch('D', 5000),
                onDone: {
                  target: 'complete',
                  "actions": (ctx, ev) => {
                    console.log('D done')
                  }
                },
                onError: {
                  actions: send('handle fetch D error')
                }
              }
            },
            complete: {
              type: "final",
            },
          },
        },
      },
      // once both promises C and D are complete
      // the onDone event transitions to the `idle` state
      onDone: {
        target: "idle",
      },
    }
    

    Here is an example by Matt Pocock using the second approach.