NgRx effect call API and dispatch action in a loop - rxjs

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?

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 -
An action triggers an observable
This needs to trigger multiple observables
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.

Related

Why is a Promise changing behavior of getValue in RXJS?

I had an issue where adding an extra pipe to subscription to a BehaviorSubject was making the wrong behavior in some tests. Whenever I did const stores = await lastValueFrom(workingStore$); in RXJS 7 or const stores = await workingStore$.toPromise(); in RXJS 6, the value was not what I expected. I reduced the code down to this fiddle: https://jsfiddle.net/Dave_Stein/v7aj6bwy/
You can see on the run without a concatMap, getValue gives 3 values in an array. With concatMap, it will only return the first value.
The same can be observed when I use toPromise in this way:
console.log('here i am', bug);
const promise = workingStore$.toPromise();
from(events).subscribe(events$);
const x = await promise;
console.log('there i am', bug, x);
I get that there is an async behavior going on with concatMap, but I would imagine using toPromise would make RXJS wait for all the events being processed via subscribe to complete before resolving the promise.
In reality my concatMap calls a method that is async and MUST use await based on a library I am using.
Is there some other way to accomplish this? Order of events matters to me which is why I chose concatMap
The solution is at: https://jsfiddle.net/Dave_Stein/nt6Lvc07/.
Rather than trying to subscribe to workingStore$ twice, I can use mergeWith operator in RXJS 7. (There is another way to accomplish this in 6). Using subscribe on the same subject twice is a bad practice that can lead to issues like these apparently.
const { Subject, operators, pipe, BehaviorSubject, from, lastValueFrom } = rxjs;
const { filter, scan, concatMap, mergeWith } = operators;
const run = async (bug) => {
const events$ = new Subject();
const historyChanges$ = new Subject();
const workingStore$ = new BehaviorSubject({});
const scanWorkingStoreMap = {};
events$.pipe(
concatMap((evt => {
return Promise.resolve(evt);
})),
filter((evt) => evt.name === 'history')
).subscribe(historyChanges$);
const newDocs$ = events$
.pipe(
filter((evt) => evt.name === 'new'));
// historyChanges$ is a Subject
historyChanges$
.pipe(
mergeWith(newDocs$),
scan((acc, evt) => {
const { id } = evt;
if (!acc[id]) {
acc[id] = [evt]
} else {
acc[id].push(evt)
}
return acc;
}, scanWorkingStoreMap),
)
.subscribe(workingStore$);
const events = [
{ name: 'new', id: 1},
{ name: 'history', id: 1, data: { a: 1}},
{ name: 'history', id: 1, data: { a: 2}}
]
console.log('here i am', bug);
from(events).subscribe(events$);
console.log('there i am', await lastValueFrom(workingStore$));
}
run();

angular and RxJS/switchMap

