RxJS - Epic Reads Only 1 Promise - javascript

I have an Epic. I want to pass 2 HTTP Get Requests. They are both Promised based. But it only bring data for the first processed one. THE Epic:
const processorsListEpic = (action$, store, deps) =>
action$.ofType(Type.LIST_ATTEMPT).pipe(
switchMap(() =>
observableFromHttpPromise(
deps.getList(store), // This bring data
deps.getTargets(store) // This doesn't
).pipe(
mergeMap((listResult, targetResult) => {
console.log('Target:', targetResult.data);
console.log('List', listResult.data);
return of(
R.mergeAll(
Actions.ListSuccess(listResult && listResult.data),
Actions.TargetsSuccess(targetResult && targetResult.data)
)
);
}),
catchError(error => of(Actions.ListFailure(error)))
)
)
);
The function observableFromHttpPromise is the following:
// From is from rxjs
export const observableFromHttpPromise = promise => from(promise);
Any Ideas? If I change the order of the requests, the other data are there..

Try this
observableFromHttpPromise(Promise.all(
deps.getList(store),
deps.getTargets(store),
))

Related

Rxjs do something on first emit from multiple subscriptions

Is there a clean way to do something on first emit from multiple subscriptions ?
e.g.:
this.subscription1 = this.service.getData1().subscribe(data => {
this.data1 = data;
console.log('1');
});
this.subscription2 = this.service.getData2().subscribe(data => {
this.data2 = data2;
console.log('2');
});
// Do something after first emit from subscription1 AND subscription2
doSomething();
...
doSomething() {
console.log('Hello world !');
}
Output goal:
1
2
Hello world !
1
2
1
1
2
1
2
2
...
There've multiple times where I also needed such a isFirst operator that'll run some predicate only for the first emission. I've slapped together a quick custom operator that uses a single state variable first to decide if the emission is indeed first and run some predicate using the tap operator.
Since it uses tap internally it does not modify the source emission in any way. It only runs the passed predicate when the emission is indeed first.
Try the following
isFirst() operator
export const isFirst = (predicate: any) => {
let first = true;
return <T>(source: Observable<T>) => {
return source.pipe(
tap({
next: _ => {
if (first) {
predicate();
first = false;
}
}
})
);
};
};
For combining multiple streams that will be triggered when any of the source emits, you could use RxJS combineLatest function.
Example
import { Component } from "#angular/core";
import { timer, Observable, Subject, combineLatest } from "rxjs";
import { tap, takeUntil } from "rxjs/operators";
#Component({
selector: "my-app",
template: `<button (mouseup)="stop$.next()">Stop</button>`
})
export class AppComponent {
stop$ = new Subject<any>();
constructor() {
combineLatest(timer(2000, 1000), timer(3000, 500))
.pipe(
isFirst(_ => {
console.log("first");
}),
takeUntil(this.stop$)
)
.subscribe({
next: r => console.log("inside subscription:", r)
});
}
}
Working example: Stackblitz
In your case it might look something like
this.subscription = combineLatest(
this.service.getData1().pipe(
tap({
next: data => {
this.data1 = data;
console.log('1');
}
})
),
this.service.getData2().pipe(
tap({
next: data => {
this.data2 = data;
console.log('2');
}
})
)
).pipe(
isFirst(_ => {
console.log("first");
})
).subscribe({
next: r => console.log("inside subscription:", r)
});
The easiest strategy is to have a 3rd Observable that will perform this action.
See below example
const Observable1$ = timer(1000, 2000).pipe(
map(() => 1),
tap(console.log)
);
const Observable2$ = timer(1700, 1700).pipe(
map(() => 2),
tap(console.log)
);
const Observable3$ = combineLatest([Observable1$, Observable2$]).pipe(
take(1),
map(() => "Hello World!"),
tap(console.log)
);
Observable1$.subscribe();
Observable2$.subscribe();
Observable3$.subscribe();
The console output is as per below, since there are two subscribers to Observable1$ (i.e Observable1$ and Observable3$same as two subscribers toObservable2$(i.eObservable2$ and Observable3$ we see console logs 1 1 2 2 'hello world ...'
Here is the link to the stackblitz
In the above we notice that we get 2 subscriptions hence 2 console logs for each. To solve this we can use Subjects to generate new Observables and combine these instead
const track1Subject$ = new Subject();
const track1$ = track1Subject$.asObservable();
const track2Subject$ = new Subject();
const track2$ = track2Subject$.asObservable();
const Observable1$ = timer(1000, 2000).pipe(
map(() => 1),
tap(console.log),
tap(() => track1Subject$.next()),
take(5)
);
const Observable2$ = timer(1700, 1700).pipe(
map(() => 2),
tap(console.log),
tap(() => track2Subject$.next()),
take(5)
);
const Observable3$ = combineLatest([track1$, track2$]).pipe(
take(1),
map(() => "Hello World!"),
tap(console.log)
);
Observable1$.subscribe();
Observable2$.subscribe();
Observable3$.subscribe();
See Link to final solution
With some further restrictions, this problem becomes easier. Unfortunately, operators like combineLatest, and zip add extra structure to your data. I'll provide a solution with zip below, but it doesn't extend at all (if you want to add more logic downstream of your zip, you're out of luck in many cases).
General solution.
Assuming, however, that getData1 and getData2 are completely orthogonal (How they emit and how they are consumed by your app are not related in any predictable way), then a solution to this will require multiple subscriptions or a custom operator tasked with keeping track of emissions.
It's almost certainly the case that you can do something more elegant than this, but this is the most general solution I could think of that meets your very general criteria.
Here, I merge the service calls, tag each call, and pass through emissions until each call has emitted at least once.
merge(
this.service.getData1().pipe(
tap(_ => console.log('1')),
map(payload => ({fromData: 1, payload}))
),
this.service.getData2().pipe(
tap(_ => console.log('2')),
map(payload => ({fromData: 2, payload}))
)
).pipe(
// Custom Operator
s => defer(() => {
let fromData1 = false;
let fromData2 = false;
let done = false;
return s.pipe(
tap(({fromData}) => {
if(done) return;
if(fromData === 1) fromData1 = true;
if(fromData === 2) fromData2 = true;
if(fromData1 && fromData2){
done = true;
doSomething();
}
})
);
})
).subscribe(({fromData, payload}) => {
if(fromData === 1) this.data1 = payload;
if(fromData === 2) this.data2 = payload;
});
In the subscription, we have to separate out the two calls again. Since you're setting a global variable, you could throw that logic as a side effect in the tap operator for each call. This should have similar results.
merge(
this.service.getData1().pipe(
tap(datum => {
console.log('1')
this.data1 = datum;
),
map(payload => ({fromData: 1, payload}))
),
...
The zip Solution
This solution is much shorter to write but does come with some drawbacks.
zip(
this.service.getData1().pipe(
tap(datum => {
console.log('1')
this.data1 = datum;
)
),
this.service.getData2().pipe(
tap(datum => {
console.log('2')
this.data2 = datum;
)
)
).pipe(
map((payload, index) => {
if(index === 0) doSomething();
return payload;
})
).subscribe();
What is passed into your subscription is the service calls paired off. Here, you absolutely must set a global variable as a side effect of the original service call. The option of doing so in the subscription is lost (unless you want them set as pairs).

How to resolve Zone Aware Promise in angular?

How to prevent Zone Aware Promise in angular?
The following code is returning ZoneAwarePromise
const versions = await itemDetails.imageSamples.map(({ item, images }) => {
const mapped = Promise.all(
images.map((image) =>
this.pushImage(image, `items/${id}/images/`)
)
);
return { item, images };
});
// here I'm mapping twice. & I suspect this double mapping is causing the Zone Aware Promise
// I tried this way also:
const versions = await Promise.all( itemDetails.imageSamples.map(({ item, images }) => {
const mapped = Promise.all( ---> // this inside mapping is causing zone aware promise
images.map((image) =>
this.pushImage(image, `items/${id}/images/`)
)
);
return { item, images };
}));
The following code is returning the promise correctly (i.e without any Zone Aware Promise).
const samples = await Promise.all(
itemDetails.samples.map((sample) =>
this.pushImage(sample, `items/${id}/samples/`)
)
);
// here I'm mapping only once. So there is no Zone Aware Promise.
This is the push Image async function.
async pushImage(image, basePath) {
const imgId = this.angularDatabase.createPushId();
const route = `${basePath}${imgId}`;
const imageRef = this.angularFireStorage.ref(route);
return await concat(
imageRef.put(image.image).snapshotChanges().pipe(ignoreElements()),
defer(() => imageRef.getDownloadURL())
)
.pipe(
map(
(url) => (
{ ...image, url }
)
)
)
.toPromise();
}
SO it seems like mapping twice is creating issue
Can someone suggest a workaround?
Update
const promises = Promise.all(
itemDetails.promises.map(({ details, images }) => ({
details,
images: Promise.all(
images.map((image) =>
this.pushImage(image, `items/${id}/images/`)
)
),
}))
);

Ngrx effect parallel http call

I have an effect that should call two different APIs (API1 and API2).
Here's the effect
$LoadKpiMission = createEffect(() =>
this.actions$.pipe(
ofType<any>(EKpiActions.GetMissionsByStation),
mergeMap(action =>
this.apiCallsService.getKpi(action.payload, '2016-04-18').pipe(
map(trips => ({ type: EKpiActions.GetMissionsSuccess, payload: trips })),
catchError(() => EMPTY)
)
)
)
);
Here's the structure of the service
getKpi(station: number, date: string) {
let Kpi = `http://192.168.208.25:8998/api/scheduling/circulation_by_date_and_station?orig=${station}&date=${date}`;
return this.http.get<ISchedules>(API1).pipe(
map(data => {
return this.formatDataToKpi1(data);
})
);
}
However, I have to retrieve additional data from API2 and merge it with the data returned from API1.
I should do that inside the formatDataToKpi1 function.
I would like to know how to run requests in parallel and pass the returned responses to formatDataToKpi1 and do treatment then return to the effect ?
You can make use of the forkJoin RxJS operator.
As stated on the documentation,
When all observables complete, emit the last emitted value from each.
This way, when the observables from both requests have been completed, it will be returned, and you can carry out the subsequent operations.
$LoadKpiMission = createEffect(() =>
this.actions$.pipe(
ofType<any>(EKpiActions.GetMissionsByStation),
mergeMap(action =>
const getKpi = this.apiCallsService.getKpi(action.payload, '2016-04-18');
const getKpi2 = this.apiCallsService.getKpi2();
forkJoin(getKpi, getKpi2).subscribe(([res1, res2] => {
// do the rest here
});
)
)
);
EDIT: Looks like I have initially misunderstood your question - Was a bit confused by the variable names
getKpi(station: number, date: string) {
let Kpi = `http://192.168.208.25:8998/api/scheduling/circulation_by_date_and_station?orig=${station}&date=${date}`;
const api1 = this.http.get<ISchedules>(API1);
const api2 = this.http.get<ISchedules>(API2);
return forkJoin(api1, api2).pipe(
map(data => {
return this.formatDataToKpi1(data);
})
);
}

How to reset a RXJS scan operator based on another Observable

I have a component which triggers an onScrollEnd event when the last item in a virtual list is rendered. This event will do a new API request to fetch the next page and merge them with the previous results using the scan operator.
This component also has a search field which triggers an onSearch event.
How do I clear the previous accumulated results from the scan operator when a search event is triggered? Or do I need to refactor my logic here?
const loading$ = new BehaviorSubject(false);
const offset$ = new BehaviorSubject(0);
const search$ = new BehaviorSubject(null);
const options$: Observable<any[]> = merge(offset$, search$).pipe(
// 1. Start the loading indicator.
tap(() => loading$.next(true)),
// 2. Fetch new items based on the offset.
switchMap(([offset, searchterm]) => userService.getUsers(offset, searchterm)),
// 3. Stop the loading indicator.
tap(() => loading$.next(false)),
// 4. Complete the Observable when there is no 'next' link.
takeWhile((response) => response.links.next),
// 5. Map the response.
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
}))
),
// 6. Accumulate the new options with the previous options.
scan((acc, curr) => {
// TODO: Dont merge on search$.next
return [...acc, ...curr]);
}
);
// Fetch next page
onScrollEnd: (offset: number) => offset$.next(offset);
// Fetch search results
onSearch: (term) => {
search$.next(term);
};
To manipulate the state of a scan you can write higher order functions that get the old state and the new update. Combine then with the merge operator. This way you stick to a clean stream-oriented solution without any side-effects.
const { Subject, merge } = rxjs;
const { scan, map } = rxjs.operators;
add$ = new Subject();
clear$ = new Subject();
add = (value) => (state) => [...state, value];
clear = () => (state) => [];
const result$ = merge(
add$.pipe(map(add)),
clear$.pipe(map(clear))
).pipe(
scan((state, innerFn) => innerFn(state), [])
)
result$.subscribe(result => console.log(...result))
add$.next(1)
add$.next(2)
clear$.next()
add$.next(3)
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.5.3/rxjs.umd.min.js"></script>
This method can easily be extended and/or adapted for other state usecases in rxjs.
Example (remove last item)
removeLast$ = new Subject()
removeLast = () => (state) => state.slice(0, -1);
merge(
..
removeLast$.pipe(map(removeLast)),
..
)
I think you could achieve what you want just by restructuring your chain (I'm omitting tap calls that trigger loading for simplicity):
search$.pipe(
switchMap(searchterm =>
concat(
userService.getUsers(0, searchterm),
offset$.pipe(concatMap(offset => userService.getUsers(offset, searchterm)))),
).pipe(
map(({ data }) => data.map((user) => ({
label: user.name,
value: user.id
}))),
scan((acc, curr) => [...acc, ...curr], []),
),
),
);
Every emission from search$ will create a new inner Observable with its own scan that will start with an empty accumulator.
Found a working solution: I check the current offset by using withLatestFrom before the scan operator and reset the accumulator if needed based on this value.
Stackblitz demo
This is an interesting stream. Thinking about it, offset$ and search$ are really 2 separate streams, though, with different logic, and so should be merged at the very end and not the beginning.
Also, it seems to me that searching should reset the offset to 0, and I don't see that in the current logic.
So here's my idea:
const offsettedOptions$ = offset$.pipe(
tap(() => loading$.next(true)),
withLatestFrom(search$),
concatMap(([offset, searchterm]) => userService.getUsers(offset, searchterm)),
tap(() => loading$.next(false)),
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
})),
scan((acc, curr) => [...acc, ...curr])
);
const searchedOptions$ = search$.pipe(
tap(() => loading$.next(true)),
concatMap(searchTerm => userService.getUsers(0, searchterm)),
tap(() => loading$.next(false)),
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
})),
);
const options$ = merge(offsettedOptions, searchedOptions);
See if that works or would make sense. I may be missing some context.
I know its old, but I just needed to do the same thing and have another solution to throw in to the mix.
There are really just 2 actions the user can trigger
const search$ = new Subject<string>();
const offset$ = new Subject<number>();
We don't really care about offset$ until search$ emits, and at that point, we want it to be 0 to start over. I would write it like this:
const results$ = search$.pipe( // Search emits
switchMap((searchTerm) => {
return offset$.pipe( // Start watching offset
startWith(0), // We want a value right away, so set it to 0
switchMap((offset) => {
return userService.getUsers(offset, searchTerm)) // get the stuff
})
)
}))
At this point we are resetting the offset every time search$ emits, and any time offset$ emits we make a fresh api call fetching the desired resources. We need the collection to reset if search$ emits, so I believe the right place is inside switchMap wrapping the offset$ pipe.
const results$ = search$.pipe( // Search emits
switchMap((searchTerm) => {
return offset$.pipe( // Start watching offset
startWith(0), // We want a value right away, so set it to 0
switchMap((offset) => {
return userService.getUsers(offset, searchTerm)) // get the stuff
}),
takeWhile((response) => response.links.next), // stop when we know there are no more.
// Turn the data in to a useful shape
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
}))
),
// Append the new data with the existing list
scan((list, response) => {
return [ // merge
...list,
...response
]
}, [])
)
}))
This great part here is that the scan is reset on every new search$ emission.
The final bit here, I would move loading$ out of tap, and declare it separately. Final code should look something like this
const search$ = new Subject<string>();
const offset$ = new Subject<number>();
let results$: Observable<{label: string; value: string}[]>;
results$ = search$.pipe( // Search emits
switchMap((searchTerm) => {
return offset$.pipe( // Start watching offset
startWith(0), // We want a value right away, so set it to 0
switchMap((offset) => {
return userService.getUsers(offset, searchTerm)) // get the stuff
}),
takeWhile((response) => response.links.next), // stop when we know there are no more.
// Turn the data in to a useful shape
map(({ data }) =>
data.map((user) => ({
label: user.name,
value: user.id
}))
),
// Append the new data with the existing list
scan((list, response) => {
return [ // merge
...list,
...response
]
}, [])
)
}));
const loading$ = merge(
search$.pipe(mapTo(true)), // set to true whenever search emits
offset$.pipe(mapTo(true)), // set to true when offset emits
results$.pipe(mapTo(false)), // set to false when we get new results
);
results$.subscribe((results) => {
console.log(results);
})

