1

Hellow, I have below Json structure, which is provided as a payload in the UpdateOrders action. In the effect, I would like to iterate over the reservations and orders, call the this.orderApiService.updateOrder service and dispatch a UpdateOrderProgress action. In the UpdateOrderProgress action I would like to provide the numberOfReservationsUpdated and the totalReservations

const reservationOrders = [
    {
        reservationNumber: '22763883',
        orders: [
            {
                orderId: 'O12341',
                amount: 25
            },
            {
                orderId: 'O45321',
                amount: 50
            }
        ]
    },
    {
        reservationNumber: '42345719',
        orders: [
            {
                orderId: 'O12343',
                amount: 75
            }
        ]
    }
];

I have the following effect to achieve this, but unfortunately, this effect does not work and throws an exception.

@Effect()
updateOrders$ = this.actions$.pipe(
    ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
    filter((action) => !!action.reservationOrders),
    exhaustMap((action) => {
        return combineLatest(action.reservationOrders.map((x, index) => {
            const totalReservations = action.reservationOrders.length;
            const numberOfReservationsUpdated = index + 1;
            return combineLatest(x.orders.map((order) => {
                const orderUpdateRequest: OrderUpdateRequest = {
                    orderId: order.orderId,
                    amount: order.amount
                };
                return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
                    switchMap(() => [new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)]),
                    catchError((message: string) => of(console.info(message))),
                );
            }))
        }))
    })
);

How can I achieve this? Which RxJs operators am I missing?

Gullit
  • 33
  • 4

1 Answers1

0

Instead of using combineLatest, you may switch to using a combination of merge and mergeMap to acheive the effect you're looking for.

Below is a representation of your problem statement -

  1. An action triggers an observable
  2. This needs to trigger multiple observables
  3. Each of those observables need to then trigger some action (UPDATE_ACTION)

One way to achieve this is as follows -

const subj = new Subject<number[]>();

const getData$ = (index) => {
  return of({
    index,
    value: 'Some value for ' + index,
  }).pipe(delay(index*1000));
};

const source = subj.pipe(
  filter((x) => !!x),
  exhaustMap((records: number[]) => {
    const dataRequests = records.map((r) => getData$(r));
    return merge(dataRequests);
  }),
  mergeMap((obs) => obs)
);

source.subscribe(console.log);

subj.next([3,1,1,4]); // Each of the value in array simulates a call to an endpoint that'll take i*1000 ms to complete

// OUTPUT - 
// {index: 1, value: "Some value for 1"}
// {index: 1, value: "Some value for 1"}
// {index: 3, value: "Some value for 3"}
// {index: 4, value: "Some value for 4"}

Given the above explaination, your code needs to be changed to something like -

const getOrderRequest$ = (order: OrderUpdateRequest, numberOfReservationsUpdated, totalReservations) => {
    const orderUpdateRequest: OrderUpdateRequest = {
        orderId: order.orderId,
        amount: order.amount
    };
    return this.orderApiService.updateOrder(orderUpdateRequest).pipe(
        switchMap(() => new UpdateOrderProgress(numberOfReservationsUpdated, totalReservations)),
        catchError((message: string) => of(console.info(message))),
    );
}

updateOrders$ = this.actions$.pipe(
    ofType<UpdateOrders>(UpdateOrdersActionType.UPDATE_ORDERS),
    filter((action) => !!action.reservationOrders),
    exhaustMap((action) => {

        const reservationOrders = action.reservationOrders;
        const totalLen = reservationOrders.length
        const allRequests = []
        reservationOrders.forEach((r, index) => {
            r.orders.forEach(order => {
                const req = getOrderRequest$(order, index + 1, totalLen);
                allRequests.push(req);
            });
        });

        return merge(allRequests)
    }), 
    mergeMap(obs=> obs)
);

Side Note - While the nested observables in your example may work, there are chances that you'll be seeing wrong results due to inherent nature of http calls taking unknown amount of time to complete. Meaning, the way you've written it, there are chances that you can see in some cases that numberOfReservationsUpdated as not an exact indicative of actual number of reservations updated.

A better approach would be to handle the state information in your reducer. Basically, pass the reservationNumber in the UPDATE action payload and let the reducer decide how many requests are pending completion. This will be an accurate representation of the state of the system. Also, it will simplify your logic in @effect to a single nested observable rather than multiple nesting.

Pankaj
  • 538
  • 4
  • 13
  • thank you very much for the thorough explanation. I like your suggestion to handle the state information in the reducer. However, I don't know how to achieve this and how to keep which reservations are processed. Would it be possible to provide an example? – Gullit Nov 22 '21 at 21:00
  • It'll be difficult for me to create the entire datastore that you have. Better create a stackblitz sample that people can help you with. – Pankaj Nov 23 '21 at 07:15