Can't get data from combineLatest - Angular - javascript

I'm trying to get files included in logs of a post. What am I doing wrong here? Data is not coming later in chain when I'm trying to pipe result of combineLatest. The whole code is used in data resolver service.
return this.API.getPost(route.params.id).pipe(
switchMap((response: any) => {
if (response["logs"]) {
response["logs"].map(logs => {
if (logs["files"]) {
return combineLatest(
...logs["files"].map(file=>
this.API.getFile(file.id)
)
).pipe(
// Not getting any files from this.API.getFile(file.id)
map(files =>
files.map(file => ({
url: this.sanitizer.bypassSecurityTrustResourceUrl(
window.URL.createObjectURL(file)
)
}))
)
),
map(files => {
logs["files"] = files;
return response;
});
}
});
}
return of(response);
})
)

Your problem
You're not returning the combineLatest observable to your switchMap.
Inside your if block in switchMap, you are just creating an unused map of observables.
In a very simplified way, you are currently doing this:
switchMap(response =>
if (response["logs"]) {
response["logs"].map(logs => of(logs));
}
return of(response);
}
I've simplified your combine latest into an of to demonstrate the problem. When the if condition passes, the block will create a new array, which is then immediately ignored. This means that regardless of your if condition, switchMap will always invoke of(response). Your combineLatest array will never run.
A solution
You need to return some kind of observable from your if block. If you think about the data type you are creating, it is an array of observables. So for that you will need a forkJoin to run an array of observables and return a single observable that switchMap can switch to.
return this.API.getPost(this.route.params.id).pipe(
switchMap((response: any) => {
if (response["logs"]) {
return forkJoin(response["logs"].map(logs => {
if (logs["files"]) {
return combineLatest(
...logs["files"].map(file=>
this.API.getFile(file.id)
)
).pipe(
map(files =>
files.map(file => ({
url: this.sanitizer.bypassSecurityTrustResourceUrl(
window.URL.createObjectURL(file)
)
}))
)
),
// Not sure what this is???
map(files => {
logs["files"] = files;
return response;
});
}
}));
}
return of(response);
})
)
Additionally, I'm not sure what the purpose of the map is that I've commented - it's currently serving no purpose, and it may even cause compilation issues.
DEMO: https://stackblitz.com/edit/angular-5jgk9z
This demo is a simplistic abstraction of your problem. The "not working" version creates a map of observables that it doesn't return. The "working" version switches to a forkJoin and returns that. In both cases, the condition guarding the if block is true.
Optimising the observable creation
I think the creation of the inner observables can be simplified and made safer.
It seems a little redundant to wrap an array of combineLatest in a forkJoin, when you can just use forkJoin directly.
And to make it clearer, I would separate the array mapping from the observable creation. This would also help you avoid bugs where you end up with an empty array going into combineLatest or forkJoin.
// inside the "if" block
// flatten files
const files = response["logs"]
.filter(log => !!log.files)
.map(log => log.files)
.flat();
if (files.length > 0) {
const observables = files.map(file => this.API.getFile(file.id));
return forkJoin(observables).pipe(
map(files => {
files.map(file => ({
url: this.sanitizer.bypassSecurityTrustResourceUrl(
window.URL.createObjectURL(file)
)
}))
})
);
}
This uses the .flat() array function which takes a multi-dimensional array like:
[
[1,2,3],
[4,5,6]
]
and flattens it out to this:
[ 1,2,3,4,5,6 ]
If you need IE support and don't have a polyfill, you can use an alternative from here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat.
I would also recommend creating some interfaces and using strong typing. You soon start to get lost if you're relying solely on good variable naming (not that we ever give variables bad names, of course...)

