rxjs: subscribing to observable in map - rxjs

My first observable returns an array of Persons. I want to update each person of that array with a list of clients from second observable. How do I do that? So far I have this:
const json: Person[] = [new Person('Alice'), new Person('Bob')];
const resultsObservable = new Observable<string[]>(subscriber => {
setTimeout(() => {
subscriber.next(['Client1', 'Client2', 'Client3']);
subscriber.complete();
}, 1000);
});
of(json).pipe(
switchMap( dataArray => {
return from(dataArray);
}),
map((x: Person) => {
resultsObservable.subscribe(r => {
x.clients = r;
});
return x;
}),
).subscribe(value => {
console.log(value);
});
}
Person:
export class Person{
name: string;
clients?: string[];
constructor(name: string) {
this.name = name;
}
}
But the problem is that return happens before the values are set, so at the end value of person.clients is undefined. How do I fix this? Or what is a better way to achieve what I'm trying to do?

Ok I think I found what I was looking for:
const result = persons.pipe(
mergeMap(p => resultsObservable.pipe(map(clients => {
p.clients = clients;
return p;
}))),
);
result.subscribe(p => console.log(p));

Related

Combining observable outputs in to one array

I need to make two calls to Firebase (as it doesn't support OR queries) and merge the output into one array at the end to return to the calling service.
I have something that gets pretty close but it outputs a 2D array of arrays (one for each call to Firebase). I've tried a few things and this is the best I can get to. Any help on tidying up the below would be great.
getAllFriends(): Observable<[Friendship[], Friendship[]]> {
const invitesSent = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('inviter', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
const invitesReceived = from(this.afAuth.currentUser.then(user => {
return user.uid;
}))
.pipe(
switchMap(
userid => {
return this.db.collection('friendships', ref => ref.where('invitee', '==', userid)).snapshotChanges().pipe(map(actions => {
return actions.map(action => {
const data = new Friendship(action.payload.doc.data());
data.id = action.payload.doc.id;
console.log(data);
return data;
});
}));
}
)
);
return combineLatest([invitesSent, invitesReceived]);
}
Friendship is just an object with property: value pairs, nothing special.
I have tried then putting a .pipe() after this returned observable but that just stops the subscription firing in the calling service.
What about returning, at the end, something like this
return combineLatest([invitesSent, invitesReceived]).pipe(
map(([frienships_1, friendships_2]) => ([...friedships_1, ...friendships_2]))
)

Passing value from one RxJS operator to another

Here is my code:
#Injectable()
export class TraitementDetailEffects {
ingoing_loadDetail: { traitementID: number, obs: Promise<any> };
#Effect()
loadTraitementDetail$: Observable<Action> = this.actions$.pipe(
ofType(ETraitementDetailActions.loadTraitementDetail),
map((action: LoadTraitementDetail) => action.payload),
switchMap((traitementID) => {
if (this.ingoing_loadDetail && this.ingoing_loadDetail.traitementID === traitementID) {
return this.ingoing_loadDetail.obs;
}
const obs = this.traitementsService.loadDetail(traitementID);
this.ingoing_loadDetail = {traitementID: traitementID, obs: obs};
return obs;
}),
map(result => {
this.ingoing_loadDetail = null;
//here I don't have access to traitementID :'(
return new LoadTraitementDetailSuccess(traitementID, result);
})
);
constructor(
private actions$: Actions,
private traitementsService: TraitementsService
) {
}
}
I'm trying to pass the variable or value traitementID to the last map.
I tried to avoid the last map with an async await but then I get a weird errors "Effect dispatched an invalid action" and "Actions must have a type property" (FYI all my actions have a type property).
Try to bake this id into observable's resolve, like:
switchMap((traitementID) => {
return this.traitementsService.loadDetail(traitementID).pipe(
map(detail => ({detail,traitementID}))
);
}),
map(({detail,traitementID}) => {
...
})

RXJS listen to first subscription

I have a function that wraps observable with error handling, but to do so I need some code to run once it's inner observable is subscribed.
I also need that cancelling the higher Observable cancels the inner one, as it is doing HTTP call.
Context
slideshow: string[] = [];
currentIndex = 0;
private is = {
loading: new BehaviorSubject(false),
}
private loadImage(src: string): Observable;
private loadNextImage(index = this.currentIndex, preload = false): Observable<number> {
const nextIndex = (index + 1) % this.slideshow.length;
if (this.currentIndex == nextIndex) {
if (!preload) {
this.is.loading.next(false);
}
throw new Error('No other images are valid');
}
return ( possible code below )
}
Defer - This worked nicely until I realised this will create a new instance for every subscriber.
defer(() => {
if (!preload) {
this.is.loading.next(true);
}
return this.loadImage(this.slideshow[nextIndex]).pipe(
finalize(() => {
if (!preload) {
this.is.loading.next(false);
}
}),
map(() => nextIndex),
catchError(err => this.loadNextImage(nextIndex)),
);
});
Of(void 0).pipe(mergeMap(...)) - This does what is should, but it is really ugly
of(void 0).pipe(
mergeMap(() => {
if (!preload) {
this.is.loading.next(true);
}
return this.loadImage(this.slideshow[nextIndex]).pipe(
finalize(() => {
if (!preload) {
this.is.loading.next(false);
}
}),
map(() => nextIndex),
catchError(err => this.loadNextImage(nextIndex)),
);
}),
)
new Observable - I think there should be a solution that I am missing

RxJS Filter Array of Arrays

I'm trying to perform a filter on a array of arrays in rxjs. Consider the following:
function guard1(): boolean | Observable<boolean> {}
function guard2(): boolean | Observable<boolean> {}
function guard3(): boolean | Observable<boolean> {}
const routes = [
{ name: 'Foo', canActivate: [guard1, guard2] },
{ name: 'Bar', canActivate: [guard3] },
{ name: 'Moo' }
];
I want to filter the routes array to only routes that return true
from the combination of results of inner array canActivate or if it doesn't have canActivate, I want it to be NOT filtered out.
Lets say guard1 returned true and guard2 returned false, I'd expect route Foo to not be in the filtered list.
I took a stab at this but its not quite doing what I expect:
this.filteredRoutes = forkJoin(of(routes).pipe(
flatMap((route) => route),
filter((route) => route.canActivate !== undefined),
mergeMap((route) =>
of(route).pipe(
mergeMap((r) => r.canActivate),
mergeMap((r) => r()),
map((result) => {
console.log('here', result, route);
return route;
})
)
)));
If I were writing this outside of RXJS, the code might look something like this:
this.filteredRoutes = [];
for (const route of this.routes) {
if (route.canActivate) {
let can = true;
for (const act of route.canActivate) {
let res = inst.canActivate();
if (res.subscribe) {
res = await res.toPromise();
}
can = res;
if (!can) {
break;
}
}
if (can) {
this.filteredRoutes.push(route);
}
} else {
this.filteredRoutes.push(route);
}
}
Thanks!
I'm sure there's other (and likely better ways to handle this, but it works...
from(routes).pipe(
concatMap((route) => {
// handle if nothing is in canActivate
if (!route.canActivate || route.canActivate.length === 0) {
// create an object that has the route and result for filtering
return of({route, result: true})
};
const results = from(route.canActivate).pipe(
// execute the guard
switchMap(guard => {
const result: boolean | Observable<boolean> = guard();
if (result instanceof Observable) {
return result;
} else {
return of(result);
}
}),
// aggregate the guard results for the route
toArray(),
// ensure all results are true
map(results => results.every(r => r)),
// create an object that has the route and result for filtering
map(result => ({route, result})),
);
return results;
}),
// filter out the invalid guards
filter(routeCanActivateResult => routeCanActivateResult.result),
// return just the route
map(routeCanActivateResult => routeCanActivateResult.route),
// turn it back into an array
toArray()
)
// verify it works
.subscribe(routes => routes.forEach(r => console.log(r.name)));
Also, here is a working example in stackblitz.

Angularfire2 & Firestore – retrieve all subcollection content for a collection list

I try to retrieve datas in a subcollection based on the key received on the first call.
Basically, I want a list of all my user with the total of one subcollection for each of them.
I'm able to retrieve the data from the first Payload, but not from pointRef below
What is the correct way to achieve that?
getCurrentLeaderboard() {
return this.afs.collection('users').snapshotChanges().map(actions => {
return actions.map(a => {
const data = a.payload.doc.data()
const id = a.payload.doc.id;
const pointRef: Observable<any> = this.afs.collection('users').doc(`${id}`).collection('game').valueChanges()
const points = pointRef.map(arr => {
const sumPoint = arr.map(v => v.value)
return sumPoint.length ? sumPoint.reduce((total, val) => total + val) : ''
})
return { id, first_name: data.first_name, point:points };
})
})
}
I tried to put my code in a comment, but I think it's better formated as a answer.
First you need subscribe your pointRef and you can change your code like this.
getCurrentLeaderboard() {
return this.afs.collection('users').snapshotChanges().map(actions => {
return actions.map(a => {
const data = a.payload.doc.data()
const id = a.payload.doc.id;
const pointRef: Observable<any> = this.afs.object(`users/${id}/game`).valueChanges() // <--- Here
const pointsObserver = pointRef.subscribe(points => { //<--- And Here
return { id, first_name: data.first_name, point:points };
})
})
}
....
//Usage:
getCurrentLeaderboard.subscribe(points => this.points = points);
And if you going to use this function alot, you should start to denormalize your data.

Resources