Handling parallel nested observables in RxJS - javascript

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.

Related

Can't get data from .pipe(map())

Got some problems with observable.
I have a function, with one returns me an Observable.
public getData(userId) {
const data = this.execute({userId: userId});
return {event: "data.get", data: data}
}
private execute(input: SomeDto): Observable<SomeRefType[]> {
return this.databaseGateway.queryMany(DatabaseCommand.WebRecordGetbyparticipantid, {
parameters: {
prm_contextuserid: input.userId,
prm_filterparticipantids: null,
prm_filtertext: null
}
}).pipe(map(res => res));
}
Type what pipe(map) returns
What I'm got when trying to return or log data
Question: Why .pipe(map(res => res)) don't work? What am I doing wrong?
For sure, I can read data from .pipe(take(1)).subscribe(data => console.log(data)), but, how can I return data from construction like this?
Thanks everyone! Have a good day!
As said in the Rxjs documentation observable are lazy computation.
It means the as long as you don't subscribe to them they won't do anything. It exists two ways to trigger a subscription.
Either within a ts file using .susbcribe() or within a view when calling an endpoint.
If you're using nestjs it would be when calling the url defined within a #Controller('') with an http verb like #Get('path')
By convention you suffix observable variables with $: data$ = new Observable<any>().
At some point you'll have to convert the observable to a promise. Best to do it early and convert it using firstValueFrom immediately after the query.
Then convert your caller to an async method to use the returned value.
public async getData(userId): Promise<{ event: string, data: SomeRefType[] }> {
const data = await this.execute({userId: userId});
return { event: 'data.get', data };
}
private execute(input: SomeDto): Promise<SomeRefType[]> {
const res$ = this.databaseGateway.queryMany(DatabaseCommand.WebRecordGetbyparticipantid, {
parameters: {
prm_contextuserid: input.userId,
prm_filterparticipantids: null,
prm_filtertext: null
}
});
return firstValueFrom(res$);
}

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.

Angular subscribe within subscribe: data doesn't load at the same time within view