Be aware that combineLatest will not emit an initial value until each observable emits at least one value. This is the same behavior as withLatestFrom and can be a gotcha as there will be no output and no error but one (or more) of your inner observables is likely not functioning as intended, or a subscription is late.
Maybe one of your API calls to:
this.API.getFile(file.id)
returns an error or never completes?
This comes straight from the documentation here
UPDATE:
After mentioning in your comment that you use RxJs 6, you need to know that combineLatest expects an array as an argument.
So you should consider changing to:
return combineLatest(
logs["files"].map(
file => this.API.getFile(file.id);
),
);

Related

append new observables to array of observables?

I am trying to avoid the following:
switchMap(([action, state]) =>
from(TodosDB.getAll()).pipe(
map(
(todos) => onQueryTodoDone({ items: todos }),
catchError((err) => of(onQueryTodoFail({ error: err })))
)
)
),
to something more linear like like we do with combineLatestFrom in ngrx.
So far I tried to do the below. But the promise does not seem to work.
withLatestFrom(
from(TodosDB.getAll())
),
and
withLatestFrom(
from(TodosDB.getAll()).pipe(
map((todos) => onQueryTodoDone({ items: todos }))
)
)
Any ideas how to deal with this scenario without nesting pipe map in a switchMap?
PS: this might be obvious to you but I don't know much and I looked up on the internet and found withLatestFrom but not sure what I am missing.
EDIT: this is the best I got so far:
switchMap(([action, state]) =>
forkJoin([of(action), of(state), TodosDB.getAll()])
),
map(
([action, state, todos]) => onQueryTodoDone({ items: todos }),
catchError((err) => of(onQueryTodoFail({ error: err })))
),
The above is better but I have no idea if that can cause issues later. But hopefully I was able to communicate the idea. Which is append a promise result to the piped observable keep it's original structure
[Observable<Actions>,Observable<State>,Observable<FromPromise>]
Joining a few dots here and taking a bit of a guess I would say the problem stems from TodosDB.getAll returning a Promise.
With a promise based function you want to lazily evaluate it, as such a function is executed immediately when it is called, unlike an observable based function which requires a subscription.
This is why the switchMap based solutions work, because the body of the switchMap is not evaluated until the source emits.
In your shortened version using withLatestFrom, there is no lazy evaluation, the getAll call is probably evaluated once and once only when the effect is set up.
You can use the defer operator to convert your promise based function to one that behaves more appropriately with the rxjs observable based operators.
withLatestFrom(defer(() => this.getAll())), // use of defer
map(([[action, state], todos]) => [action, state, todos]), // refine the shape of the data to your requirements
...
Stackblitz: https://stackblitz.com/edit/angular-ivy-qkwqyw?file=src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fapp.component.html
Note: The concatLatestFrom ngrx operator also looks like it may be of use but I couldn't get it to work how I wanted.

Struggling with flatMap vs concatMap in rxJs