Multipe ajax calls in parallel and dispatching redux actions after success leads to "Uncaught Error: Actions must be plain objects"

I am new to rxjs and redux-observable. Trying to create an epic that allows me to do multiple parallel ajax calls and dispatches respective actions on success:
const loadPosters = (action$) => action$.pipe(
ofType(Types.LOAD_FILMS_SUCCESS),
switchMap(({ films }) =>
forkJoin(films.map(film =>
ajax
.getJSON(`https://api.themoviedb.org/3/search/movie?query=${film.title}`)
.pipe(
map(response => {
const [result] = response.results;
const poster = `http://image.tmdb.org/t/p/w500/${result.poster_path}`;
return Creators.savePoster(film, poster);
})
),
))
),
);
Creators.savePoster() is an action creator for an action named SAVE_POSTER. But, whenever i run my application, no such action is dispatched. Instead i get an error message in browser console:
Uncaught Error: Actions must be plain objects. Use custom middleware
for async actions.
edit
Tried a simplified version without forkJoin, sadly yielding the same result:
const loadPosters = (action$) => action$.pipe(
ofType(Types.INIT_SUCCESS),
mergeMap(({ films }) =>
films.map(film =>
ajax
.getJSON(`https://api.themoviedb.org/3/search/movie?query=${film.title}`)
.pipe(
map(response => {
const [result] = response.results;
const poster = `http://image.tmdb.org/t/p/w500/${result.poster_path}`;
console.log(Creators.savePoster(film, poster));
return Creators.savePoster(film, poster);
})
)
),
),
);
Appendix
Just for reference, I have another epic which does a simple ajax call which works fine:
const loadFilms = action$ => action$.pipe(
ofType(Types.INIT_REQUEST),
mergeMap(() =>
ajax
.getJSON('https://star-wars-api.herokuapp.com/films')
.pipe(
map(response => Creators.initSuccess(response))
),
),
);
The problem is that i don't return an Observable in my inner map. Changing:
return Creators.savePoster(film, poster);
to
return of(Creators.savePoster(film, poster));
makes it work.
By the way, if used with forkJoin it's also possible (and in my case better) to take the mapped results after all requests resolved and dispatch a single action instead of multiple ones:
const loadPosters = (action$) => action$.pipe(
ofType(Types.INIT_SUCCESS),
mergeMap(({ films }) =>
forkJoin(
films.map(film =>
ajax
.getJSON(`https://api.themoviedb.org/3/search/movie?query=${film.title}`)
.pipe(
map(response => ({ film, response }))
)
),
)
),
mergeMap(data => {
const posters = data.reduce((acc, { film, response}) => ({
...acc,
[film.id]: `http://image.tmdb.org/t/p/w500/${response.results[0].poster_path}`,
}), {});
return of(Creators.savePosters(posters));
})
);
In fact this is my favorite solution so far.

Categories