I know it is bad practice to call subscribe within subscribe but I don't know how to handle it differently with my special case.
The code as it is now works, but my problem is that if I update my website for example every second, parts of the table are loaded first and other parts are loaded afterwards (the content of the subscibe within my subscribe).
I have a service containing a function that returns an Observable of a list of files for different assets.
Within that function I request the filelist for each asset by calling another service and this service returns observables.
I then iterate over the elements of that list and build up my data structures to return them later on (AssetFilesTableItems).
Some files can be zip files and I want to get the contents of those files by subscribing to another service (extractZipService). To be able to get that correct data I need the name of the file which I got by requesting the filelist. I then add some data of the zip contents to my AssetFilesTableItems and return everything at the end.
The code of that function is as follows:
getAssetfilesData(assetIds: Array<string>, filter: RegExp, showConfig: boolean): Observable<AssetFilesTableItem[][]> {
const data = assetIds.map(assetId => {
// for each assetId
return this.fileService.getFileList(assetId)
.pipe(
map((datasets: any) => {
const result: AssetFilesTableItem[] = [];
// iterate over each element
datasets.forEach((element: AssetFilesTableItem) => {
// apply regex filter to filename
if (filter.test(element.name)) {
this.logger.debug(`Filter ${filter} matches for element: ${element.name}`);
// build up AssetFilesTableItem
const assetFilesItem: AssetFilesTableItem = {
name: element.name,
type: element.type,
asset: assetId
};
// save all keys of AssetFilesTableItem
const assetFilesItemKeys = Object.keys(assetFilesItem);
// if file is of type ZIP, extract 'config.json' from it if available
if (showConfig && element.type.includes('zip')) {
this.extractZipService.getJSONfromZip(assetId, element.name, 'config.json')
.subscribe((configJson: any) => {
const jsonContent = JSON.parse(configJson);
const entries = Object.entries(jsonContent);
entries.forEach((entry: any) => {
const key = entry[0];
const value = entry[1];
// only add new keys to AssetFilesTableItem
if (!assetFilesItemKeys.includes(key)) {
assetFilesItem[key] = value;
} else {
this.logger.error(`Key '${key}' of config.json is already in use and will not be displayed.`);
}
});
});
}
result.push(assetFilesItem);
}
});
return result;
}));
});
// return combined result of each assetId request
return forkJoin(data);
}
}
I update my table using the following code within my component:
getValuesPeriodically(updateInterval: number) {
this.pollingSubscription = interval(updateInterval)
.subscribe(() => {
this.getAssetfilesFromService();
}
);
}
getAssetfilesFromService() {
this.assetfilesService.getAssetfilesData(this.assetIds, this.filterRegEx, this.showConfig)
.subscribe((assetFilesTables: any) => {
this.assetFilesData = [].concat.apply([], assetFilesTables);
});
}
Edit: I tried ForkJoin, but as far as I understandit is used for doing more requests in parallel. My extractZipService though depends on results that I get from my fileService. Also I have a forkJoin at the end already which should combine all of my fileList requests for different assets. I don't understand why my view is not loaded at once then.
EDIT: The problem seems to be the subscribe to the extractZipService within the forEach of my fileService subscribe. It seems to finish after the fileService Subscribe. I tried lots of things already, like SwitchMap, mergeMap and the solution suggested here, but no luck. I'm sure it's possible to make it work somehow but I'm running out of ideas. Any help would be appreciated!
You are calling this.extractZipService.getJSON inside a for loop. So this method gets called asynch and your function inside map is not waiting for the results. When result does come as your items are same which is in your view they get refreshed.
To solve this you need to return from this.extractZipService.getJSON and map the results which will give you a collections of results and then you do forkJoin on results ( Not sure why you need to forkjoin as there are just the objects and not API's which you need to call )
this.logger.debug(`ConfigJson found for file '${element.name}': ${configJson}`);
const jsonContent = JSON.parse(configJson);
const entries = Object.entries(jsonContent);
entries.forEach((entry: any) => {
// code
});
complete code should look on similar lines :-
getAssetfilesData(assetIds: Array<string>, filter: RegExp, showConfig: boolean): Observable<AssetFilesTableItem[][]> {
const data = assetIds.map(assetId => {
// for each assetId
return this.fileService.getFileList(assetId)
.pipe(
map((datasets: any) => {
// iterate over each element
datasets.forEach((element: AssetFilesTableItem) => {
return this.extractZipService.getJSONfromZip(assetId, element.name,
'config.json')
});
})).map((configJson: any) => {
// collect your results and return from here
// return result
});;
});
// return combined result of each assetId request
return forkJoin(data);
}
}
I have created a Stackblitz(https://stackblitz.com/edit/nested-subscribe-solution) which work along the same lines. You need to use concatMap and forkJoin for getting all the results.
Hope this helps.

Subscribe onComplete never completes with flatMap

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.

Typescript Resolve Observable to Entity

I want to resolve an observable to it's underlying type. Yes, I am aware I should NOT be doing so, but at the moment I would like to test something without Fixing My architecture, and I would like to know if its possible.
This is a function in the base provider to call a GET request
getByRoute(url: string): Observable<T> {
return this.http.get(url).map(res => <T>res.json());
}
I had an only class returning a concrete object with just an Id on it.
//public currentUser = { id: 1 };
But now I'm trying to implement my provider to lazy load the current user, I don't have time right now to switch everything to use an observable, but I would like to know my other code works.
private _user: User;
get currentUser(): User {
if (this._user == null) {
this._user = this.getLoggedInUser();
}
return this._user;
}
set currentUser(user: User) {
this._user = user;
}
constructor(public usersProvider: Users) {
}
getLoggedInUser(): User {
return this.usersProvider.getByRoute(URL).single();
};
How can I Resolve my Observable to get the entity?
You will need to subscribe to the observable and then assign it to the property on your class.
this.usersProvider.getByRoute(URL).subscribe(next => {
this.currentUser = next;
});
Note that you will need to handle cleanup of this subscription on your own! (this thread will help with that: Angular/RxJs When should I unsubscribe from `Subscription`)
Also, you can't really "resolve" an Observable -- it is not a Promise! Check out this thread for more on that: Angular - Promise vs Observable

Categories

Resources