I've made this code that do its work, using RxJS:
from(dateQuery.first())
.subscribe((result) => {
query.greaterThanOrEqualTo('createdAt', result.createdAt);
const dateFormat = moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss');
from(query.find())
.map(el => el.map((e) => {
return {
id: e.id,
username: e.get('username')
}
}))
.mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
}))
.subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed'));
},
(error) => console.log(error)
);
My question: is it possible to create only one subscription instead of this two? Because I need to use result of the first subscription also in the other mergeMap before and I don't know how to store it if I try to do only one subscription.
Thank you for help.
You can do something like that
from(dateQuery.first()).pipe(
tap(() => query.greaterThanOrEqualTo('createdAt', result.createdAt)),
map(result => moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss')),
mergeMap(dateFormat => from(query.find()).pipe(
map(el => el.map((e) => ({
id: e.id,
username: e.get('username')
}))),
mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
})),
tap(() => res.send(serialize(next))),
catchError(error => of('Find error'))
))
).subscribe();
Here's my first idea from what I understood.
I have exported the first subscription in an observable and I reuse it to construct the second observable (find$) where I do the second subscription.
I don't know what query.greaterThanOrEqualTo('createdAt', result.createdAt) does, so I put it in a .do() operator as a side effect. If you need to execute this method before executing the remaining code, return an observable or a promise from it and use a mergeMap as well.
const dataQueryFirst$ = dateQuery.first();
const find$ = dataQueryFirst$
.do((result) => query.greaterThanOrEqualTo('createdAt', result.createdAt))
.mergeMap(result => {
const dateFormat = moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss');
return from(query.find())
.map(el => el.map((e) => {
return {
id: e.id,
username: e.get('username')
}}))
.mergeMap((array) => Observable.of({
names: array,
lastUpdate: dateFormat
}))
});
find$.subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed')
);
I would implement it with a "switchMap" .
Plus i would extract some logic in functions to make it more readable. And i prefere the new "pipe" way of rxjs since v5.5.
from(dateQuery.first()).pipe(
// TAP because it seems to be a sideeffect. If this could stop your stream, "filter" would be appropriate
tap(result =>query.greaterThanOrEqualTo('createdAt', result.createdAt)),
// MAP because we can discard "result" and only need the Moment-Object in the rest of the stream
map(result => getMoment(result.createdAt),
// switchMap to switch to the second stream, because it is only one command (in a long pipe) you don´t need a explicit return-command
switchMap( createdAt =>
from(query.find()).pipe(
// MAP because we now want a UserObjects-Array in our stream
map(el => createUserObjects(el)),
// MAP because we want our NamesUpdateList. Depending on the usage you may not need "Observable.of()" here
map(userObjects => createNamesUpdateList(userObjects, createdAt ))
)
)
).subscribe(
(next) => res.send(serialize(next)),
(error) => res.send(serialize(error)),
() => console.log('completed')
)
getMoment(date){
return moment(result.createdAt).format('DD/MM/YYYY - HH:mm:ss')
}
createUserObjects(el){
el.map((e) => {
return {
id: e.id,
username: e.get('username')
}
}
}
createNamesUpdateList(array, dateFormat){
return {
names: array,
lastUpdate: dateFormat
}
}
warm regards
Related
i have this following function
file: subcategory.service.ts
getSubCategoriesById(inp_subCatid: String): Observable<any>{
this.getSubCategoriesList().snapshotChanges().pipe(
map(changes =>
changes.map(c =>
({ key: c.payload.key, ...c.payload.val() })
)
)
).subscribe(subCategories => {
subCategories.filter(function (subCat) {
return subCat.id == inp_subCatid;
});
});
and i´m calling the top function in the following file
file: subcategory.page.ts
this.SubCategoryService.getSubCategoriesById(subCatid).subscribe((subCategories: any) => {
this.subCat = subCategories ;
})
the problem what i got is i´m getting following error message:
ERROR TypeError: "this.SubCategoryService.getSubCategorysById(...) is undefined"
i want to get the data when there are loaded from the file "subcategory.service.ts"
hope someone can help me.
Your method should be like this:
getSubCategories(inp_subCatid: string): Observable<any> {
return this.getSubCategoriesList().snapshotChanges().pipe(
map(changes => changes.map(c =>
({ key: c.payload.key, ...c.payload.val() })
).filter((subCat) => subCat.id === inp_subCatid)
));
}
Then you will be able to use like this:
this.subCategoryService.getSubCategories(subCatid)
.subscribe(subCategories => this.subCat = subCategories);
If I'm interpreting correclty your methods, it seems to me that you're using firebase... if so, after you call this.yourService.getSubCategories(subCatid) for the first time, your subscription will remain active so that your subcategories will be updated for every change on the database, even if you change subCatid, the previous database query will be alive. To avoid it, I suggest that you take just one emission of snapshotChanges():
getSubCategories(inp_subCatid: string): Observable<any> {
return this.getSubCategoriesList().snapshotChanges().pipe(
// finish the subscription after receiving the first value
take(1),
map(changes => changes.map(c =>
({ key: c.payload.key, ...c.payload.val() })
).filter((subCat) => subCat.id === inp_subCatid)
));
}
Thanks a lot
what if i want to filter for a specific data ?? like for "id"
getSubCategoriesbyId(inp_subCatid): Observable<any>{
this.getSubCategoriesList().snapshotChanges().pipe(
map(changes =>
changes.map(c =>
({ key: c.payload.key, ...c.payload.val() })
)
)
).subscribe(subCategories => {
subCategories.filter(function (subCat) {
return subCat.id == inp_subCatid;
});
});
}
and then to get the filtered data back
this.yourService.getSubCategoriesbyId(subCatid)
.subscribe(subCategories => console.log(subCategories));
I am trying to get a member value from other web api in rxjs.
I did this in a pipe method with switchMap. But if there is a problem with getting the member value then I want to skip old model with values to next method.
So I dont want to return null after switchMap worked.
Here is my code:
(this.repository.getData(`employeecards/list/${this.currentUser.companyId}`) as Observable<EmployeeCard[]>)
.pipe(
flatMap(emp => emp),
tap(emp => emp),
switchMap((empCard: EmployeeCard) => this.repository.getData(`cards/${empCard.cardId}`),(empCard, card) => ({ empCard, card }) ),
//second subscribe
switchMap((emp: { empCard: EmployeeCard, card: LogisticCard }) => this.parraApiService.getBalanceByBarcode(emp.card.barcode),
(emp: { empCard: EmployeeCard, card: LogisticCard }, apiRes: ParraApiResult) => {
if (apiRes.response.isSuccess) {
//I use this subscribe only set to balance
emp.empCard.balance = apiRes.response.data['balance'];
}
return emp.empCard
}),
catchError((e) => {
return of([]); //I want to retun emp value
}),
reduce((acc, value) => acc.concat(value), [])
)
How can I solve this problem?
Thanks
I think you can achieve that by creating a closure:
(this.repository.getData(`employeecards/list/${this.currentUser.companyId}`) as Observable<EmployeeCard[]>)
.pipe(
flatMap(emp => emp),
tap(emp => emp),
switchMap((empCard: EmployeeCard) => this.repository.getData(`cards/${empCard.cardId}`), (empCard, card) => ({ empCard, card })),
switchMap(
(emp: { empCard: EmployeeCard, card: LogisticCard }) => this.parraApiService.getBalanceByBarcode(emp.card.barcode)
.pipe(
map((emp: { empCard: EmployeeCard, card: LogisticCard }, apiRes: ParraApiResult) => {
if (apiRes.response.isSuccess) {
emp.empCard.balance = apiRes.response.data['balance'];
}
return emp.empCard
}),
catchError((e) => {
// `emp` available because of closure
return of(emp);
}),
)
),
reduce((acc, value) => acc.concat(value), [])
)
Also notice that I gave up on switchMap's custom resultSelector, as it can easily be replaced with a map operator.
I get an Observable<Group[]> from my Firebase collection.
In this Group class is an id which I wanna use to retrieve another dataset array from Firebase, which would be messages for each unique group Observable<Message[]>.(each group has its own chat: Message[])
And it want to return an observable which hold an array of a new Type:
return { ...group, messages: Message[] } as GroupWithMessages
the final goal should be Observable<GroupWithMessages[]>
getGroupWithChat(): Observable<GroupWithMessages[]> {
const groupColl = this.getGroups(); // Observable<Group[]>
const messages = groupColl.pipe(
map(groups => {
return groups.map(meet => {
const messages = this.getMessagesFor(group.uid);
return { messages:messages, ...group} as GroupWithMessages
});
})
);
return messages;
}
}
and here the Message function
getMessagesFor(id: string): Observable<Message[]> {
return this.afs.collection<Message>(`meets/${id} /messages`).valueChanges();
}
sadly that doesnt work because when i create the new Obj I cannot bind messages:messages because messages ist vom typ Observable<Message[]>
I hope that cleares things
UPDATE:
my main problem now comes down to this:
getGroupsWithMessages() {
this.getJoinedGroups()
.pipe(
mergeMap(groups =>
from(groups).pipe(
mergeMap(group => {
return this.getMessagesFor(group.uid).pipe(
map(messages => {
return { ...group, messages } as GroupIdMess;
})
);
}),
tap(x => console.log('reaching here: ', x)),
toArray(),
tap(x => console.log('not reaching here = completed: ', x))
)
),
tap(x => console.log('not reaching here: ', x))
)
.subscribe(x => console.log('not reaching here: ', x));
}
when i call that function my console.log is as follows:
Not sure if I follow what you're doing here but the logic look like you'd want:
getGroupWithChat() {
return this.getGroups.pipe(map(groups=> {
return groups.map(group => this.getMessagesFor(group.uid));
})).subscribe(); // trigger "hot" observable
}
Let me know if I can help further after you clarify.
UPDATE:
So it looks like you need to get the UID of the group before making the call to get the GroupMessages[]?
get Group: Observable
call getMessagesFor(Group.uid)
this example gets groups result$ then
concatMap uses groups result$ to make the messages query
this.getGroups().pipe(
concatMap((group: Group) => this.getMessagesFor(group.uid))
).subscribe((messages: GroupWithMessages[]) => {
console.log(messages);
});
You may still want to map them together but it seems like you know how to do that. concatMap waits for the first to finish, then makes the second call which you need.
Is this closer?
Use forkJoin to wait for messages to be received for all groups. Then map the result of forkJoin to an array of GroupWithMessages like this -
getGroupWithChat(): Observable<GroupWithMessages[]> {
return this.getGroups()
.pipe(
switchMap(groups => {
const messagesForAllGroups$ = groups.map(group => this.getMessagesFor(group.uid));
return forkJoin(messagesForAllGroups$)
.pipe(
map(joined => {
//joined has response like -
//[messagesArrayForGroup0, messagesArrayForGroup1, messagesArrayForGroup2....];
const messagesByGroup = Array<GroupWithMessages>();
groups.forEach((group, index) => {
//assuming that GroupWithMessages has group and messages properties.
const gm = new GroupWithMessages();
gm.group = group;
gm.messages = joined[index];
messagesByGroup.push(gm);
});
return messagesByGroup;
})
)
})
)
}
I usually do that by splitting Observable<any[]> to Observable<any> and then mergeMap the results to inner Observable.
Something like this should work:
getMessagesFor(id: string): Observable<number> {
return of(1);
}
getGroups(): Observable<string[]> {
return of(["1", "2"]);
}
getGroupWithChat() {
this.getGroups().pipe(
mergeMap(groups => from(groups)), // Split the stream into individual group elements instead of an array
mergeMap(group => {
return this.getMessagesFor(group).pipe(
map(messages => {
return Object.assign(group, messages);
})
);
})
);
}
Edit:
Consider BehaviorSubject. It doesn't complete at all:
const behSub: BehaviorSubject<number[]> = new BehaviorSubject([1, 2, 3]);
setTimeout(() => {
behSub.next([4, 5, 6]);
}, 5000);
behSub
.pipe(
mergeMap(arr =>
from(arr).pipe(
tap(), // Do something with individual items, like mergeMap to messages
toArray() // Go back to array
)
)
)
.subscribe(console.log, null, () => {
console.log('Complete');
});
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);
})
In my code I am getting a list of users from MS Graph, but this lists all users. I want to emit only those users that are a member of a specified group. So I have set up a filter:
private loadUsers() {
console.log(`Loading users...`);
this.graph.get$(`users`)
.map(resp => resp.json().value)
.flatMap(user => user)
.do(user => {
console.log(`Got user: ${user['displayName']}`);
})
.filter(user => {
// we only want users who are members of the TrackerContact group
const userId: string = user['id'];
console.log(`Checking user ${userId}`);
return this.userIsTrackerContact(user); // <<= PROBLEM HERE!
})
.subscribe(_ => {
console.log(`Completed user fetch!`);
});
}
The problem is in order to find out what groups a user is a member of, I must make another REST call back to MS Graph:
private userIsTrackerContact(user): Observable<boolean> {
// get the groups this user is a member of
return this.graph.get$(`users/${user['id']}/memberOf`)
.map(resp => resp.json().value)
.do(groups => {
console.log(`${user['displayName']} is a member of ${groups.length} groups:`);
groups.forEach((group, idx) => {
console.log(` [${idx+1}]: ${group['displayName']}`);
})
})
.map(groups => {
const found = groups.find((group) => group['id'] === this.authConfig.azureGroups.trackerContact)
return !isUndefined(found);
})
}
It does not seem to me that it's possible to filter an Observable based on another observable. The predicate test must be a simple true/false test.
How can I filter an Observable based on the results of another Observable?
.concatAll() might be better:
this.graph.get$(`users`)
.map(resp => resp.json().value)
.flatMap(user => user)
.do(user => {
console.log(`Got user: ${user['displayName']}`);
})
.filter(user => {
// we only want users who are members of the TrackerContact group
const userId: string = user['id'];
console.log(`Checking user ${userId}`);
return this.userIsTrackerContact(user); // <<= PROBLEM HERE!
})
.concatAll()
.subscribe(...)
I believe you should be able to merge the async observables then filter.
Rx.Observable.merge(this.graph.get$(`users`)
.map(resp => resp.json().value)
.flatMap(user => user)
.do(user => {
console.log(`Got user: ${user['displayName']}`);
})
.map(user => {
// we only want users who are members of the TrackerContact group
const userId: string = user['id'];
console.log(`Checking user ${userId}`);
return this.userIsTrackerContact(user); // <<= PROBLEM HERE!
}))
.filter(x => x)
.subscribe(_ => console.info('success'));
You can change your predicate set a property on user isGroupMember. Then filter(x => x.isGroupMember). Then you have the user objects in the subscribe method.