Hello I'm new to RxJS and I'm just getting to know operators. I want to show in console next 6 numbers in one-second time interval after button click. I want to reset that counter after next click using switchMap.
I've been trying to do with switchMap, but counter is not reseting.
obsSwitchMap: Observable<any>;
this.obsSwitchMap = of(null).pipe(
switchMap(x => from([1, 2, 3]).pipe(
concatMap(item => of(item).pipe(delay(300)))
)
)
)
onSwitchMapBtnClick() {
this.obsSwitchMap.subscribe(x => {
console.log(x)
})
}
Numbers are displaying independently of each other
Although you want to learn, I think you should learn with the best practices from the start.
And it means you can do it very simply without switchMap :
const newInterval = () => rxjs.timer(0, 1000).pipe(
rxjs.operators.map(nb => new Array(6).fill(nb).map((v, i) => v + i + 1))
);
let subscription;
function resetTimer() {
subscription && subscription.unsubscribe();
subscription = newInterval().subscribe(v => console.log(v));
}
resetTimer();
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.js"></script>
<button onclick="resetTimer()">Reset timer</button>
EDIT
Here is a switchMap example :
const horsemen = [
{ id: 1, name: 'Death' },
{ id: 2, name: 'Famine' },
{ id: 3, name: 'War' },
{ id: 4, name: 'Conquest' },
];
// Fake HTTP call of 1 second
function getHorseman(id) {
return rxjs
.of(horsemen.find(h => h.id === id))
.pipe(rxjs.operators.delay(1000));
}
const query = document.querySelector('input');
const result = document.querySelector('div.result');
// Listen to input
rxjs.fromEvent(query, 'input')
.pipe(
rxjs.operators.map(event => +event.target.value), // Get ID
rxjs.operators.switchMap(id => getHorseman(id)) // Get Horseman
).subscribe(horseman => {
let content;
if (horseman) content = `Horseman = ${horseman.name}`;
else content = `Horseman unknown`;
result.innerText = content;
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.2/rxjs.umd.js"></script>
<input type="text" placeholder="Input an ID here (1-4)">
<div class="result"></div>
I have found simply solution using switchMap. On every click, restart your observable counter and take how many items you want.
const btn = document.querySelector('button');
fromEvent(btn, 'click').pipe(
switchMap((item => interval(1000).pipe(take(6)))),
).subscribe(console.log)

Testing NGRX effect with delay

I want to test an effect that works as follows:
Effect starts if LoadEntriesSucces action was dispatched
It waits for 5 seconds
After 5 seconds passes http request is send
When response arrives, new action is dispatched (depending, whether response was succes or error).
Effect's code looks like this:
#Effect()
continuePollingEntries$ = this.actions$.pipe(
ofType(SubnetBrowserApiActions.SubnetBrowserApiActionTypes.LoadEntriesSucces),
delay(5000),
switchMap(() => {
return this.subnetBrowserService.getSubnetEntries().pipe(
map((entries) => {
return new SubnetBrowserApiActions.LoadEntriesSucces({ entries });
}),
catchError((error) => {
return of(new SubnetBrowserApiActions.LoadEntriesFailure({ error }));
}),
);
}),
);
What I want to test is whether an effect is dispatched after 5 seconds:
it('should dispatch action after 5 seconds', () => {
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
expect(effects.continuePollingEntries$).toBeObservable(expected);
});
However this test does not work for me. Output from test looks like this:
Expected $.length = 0 to equal 3.
Expected $[0] = undefined to equal Object({ frame: 20, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[1] = undefined to equal Object({ frame: 30, notification: Notification({ kind: 'N', value: undefined, error: undefined, hasValue: true }) }).
Expected $[2] = undefined to equal Object({ frame: 50, notification: Notification({ kind: 'N', value: LoadEntriesSucces({ payload: Object({ entries: [ Object({ type: 'type', userText: 'userText', ipAddress: '0.0.0.0' }) ] }), type: '[Subnet Browser API] Load Entries Succes' }), error: undefined, hasValue: true }) }).
What should I do to make this test work?
Like mentioned in another answer, one way to test that effect would be by using the TestScheduler but it can be done in a simpler way.
We can test our asynchronous RxJS code synchronously and deterministically by virtualizing time using the TestScheduler. ASCII marble diagrams provide a visual way for us to represent the behavior of an Observable. We can use them to assert that a particular Observable behaves as expected, as well as to create hot and cold Observables we can use as mocks.
For example, let's unit test the following effect:
effectWithDelay$ = createEffect(() => {
return this.actions$.pipe(
ofType(fromFooActions.doSomething),
delay(5000),
switchMap(({ payload }) => {
const { someData } = payload;
return this.fooService.someMethod(someData).pipe(
map(() => {
return fromFooActions.doSomethingSuccess();
}),
catchError(() => {
return of(fromFooActions.doSomethinfError());
}),
);
}),
);
});
The effect just waits 5 seconds after an initial action, and calls a service which would then dispatch a success or error action. The code to unit test that effect would be the following:
import { TestBed } from "#angular/core/testing";
import { provideMockActions } from "#ngrx/effects/testing";
import { Observable } from "rxjs";
import { TestScheduler } from "rxjs/testing";
import { FooEffects } from "./foo.effects";
import { FooService } from "../services/foo.service";
import * as fromFooActions from "../actions/foo.actions";
// ...
describe("FooEffects", () => {
let actions$: Observable<unknown>;
let testScheduler: TestScheduler; // <-- instance of the test scheduler
let effects: FooEffects;
let fooServiceMock: jasmine.SpyObj<FooService>;
beforeEach(() => {
// Initialize the TestScheduler instance passing a function to
// compare if two objects are equal
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
TestBed.configureTestingModule({
imports: [],
providers: [
FooEffects,
provideMockActions(() => actions$),
// Mock the service so that we can test if it was called
// and if the right data was sent
{
provide: FooService,
useValue: jasmine.createSpyObj("FooService", {
someMethod: jasmine.createSpy(),
}),
},
],
});
effects = TestBed.inject(FooEffects);
fooServiceMock = TestBed.inject(FooService);
});
describe("effectWithDelay$", () => {
it("should dispatch doSomethingSuccess after 5 seconds if success", () => {
const someDataMock = { someData: Math.random() * 100 };
const initialAction = fromFooActions.doSomething(someDataMock);
const expectedAction = fromFooActions.doSomethingSuccess();
testScheduler.run((helpers) => {
// When the code inside this callback is being executed, any operator
// that uses timers/AsyncScheduler (like delay, debounceTime, etc) will
// **automatically** use the TestScheduler instead, so that we have
// "virtual time". You do not need to pass the TestScheduler to them,
// like in the past.
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
const { hot, cold, expectObservable } = helpers;
// Actions // -a-
// Service // -b|
// Results // 5s --c
// Actions
actions$ = hot("-a-", { a: initialAction });
// Service
fooServiceMock.someMethod.and.returnValue(cold("-b|", { b: null }));
// Results
expectObservable(effects.effectWithDelay$).toBe("5s --c", {
c: expectedAction,
});
});
// This needs to be outside of the run() callback
// since it's executed synchronously :O
expect(fooServiceMock.someMethod).toHaveBeenCalled();
expect(fooServiceMock.someMethod).toHaveBeenCalledTimes(1);
expect(fooServiceMock.someMethod).toHaveBeenCalledWith(someDataMock.someData);
});
});
});
Please notice that in the code I'm using expectObservable to test the effect using the "virtual time" from the TestScheduler instance.
you could use the done callback from jasmine
it('should dispatch action after 5 seconds', (done) => {
const resMock = 'resMock';
const entries: SubnetEntry[] = [{
type: 'type',
userText: 'userText',
ipAddress: '0.0.0.0'
}];
const action = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
const completion = new SubnetBrowserApiActions.LoadEntriesSucces({entries});
actions$ = hot('-a', { a: action });
const response = cold('-a', {a: entries});
const expected = cold('- 5s b ', { b: completion });
subnetBrowserService.getSubnetEntries = () => (response);
effects.continuePollingEntries$.subscribe((res)=>{
expect(res).toEqual(resMock);
done()
})
});
The second notation doesn't work with jasmine-marbles, use dashes instead:
const expected = cold('------b ', { b: completion });
You will need to do 3 things
1- Inside your beforeEach, you need to override the internal scheduler of RxJs as follows:
import { async } from 'rxjs/internal/scheduler/async';
import { cold, hot, getTestScheduler } from 'jasmine-marbles';
beforeEach(() => {.....
const testScheduler = getTestScheduler();
async.schedule = (work, delay, state) => testScheduler.schedule(work, delay, state);
})
2- Replace delay, with delayWhen as follows:
delayWhen(_x => (true ? interval(50) : of(undefined)))
3- Use frames, I am not really sure how to use seconds for this, so I used frames. Each frame is 10ms. So for example my delay above is 50ms and my frame is -b, so that is the expected 10 ms + I needed another 50ms so this equals extra 5 frames which was ------b so as follows:
const expected = cold('------b ', { b: outcome });

Reduce returns empty array, however scan does not

Code:
const Rx = require('rxjs')
const data = [
{ name: 'Zachary', age: 21 },
{ name: 'John', age: 20 },
{ name: 'Louise', age: 14 },
{ name: 'Borg', age: 15 }
]
const dataSubj$ = new Rx.Subject()
function getDataStream() {
return dataSubj$.asObservable().startWith(data);
}
getDataStream()
.mergeMap(Rx.Observable.from)
.scan((arr, person) => {
arr.push(person)
return arr
}, [])
.subscribe(val => console.log('val: ', val));
Using .reduce(...) instead of .scan(...) returns an empty array and nothing is printed. The observer of dataSub$ should receive an array.
Why does scan allow elements of data to pass through, but reduce does not?
Note: I am using mergeMap because I will filter the elements of the array before reducing them back into a single array.
scan emits the accumulated value on every source item.
reduce emits only the last accumulated value. It waits until the source Observable is completed and only then emits the accumulated value.
In your case the source Observable, which relies on a subject, never completes. Thus, the reduce would never emit any value.
You may want to apply the reduce on the inner Observable of the mergeMap. For each array, the inner Observable would complete when all the array items are emitted:
const data = [
{ name: 'Zachary', age: 21 },
{ name: 'John', age: 20 },
{ name: 'Louise', age: 14 },
{ name: 'Borg', age: 15 }
]
const dataSubj$ = new Rx.Subject()
function getDataStream() {
return dataSubj$.asObservable().startWith(data);
}
getDataStream()
.mergeMap(arr => Rx.Observable.from(arr)
.reduce((agg, person) => {
agg.push(person)
return agg
}, [])
)
.subscribe(val => console.log('val: ', val));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.6/Rx.js"></script>

RxJS unsubscribe with id

Upon unsubscribe, I need to call a function with an id that was originally sent in the data. What is the correct way of doing that?
It is a nested observable as seen in below snippet.
Observable.create(subscriber => {
const myData = nested.getData
.map(x => x > 1)
.subscribe(subscriber);
return () => {
myData.unsubscribe();
callWithIdThatIsInData({id:123})
};
})
When unsubscribed, I need to callWithIdThatIsInData() with a property what was sent in the nested observable..
Can anyone point me in the right direction please?
A solution is to store your properties which match your filter condition, during the subscription of the nested observable.
Here is an example :
const nested = Rx.Observable.from([1, 2, 3, 4, 5]);
const stream = Rx.Observable.create((subscriber) => {
const saved = [];
const myData = nested.filter(x => x > 1).subscribe(x => {
subscriber.next(x);
saved.push(x);
});
subscriber.complete();
return () => {
myData.unsubscribe();
//Here you have access to your data
console.log(saved);
}
});
stream.subscribe(x => console.log(x));

Resources