I am struggling to understand the difference between the flatMap and concatMap in rxJs.
The most clear answer that I could understand was that here difference-between-concatmap-and-flatmap
So I went and tried things out by my self.
import "./styles.css";
import { switchMap, flatMap, concatMap } from "rxjs/operators";
import { fromFetch } from "rxjs/fetch";
import { Observable } from "rxjs";
function createObs1() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(1);
subscriber.complete();
}, 900);
});
}
function createObs2() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(2);
//subscriber.next(22);
//subscriber.next(222);
subscriber.complete();
}, 800);
});
}
function createObs3() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(3);
//subscriber.next(33);
//subscriber.next(333);
subscriber.complete();
}, 700);
});
}
function createObs4() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(4);
subscriber.complete();
}, 600);
});
}
function createObs5() {
return new Observable<number>((subscriber) => {
setTimeout(() => {
subscriber.next(5);
subscriber.complete();
}, 500);
});
}
createObs1()
.pipe(
flatMap((resp) => {
console.log(resp);
return createObs2();
}),
flatMap((resp) => {
console.log(resp);
return createObs3();
}),
flatMap((resp) => {
console.log(resp);
return createObs4();
}),
flatMap((resp) => {
console.log(resp);
return createObs5();
})
)
.subscribe((resp) => console.log(resp));
console.log("hellooo");
I have used that playground here playground example
Questions
1)
From my understanding the use of flatMap should mix the outputs so that the console logs are like (1,3,2,4,5). I have tried more than 30 times and always come on the same row (1, 2, 3, 4, 5)
What am I doing wrong or have undestood wrong?
2)
If on createObs2() and createObs3() you remove the comments and include the code with multiple emitted events then things get messy. Even if you change to concatMap it messes things and results come mixed. Multiple numbers that I expect only once come multiple times. The result can be (1, 2, 33, 3, 2, 22, 3, 33, 4, 5, 4, 3, 4, 5) Why this happens?
How I test the example on playground. I just remove only 1 letter from the last console.log("hello"). Only one change for example console.log("heloo") and is then observed and project is compiled again and output printed in console.
Edit: The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library.
createObs1().subscribe( (resp1) => {
console.log(resp1);
createObs2().subscribe( (resp2) => {
console.log(resp2);
createObs3().subscribe( (resp3) => {
console.log(resp3);
createObs4().subscribe( (resp4) => {
console.log(resp4);
createObs5().subscribe( (resp5) => {
console.log(resp5);
})
})
})
})
})
Your test scenario is not really sufficient to see the differences between these two operators. In your test case, each observable only emits 1 time. If an observable only emits a single value, there is really no different between concatMap and flatMap (aka mergeMap). The differences can only be seen when there are multiple emissions.
So, let's use a different scenario. Let's have a source$ observable that simply emits an incrementing integer every 1 second. Then, within our "Higher Order Mapping Operator" (concatMap & mergeMap), we will return an observable that emits a variable number of times every 1 second, then completes.
// emit number every second
const source$ = interval(1000).pipe(map(n => n+1));
// helper to return observable that emits the provided number of times
function inner$(max: number, description: string): Observable<string> {
return interval(1000).pipe(
map(n => `[${description}: inner source ${max}] ${n+1}/${max}`),
take(max),
);
}
Then let's define two separate observables based on the source$ and the inner$; one using concatMap and one using flatMap and observe the output.
const flatMap$ = source$.pipe(
flatMap(n => inner$(n, 'flatMap$'))
);
const concatMap$ = source$.pipe(
concatMap(n => inner$(n, 'concatMap$'))
);
Before looking the differences in the output, let's talk about what these operators have in common. They both:
subscribe to the observable returned by the passed in function
emit emissions from this "inner observable"
unsubscribe from the inner observable(s)
What's different, is how they create and manage inner subscriptions:
concatMap - only allows a single inner subscription at a time. As it receives emissions, it will only subscribe to one inner observable at a time. So it will initially subscribe to the observable created by "emission 1", and only after it completes, will it subscribe to the observable created by "emission 2". This is consistent with how the concat static method behaves.
flatMap (aka mergeMap) - allows many inner subscriptions. So, it will subscribe to the inner observables as new emissions are received. This means that emissions will not be in any particular order as it will emit whenever any of its inner observables emit. This is consistent with how the merge static method behaves (which is why I personally prefer the name "mergeMap").
Here's a StackBlitz that shows the output for the above observables concatMap$ and mergeMap$:
Hopefully, the above explanation helps to clear up your questions!
#1 - "use of flatMap should mix the outputs"
The reason this wasn't working as you expected was because only one emission was going through the flatMap, which means you only ever had a single "inner observable" emitting values. As demonstrated in the above example, once flatMap receives multiple emissions, it can have multiple inner observables that emit independently.
#2 - "...and include the code with multiple emitted events then things get messy."
The "things get messy" is due to having multiple inner subscription that emit values.
For the part you mention about using concatMap and still getting "mixed" output, I would not expect that. I have seen weird behavior in StackBlitz with observable emissions when "auto save" is enabled (seems like sometimes it doesn't completely refresh and old subscriptions seem to survive the auto refresh, which gives very messy console output). Maybe code sandbox has a similar problem.
#3 - "The reason I have gone to flatMap and concatMap was to find a replacement for nested subscriptions in angular using the http library"
This makes sense. You don't want to mess around with nested subscriptions, because there isn't a great way to guarantee the inner subscriptions will be cleaned up.
In most cases with http calls, I find that switchMap is the ideal choice because it will drop emissions from inner observables you no longer care about. Imagine you have a component that reads an id from a route param. It uses this id to make an http call to fetch data.
itemId$ = this.activeRoute.params.pipe(
map(params => params['id']),
distinctUntilChanged()
);
item$ = this.itemId$.pipe(
switchMap(id => http.get(`${serverUrl}/items/${id}`)),
map(response => response.data)
);
We want item$ to emit only the "current item" (corresponds to the id in the url). Say our UI has a button the user can click to navigate to the next item by id and your app finds itself with a click-happy user who keeps smashing that button, which changes the url param even faster than the http call can return the data.
If we chose mergeMap, we would end up with many inner observables that would emit the results of all of those http calls. At best, the screen will flicker as all those different calls come back. At worst (if the calls came back out of order) the UI would be left displaying data that isn't in sync with the id in the url :-(
If we chose concatMap, the user would be forced to wait for all the http calls to be completed in series, even though we only care about that most recent one.
But, with switchMap, whenever a new emission (itemId) is received, it will unsubscribe from the previous inner observable and subscribe to the new one. This means it will not ever emit the results from the old http calls that are no longer relevant. :-)
One thing to note is that since http observables only emit once, the choice between the various operators (switchMap, mergeMap, concatMap) may not seem to make a difference, since they all perform the "inner observable handling" for us. However, it's best to future-proof your code and choose the one that truly gives you the behavior you would want, should you start receiving more than a single emission.
Every time the first observable emits, a second observable is created in the flatMap and starts emitting. However, the value from the first observable is not passed along any further.
Every time that second observable emits, the next flatMap creates a third observable, and so on. Again, the original value coming into the flatMap is not passed along any further.
createObs1()
.pipe(
flatMap(() => createObs2()), // Merge this stream every time prev observable emits
flatMap(() => createObs3()), // Merge this stream every time prev observable emits
flatMap(() => createObs4()), // Merge this stream every time prev observable emits
flatMap(() => createObs5()), // Merge this stream every time prev observable emits
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
So, it's only the values emitted from createObs5() that actually get emitted to the observer. The values emitted from the previous observables have just been triggering the creation of new observables.
If you were to use merge, then you would get what you may have been expecting:
createObs1()
.pipe(
merge(createObs2()),
merge(createObs3()),
merge(createObs4()),
merge(createObs5()),
)
.subscribe((resp) => console.log(resp));
// OUTPUT:
// 5
// 4
// 3
// 2
// 1

RxJS Subscribing varying amount of Observables inside Subscription

In the following code snippet I subscribe to a portfolio and iterate through an array. For each member of the array I create another subscription to listen to its quotes.
this.portfolio$
.subscribe(portfolio) => {
portfolio.watchlist.all.forEach((a) => {
this.store
.select((s) => s.quotes.quotes[a._id])
.subscribe((q) => {
console.warn('subscription')
}
}
})
})
Is there any RxJS operator which allows me to get rid of the outer subscription? In the end I only want to have as many subscriptions as I have quotes in the array.
yea nesting subscribes is a bad idea and you're probably creating a pretty severe memory leak here.
try more like:
this.portfolio$.pipe(
switchMap(portfolio => {
return combineLatest(portfolio.watchlist.all.map(
a => this.store.select(s => s.quotes.quotes[a.id])
))
})
).subscribe(combinedQ => console.log(combinedQ))
here you use switchMap to switch into a new stream, which is all of the observables combined in combineLatest which will emit all latest values in an array.

