Subscribe onComplete never completes with flatMap - javascript

I'm using Angular 6 with RxJS 6.2.2 and RxJS Compact 6.2.2.
I have a code to call my api service to load some records, which is:
this.route.params
.flatMap((params: Params) => {
if (!params['id']) {
return Observable.throwError('Id is not specified.');
}
this.itemId = params['id'];
this.isEditMode = true;
this.loadCategoryGroupCondition = new LoadCategoryGroupViewModel();
this.loadCategoryGroupCondition.id = [this.itemId];
this.loadCategoryGroupCondition.pagination = new Pagination();
return this.categoryGroupService
.loadCategoryGroup(this.loadCategoryGroupCondition);
})
.subscribe(
(loadCategoryGroupResult: SearchResult<CategoryGroup>) => {
console.log(loadCategoryGroupResult);
},
() => {},
() => {
console.log('Completed')
});
The code above can print a list of my items returned from my api service. That means onSuccess has been called.
But the complete method is fired.
What is wrong with my code ?
Thank you,

As discussed, the flatMap operator does itself not complete its source observable. You are using this.route.params as your source observable, which is long-lived - it never completes by itself.
To get a complete notification you can use an operator such as take. It will re-emit the number of items you pass as a parameter and complete afterwards. For example, if you just want to receive the current route and are not interested in further notifications of your source observable, use take(1), like:
this.route.params
.take(1)
.flatMap((params: Params) => {
Also, note that the recommeded way for doing this in RxJS 6+ is using pipeable operators. This would look like so:
this.route.params.pipe(
first(),
mergeMap((params: Params) => {
...
})
I also replaced the operators with the newer recommended variants.

Related

Refactoring chained RxJs subscriptions

I have a piece of code that I need to refactor because it's a hell of chained subscriptions.
ngOnInit(): void {
this.dossierService.getIdTree()
.subscribe(idTree => {
this.bootstrappingService.refreshObligations(idTree)
.subscribe(() => {
this.dossierPersonsService.retrieveDossierPersons(idTree)
.subscribe(debtors => {
this.retrieveObligations();
this.debtors = debtors;
});
});
});
}
The first call dossierService.getIdTree() retrieves idTree which is used by other services except obligationsService.retrieveObligations().
All service methods should be executed in the order they executed now. But retrieveDossierPersons and retrieveObligations can be executed in parallel.
retrieveObligations() is a method that subscribes to another observable. This method is used in a few other methods.
I've refactored it and it seems to work. But did I refactor it in a proper way or my code can be improved?
this.dossierService.getIdTree()
.pipe(
map(idTree => {
this.idTree = idTree;
}),
switchMap(() => {
return this.bootstrappingService.refreshObligations(this.idTree)
}),
switchMap(
() => {
return this.dossierPersonsService.retrieveDossierPersons(this.idTree)
},
)
)
.subscribe(debtors => {
this.retrieveObligations();
this.debtors = debtors;
});
Something like this (not syntax checked):
ngOnInit(): void {
this.dossierService.getIdTree().pipe(
switchMap(idTree =>
this.bootstrappingService.refreshObligations(idTree)).pipe(
switchMap(() => this.dossierPersonsService.retrieveDossierPersons(idTree).pipe(
tap(debtors => this.debtors = debtors)
)),
switchMap(() => this.retrieveObligations())
)
).subscribe();
}
Using a higher-order mapping operator (switchMap in this case) will ensure that the inner observables are subscribed and unsubscribed.
In this example, you don't need to separately store idTree because you have access to it down the chained pipes.
You could try something like:
ngOnInit(): void {
const getIdTree$ = () => this.dossierService.getIdTree();
const getObligations = idTree => this.bootstrappingService.refreshObligations(idTree);
const getDossierPersons = idTree => this.dossierPersonsService.retrieveDossierPersons(idTree);
getIdTree$().pipe(
switchMap(idTree => forkJoin({
obligations: getObligations(idTree)
debtors: getDossierPersons(idTree),
}))
).subscribe(({obligations, debtors}) => {
// this.retrieveObligations(); // seems like duplicate of refreshObligations?
this.debtors = debtors;
});
}
Depending on the rest of the code and on the template, you might also want to avoid unwrapping debtors by employing the async pipe instead
forkJoin will only complete when all of its streams have completed.
You might want also want to employ some error handling by piping catchError to each inner observable.
Instead of forkJoin you might want to use mergeMap or concatMap (they take an array rather than an object) - this depends a lot on logic and the UI. concatMap will preserve the sequence, mergeMap will not - in both cases, data could be display accumulatively, as it arrives. With forkJoin when one request gets stuck, the whole stream will get stuck, so you won't be able to display anything until all streams have completed.
You can use switchMap or the best choice is concatMap to ensure orders of executions
obs1$.pipe(
switchMap(data1 => obs2$.pipe(
switchMap(data2 => obs3$)
)
)

Need to combine two impure observables

I need to make 2 AJAX requests to the same endpoint that would return filtered and unfiltered data. Then I need to combine results and use them both in processing.
loadUnfilteredData() {
// remember status
const {status} = this.service.filters;
delete this.service.filters.status;
this.service.saleCounts$()
.subscribe((appCounts) =>
this.processUnfilteredData(appCounts)
);
// restore status
if (status) {
this.service.filters.status = status;
}
}
loadFilteredData() {
this.service.saleCounts$()
.subscribe((appCounts) =>
this.processFilteredData(appCounts)
);
}
The problem is that this.service.saleCounts$() is impure and instead of using arguments just uses this.service.filters.
That's why i have to store the status, then delete it from filter, then do the request, and then restore (because same filter is used by other requests).
So I can't just do combineLatest over two observables (because i need to restore).
Is there any workaround?
(p.s. I know the approach is disgusting, i know about state management and about pure functions. Just wanted to know is there any beautiful solution).
I believe your constraints require that the two operations are run sequentially , one after the other, rather than in parallel as is generally the case when we're using combineLatest.
To run two Observables sequentially, we can use switchMap (as an operator inside a pipe call in modern rxjs):
doFirstOperation()
.pipe(
switchMap(result => return doSecondOperation())
);
One potential issue with that is that you lose access to the result of doFirstOperation when you switchMap it to the result of doSecondOperation. To work around that, we can do something like:
doFirstOperation()
.pipe(
switchMap(firstResult => return doSecondOperation())
.pipe(
map(secondResult => [firstResult, secondResult])
)
);
i.e., use map to change the returned value of switchMap to be an array including both values.
Putting this together with your "disgusting" requirements for state management, you could use something like:
loadData() {
const { status } = this.service.filters;
delete this.service.filters.status;
return this.service
.saleCounts$()
.pipe(
finalize(() => {
if (status) {
this.service.filters.status = status;
}
}),
switchMap(filteredData => {
return this.service
.saleCounts$() // unfiltered query
.pipe(map(unfilteredData => [filteredData, unfilteredData]));
})
)
.subscribe(results => {
const [filteredData, unfilteredData] = results;
this.processFilteredData(filteredData);
this.processUnfilteredData(unfilteredData);
});
}
I'm not too many people would categorize that is beautiful, but it does at least allow you to get results in a way that looks like you used combineLatest, yet works around the constraints imposed by your impure method.

Whats the correct method to chain Observable<void> with typescript (Angular)?

I've been searching for the right/best/correct way to chain a few Observable methods. I've read on the using pipe() and map() and so on, but i'm not 100% i fully understand. The basis is i have a few actions to carry out, which some needs to be in sequence, and some dont.
Below are the 4 methods i need to call.
createOrder(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateCurrentStock(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateSalesSummary(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
updateAnotherDocument(order:Order):Observable<void>{
return new Observable((obs)=>
//Do Something
obs.complete();
)
}
From this 4, the flow should be createOrder ->updateCurrentStock->updateSalesSummary, updateAnotherDocument.
As of now, what i have is
var tasks = pipe(
flatMap(e => this.createOrder(order)),
flatMap(e => this.updateCurrentStock(order)),
flatMap(e => forkJoin([this.updateSalesSummary(order),this.updateAnotherDocument(order)])),
);
of(undefined).pipe(tasks).subscribe({
error: (err) => {
console.log(err);
obs.error(err);
},
complete: () => {
console.log('completed');
obs.complete();
}
});
It works, but i'm not sure if this is the right/cleanest way of doing it and if there is any possible issues in the future.
Using concat
concat will subscribe to your streams in order (not starting the next until the previous one completes.
This should be roughly equivalent.
One difference here is that unlike mergeMap, you're not transforming the output of an api call, it still gets emitted. Since you're not doing anything with the next callback in your subscription, it'll still look similar in the case.
concat(
this.createOrder(order),
this.updateCurrentStock(order),
forkJoin([
this.updateSalesSummary(order),
this.updateAnotherDocument(order)
])
).subscribe({
error: concosle.log,
complete: () => console.log('completed');
});
An Aside:
Here's how I would re-write your original code to be a bit easier to read.
this.createOrder(order).pipe(
mergeMap(_ => this.updateCurrentStock(order)),
mergeMap(_ => forkJoin([
this.updateSalesSummary(order),
this.updateAnotherDocument(order)
]),
).subscribe({
error: (err) => {
console.log(err);
obs.error(err); // <-- What's obs here?
},
complete: () => {
console.log('completed');
obs.complete();
}
});
There are a lot of rxjs operators, I recommend you read https://www.learnrxjs.io/learn-rxjs.
When you have an observable that depends on other's value use switchMap
Example:
const userId$: Observable<number>;
function getUserData$(userId: number): Observable<Data> {
// yourService.fetchUser(userId);
}
userId$.pipe(switchMap((userId: number) => getUserData$(userId)));
When you do not care about the order, you can use:
if you want to emit the last value when all observables complete: forkJoin
if you want to emit every value as any observable emits a value: combineLatest
Example:
const userId$: Observable<number>;
function getUserAge$(userId: number): Observable<number> {
// ...
}
function getUserName$(userId: number): Observable<String> {
// ...
}
userId$.pipe(
switchMap((userId: number) =>
forkJoin([getUserAge$(userId), getUserName$(userId)])
)
);
In your case I think the order does not matter, as none of your observables needs data from the previous one. So I think you should use combineLatest.
If the order of emission and subscription of inner observables is important, try concatMap.

Handling parallel nested observables in RxJS

I'm working on cleaning up nested parallel observables in my project and I am having some issues wrapping my head around handling the observable events. Here's one situation in my music application where I load the Playlist page and need to load the playlist data, information about the user, and if the user is following the playlist:
Route Observable (returns parameters from URL, I need to extract the playlistId)
Playlist Observable (needs playlistId and returns PlaylistObject and resolves)
User Observable (returns UserObject)
IsUserFollowing Observable (needs playlistId and UserObject.id and returns T/F if user is following the playlist)
In summary, I receive the route parameter from Angular's Route observable, I need to fire two other observables in parallel, the Playlist Observable and User Observable. The Playlist Observable can resolve once the playlist data is returned, but the User Observable needs to continue on to another observable, IsFollowingUser, until it can resolve.
Here is my current (bad) implementation with nested observables:
this.route.params.pipe(
first()
).subscribe((params: Params) => {
this.playlistId = parseInt(params["playlistId"]);
if (this.playlistId) {
this.playlistGQL.watch({
id: this.playlistId,
}).valueChanges.pipe(
takeUntil(this.ngUnsubscribe)
).subscribe((response) => {
this.playlist = response; // use this to display playlist information on HTML page
this.playlistTracks = response.tracks; //loads tracks into child component
});
this.auth.user$.pipe(
filter(stream => stream != null)
).subscribe((user) => {
this.user = user;
this.userService.isFollowingPlaylist(this.user.id, this.playlistId).subscribe((response) => {
this.following = response; //returns a T/F value that I use to display a follow button
})
})
}
This works but obviously does not follow RxJS best practices. What is the best way to clean up nested parallel observables like this?
This is something I would do although this may not be the most optimal but could be better than before.
I will assume the previous code block is in an ngOnInit.
async ngOnInit() {
// get the params as if it is a promise
const params: Params = await this.route.params.pipe(
first()
).toPromise();
this.playlistId = parseInt(params["playlistId"]);
if (this.playlistId) {
// can name these functions to whatever you like (better names)
this.reactToPlaylistGQL();
this.reactToUser();
}
}
private reactToPlaylistGQL() {
this.playListGQL.watch({
id: this.playListId,
}).valueChanges.pipe(
takeUntil(this.ngUnsubscribe)
).subscribe(response => {
this.playlist = response;
this.playlistTracks = response.tracks;
});
}
private reactToUser() {
this.auth.user$.pipe(
filter(stream => stream != null),
// switch over to the isFollowingPlaylist observable
switchMap(user => {
this.user = user;
return this.userService.isFollowingPlaylist(this.user.id, this.playListId);
}),
// consider putting a takeUntil or something of that like to unsubscribe from this stream
).subscribe(response => this.following = response);
}
I find naming the functions in the if (this.playlistId) block to human friendly names makes it unnecessary to write comments (the descriptive function name says what it should do) and looks cleaner than putting a blob of an RxJS stream.
I wrote this all freehand so there might be some typos.

Store result of query in local variable and send a confirmation for the same in Angular

My node server is giving me a response for a query result which I want to store in my Angular Service's Local Variable(movie) and pass a confirmation message({"result":true}) to the asking angular component.
redirect(id){
this.movie.getMovie(id).subscribe( confirmation =>{
if(confirmation.result) this.router.navigate(["/movies/movDetails"]);
});
}
This is my angular Component
getMovie(id):Observable<any>{
return this.http.post("http://localhost:3000/getMovie",{ "_id":id }).subscribe(IncomingValue => this.movie = IncomingValue).pipe(map( return {"result":true} ));
}
Service Component
When retrieving data, one often uses a get, not a post. This is what one of my simple gets looks like:
getProducts(): Observable<IProduct[]> {
return this.http.get<IProduct[]>(this.productUrl);
}
Using your code ... you can then use RxJS pipeable operators to perform additional operations:
getMovie(id):Observable<any>{
return this.http.post("http://localhost:3000/getMovie",{ "_id":id })
.pipe(
tap(IncomingValue => this.movie = IncomingValue),
map(() => {return {"result":true}} )
);
}
The first pipeable operator, tap, stores the incoming value in your local property.
The second pipeable operator, map, maps the result as the defined key and value pair.
Hope this helps.

Categories

Resources