waiting observable subscribe inside foreach with forkJoin

I am trying to populate an array in my component called conventions which is an array of convention.
Each organization has a list of contracts, and each contract has a convention id, with this id i got the convention.
I use getOrganizationForUser to get current organization and then get the list of contract.
Then i use the convention id from contract to call the second API to get the convention.
Currently, my code looks something like this:
public getOrganizationForUser(): Observable<Organization> {
return this.httpClient
.get<Organization>(`${c.serviceBaseUrl.sp}/organizationByUser`)
.pipe(catchError((err, source) => this.responseHandler.onCatch(err, source)));
}
public getById(id: number) {
return this.httpClient
.get<Convention>(`${c.serviceBaseUrl.sp}/conventions/` + id)
.pipe(catchError((err, source) => this.responseHandler.onCatch(err, source)));
}
ngOnInit() {
this.OrganizationService.getOrganizationForUser().subscribe((organization: Organization) => {
organization.contracts.forEach((contract) => {
this.conventionService.getById(contract.conventionId).subscribe((convention: Convention) => {
this.conventions.push(convention);
})
})
})
}
I understand that I can create an array of observables, and use Observable.forkJoin() to wait for all these async calls to finish but I want to be able to define the subscribe callback
function for each of the calls since I need a reference to the process. Any ideas on how I can go about approaching this issue?
i tried with this function but always is return understand
getTasksForEachProcess(): Observable<Array<any>> {
let tasksObservables = this.organizationService.getOrganizationForUser().pipe(map((organization: Organization) => {
organization.contractOrganizations.map(contract => {
return this.conventionService.getById(contract.conventionId).subscribe(convention =>
this.conventions.push(convention)
)
});
})
);
return forkJoin(tasksObservables);
};
ngOnInit() {
this.getTasksForEachProcess().subscribe(item => {
console.log(item);
}
}
First of all I am not sure of what your are really trying to achieve, since I do not understand what you mean by
I want to be able to define the subscribe callback function for each
of the calls since I need a reference to the process
Anyways, in a situation like the one you describe, I would do something like this
public getOrganizationForUser(): Observable<Organization> {
return this.httpClient
.get<Organization>(`${c.serviceBaseUrl.sp}/organizationByUser`)
.pipe(catchError((err, source) => this.responseHandler.onCatch(err, source)));
}
public getById(id: number) {
return this.httpClient
.get<Convention>(`${c.serviceBaseUrl.sp}/conventions/` + id)
.pipe(catchError((err, source) => this.responseHandler.onCatch(err, source)));
}
ngOnInit() {
const obsOfObservables = this.OrganizationService.getOrganizationForUser()
.pipe(
map(organization => organization.contracts),
map(contracts => contracts.map(contract => this.conventionService.getById(contract.conventionId)))
);
obsOfObservables
.pipe(
switchMap(conventionObservables => forkJoin(conventionObservables))
)
.subscribe(
conventions => { // do stuff with conventions }
)
}
The key points here are the following.
Via getOrganizationForUser() you get an Observable which emits the Organization. The first thing you do you transform the object emitted by the Observable into an Array of contracts with the first map operator.
The second map operator transforms the Array of contracts into an Array of Observables of conventions. To perform this transformation we use the map method of Array within the map operator of Observable. This may be a bit confusing, but it is worth understanding.
If we stop here, what we have is obsOfObservables, i.e. an Observable which emits an Array of Observables.
We then pass the Array of Observables emitted by obsOfObservables to the forkJoin function, which in itself returns an Observable. Since we actually interested in what is notified by the Observable returned by forkJoin, i.e. we are interested in the conventions, then we need to switch from the first Observable to the second one, and this is done via switchMap operator.
The net result is an Observable which returns an Array of conventions. Consider that the constant obsOfObservables has been added as an attempt to clarify the reasoning and it is totally unnecessary (as Barney Panofsky would say).
I have not simulated the whole thing, so I hope I have not inserted mistakes, but more or less this is the thought process I would use in this case.
Last note, be generally suspicious when you have subscribe within subscibe.
I agree with Picci's logic in thinking through his answer. Here is a slight variation to what he proposed, though like him I have not rigorously tested this and there may be some errors.
The logic to this is that what you ultimately want is an array of convention, and producing an array from observables is what 'zip' does. So here is the flow:
get an organization, then create a stream of observables out of the organization.contracts array using rxjs' from.
each item in that stream will be a contract which will then be transformed (using map) into a convention based on and API lookup using the contract.conventionId property.
this whole resulting stream of observables of conventions will finally be transformed back into an array by the wrapping zip, and delivered as an observable that can be subscribed to resulting in the wanted array of conventions.
Here is the code:
ngOnInit() {
zip( this.OrganizationService.getOrganizationForUser()
.pipe(
map((organization: Organization) =>
from(organization.contracts).pipe(
map(contract => this.conventionService.getById(contract.conventionId))
)
)
)
)
.subscribe((conventions: Convention[]) => this.conventions = conventions)
}

RxJS share parent observable among partitioned child observables

I'm coding a game in which the character can fire their weapon.
I want different things to happen when the player tries to fire, depending on whether they have ammo.
I reduced my issue down to the following code (btw I'm not sure why SO's snippet feature does not work, so I made CodePen where you can try out my code).
const { from, merge } = rxjs;
const { partition, share, tap } = rxjs.operators;
let hasAmmo = true;
const [ fire$, noAmmo$ ] = from([true]).pipe(
share(),
partition(() => hasAmmo),
);
merge(
fire$.pipe(
tap(() => {
hasAmmo = false;
console.log('boom');
}),
),
noAmmo$.pipe(
tap(() => {
console.log('bam');
}),
)
).subscribe({
next: val => console.log('next', val),
error: val => console.log('error', val),
complete: val => console.log('complete', val),
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.js"></script>
When I run this code I get the following:
"boom"
"next" true
"bam"
"next" true
"complete" undefined
I don't understand why I get a "bam".
The first emission goes to fire$ (I get a "boom"), which makes sense because hasAmmo is true. But as a side-effect of fire$ emitting is that the result of the partition condition changes, which I guess is causing me to get a "bam".
Am I not supposed to cause side-effects that affect partition()?
Or maybe is there an issue with the way I share() my parent observable? I may be wrong but I would intuitively think the fire$ and noAmmo$ internally subscribe to the parent in order to split it, in which case share() should work?
It actually works correctly. The confusion comes from the partition operator which is basically just two filter operators.
If you rewrite it without partition it looks like this:
const fire$ = from([true]).pipe(
share(),
filter(() => hasAmmo),
);
const noAmmo$ = from([true]).pipe(
share(),
filter(() => !hasAmmo),
);
Be aware that changing hasAmmo has no effect on partition itself. partition acts only when it receives a value from its source Observable.
When you later use merge() it makes two separate subscriptions to two completely different chains with two different from([true])s. This means that true is passed to both fire$ and noAmmo$.
So share() has no effect here. If you want to share it you'll have to wrap from before using it on fire$ and noAmmo$. If the source Observable is just from it's unfortunately going to be even more confusing because the initial emission will arrive only to the first subscriber which is fire$ later when used in merge:
const shared$ = from([true]).pipe(
share(),
);
const fire$ = shared$.pipe(...);
const noAmmo$ = shared$.pipe(...);
The last thing why you're receiving both messages is that partition doesn't modify the value that goes through. It only decides which one of the returned Observable will reemit it.
Btw, rather avoid partition completely because it's probably going to be deprecated and just use filter which is more obvious:
https://github.com/ReactiveX/rxjs/issues/3797
https://github.com/ReactiveX/rxjs/issues/3807

Categories

Resources