Angular - properly nesting Observables and its subscriptions - javascript

I am using angularFire2 to extract my data from Firebase as Observable objects. Here is a simplified version of my code with some explanations to it below:
this.af.getObservable(`userChats/${this.userID}/`).subscribe((recentConversations) => {
recentConversations.forEach(conversation => {
this.af.getObservableSortByVar(`allChats/${conversation.conversationID}/`, "lastUpdate").subscribe((conversationData) => {
let userKey, lastMessage, lastMessageText, lastMessageSender, lastMessageDate;
for (var i = 0; conversationData.length > i; i++) {
switch (conversationData[i].key) {
case "messages": {
lastMessage = (conversationData[i][Object.keys(conversationData[i])[Object.keys(conversationData[i]).length - 1]]);
lastMessageText = lastMessage.message;
lastMessageSender = lastMessage.sender;
lastMessageDate = lastMessage.date;
}
case "users": {
userKey = conversationData[i].userKey;
}
}
}
this.recentChats.push(this.createConversationObject("username", userKey, lastMessageSender, lastMessageText, lastMessageDate));
});
});
});
Currently, I am making a call to the database to retrieve a list of all conversations of a user.
I receive an Observable object of all the conversations which I subscribe to since I want to keep the data up-to-date with the database.
I am then iterating through the conversations' Observable. I need to make a new database call for each iterated element(each conversation) in order to obtain information/metadata about it(content, senderID, date of conversation etc). Thus, I result in having two Observables - one nested into the other which both have been subscribed to.
After obtaining the contents/metadata of the conversation from the second observable, I push the metadata obtained, as an Object into an array called "recentChats".
This gets the job done when I execute this whole block of code once(the initial call at the start of the program). However, when the data in the database is modified(the 'userChat' node in the database or the 'allChats' node, or both!) and subscriptions are activated, and I get multiple (repetitive) calls of this whole block of code which floods my array with the same result multiple times.
I get unnecessary calls when I just want to have one single call to refresh the information.
And thus, I can see that my logic and understanding of Observables is not correct. Can someone explain what would be the proper solution of this example above? How can I nest Observable subscriptions without having repetitive (the same) calls?

I think with RxJS you should never have to write your code like that.
However, when the data in the database is modified(the 'userChat' node in the database or the 'allChats' node, or both!) and subscriptions are activated, and I get multiple (repetitive) calls of this whole block of code which floods my array with the same result multiple times.
Each time your outer Observable emits a value, you subscribe to each inner Observable again. That means you have for the same conversations multiple Subscriptions which get executed.
I suggest using operators to have only one Observable and subscribe once
Example (with RxJS 6 syntax, if you are below 5.5 it may look different) (maybe you have to use different operators):
this.af.getObservable(`userChats/${this.userID}/`).pipe(
// we map the array of conversations to an array of the (before inner) observables
map(recentConversations =>
recentConversations.map(conversation =>
this.af.getObservableSortByVar(`allChats/${conversation.conversationID}/`, 'lastUpdate'))),
// combine the observables. will emit a new value of ALL conversation data when one of the conversations changes
switchMap(recentConversations => combineLatest(recentConversations)),
// map each conversation to the conversation object (this is the code you had in your inner subscription)
map(conversations =>
conversations.map(conversationData => {
let userKey, lastMessage, lastMessageText, lastMessageSender, lastMessageDate;
for (let i = 0; conversationData.length > i; i++) {
switch (conversationData[i].key) {
case 'messages': {
lastMessage = (conversationData[i][Object.keys(conversationData[i])[Object.keys(conversationData[i]).length - 1]]);
lastMessageText = lastMessage.message;
lastMessageSender = lastMessage.sender;
lastMessageDate = lastMessage.date;
} // don't you need a "break;" here?
case 'users': {
userKey = conversationData[i].userKey;
}
}
}
return this.createConversationObject('username', userKey, lastMessageSender, lastMessageText, lastMessageDate);
}))
).subscribe(recentChats => this.recentChats = recentChats);

Related

complete dynamic array observables one by one (concatMap)

I am making a chat app and I would like to save messages in the correct order.
Imagine, I would have a static number of messages
// 4 messages. array of static length: 4
chatMessages: string[] = ['hello', 'world', 'and', 'stack overflow members']; //
now, let's create a save observables for them.
chatMessages: Observable<ChatMessage>[] = chatMessages.map((message: string) => {
return chatService.saveMessage(message); // returns an Observable to call API
})
Now, I want to save them one by one, one after another.
I do it this way:
from(chatMessages).pipe(
concatMap((observable) => observable),
toArray(),
take(1)
).subscribe();
Now My question is, if the initial chatMessages array is dynamic - (can be added a message in any point of time, even during saving).
How do I loop the array to save chat messages one by one, keeping the order they were added ?
For example: two out of four messages were saved, the 3rd is being processed, and in that moment the 5th message is added to the chatMessages array. How do I manage that?
If the initial chatMessages array is dynamic - (can be added a message in any point of time, even during saving)
You are describing an Observable of Message (string), not an array! Since you are processing items one at a time, there is no need for array.
You can just use a simple subject that emits messages as they are received and have one subscription to that stream that saves the messages for you:
chatMessage$ = new Subject<string>();
function saveMessage(message: string) {
chatMessage$.next(message);
}
chatMessage$.pipe(
concatMap(message => chatService.saveMessage(message))
).subscribe();
This will processes the new messages one at a time, in the correct order.
If I understand right the problem, you have to deal with with an array which can receive additional element over time and such elements have to be added at the end of the array.
This can be seen as a stream of messages, the ones originally stored in the array being the first elements of the stream, to which it is possible to add other messages over time, for instance calling a specific function addMessage(msg).
The code to build such stream could look like this
const myInitialArray = ['hello', 'world', 'and', 'stack overflow members']
function saveMessage(msg: string) {
return of(`saved: ${msg}`).pipe(delay(1000))
}
const add$ = new Subject<string>()
const myStream = merge(from(myInitialArray), add$).pipe(
concatMap(msg => saveMessage(msg))
)
Now what you have to do is to subscribe to myStream and, any time you want to add a message at the end of the array (or stream), you have to call the function addMessage.
This stackblitz shows the example working.

Problem with calling multiple asynchronous functions in ngOnInit

Good afternoon,
I am struggling with placing functions in ngOnInit. If I understand what is going wrong correctly, the problem appears to be that when I place asynchronous functions in ngOnInit and the next function depends on the complete execution of the first, if the first has not been completed, then the data I require is still undefined and it will fail.
What I want to do is quite simple, really. ngOnInit populates two arrays by querying my API. One is groups associated with a user, and another is all groups (these come from a DB). It will then populate a third array by comparing these two (allgroups - groupsassociated = groupsavailable).
After this initial populating, I want to be able to do further comparisons between the arrays depending on user interaction. These functions work, but about 10% of the time I will receive an undefined problem which can be cleared up by refreshing the page (sometimes two or three times). This suggests to me that it's a problem of order of operations.
How can I move forward here? Can I make these function calls synchronous? Can I chain them together somehow?
I appreciate any help.
Relevant code below:
NGONINIT AND THREE FUNCTIONS:
ngOnInit(): void {
this.populatePickListUserAssociatedGroups();
this.populateOriginalUserAssociatedGroups();
this.populateAvailableGroups();
}
// METHODS
private populatePickListUserAssociatedGroups = () => {
this.userDataService.getSingleUserAssociatedGroups()
.then(groups => this.pickListUserAssociatedGroups = groups)
.then(groups1 => this.originalUserAssociatedGroups = groups1);
}
private populateOriginalUserAssociatedGroups = () => {
this.userDataService.getSingleUserAssociatedGroups().then(groups => this.originalUserAssociatedGroups);
}
private populateAvailableGroups = () => {
this.userDataService.getAllGroups()
.then(groups => this.allGroups = groups)
.then(groups => groups.filter(i1 => !this.originalUserAssociatedGroups
.some(i2 => i1.id === i2.id)))
.then(groups => this.availableGroups = groups);
}
I have described my main problem, but additionally I have another. For some reason the two arrays: pickListUserAssociatedGroups and originalUserAssociatedGroups always change together. This defeats the purpose. The pick list array represents what the user changes (I'm working with ngPrime pick list) and originalUserAssociatedGroups is supposed to be unchanged and represents what is in the DB so I can later make comparisons between the two.
Hopefully this is clear.
Thanks in advance.
You could achieve that by using rxjs. There is multiple solution to your question but I think the best is to use a forkJoin. Here is how you could achieve that :
ngOnInit() {
forkJoin(
this.populatePickListUserAssociatedGroups(),
this.populateOriginalUserAssociatedGroups(),
).subscribe(finalResult => {
// do your things with finalResult[0] and finalResult[1]
this.populateAvailableGroups();
});
}
populatePickListUserAssociatedGroups(): Observable<any> {
return this._myService.getPickListUserAssociatedGroups();
}
populateOriginalUserAssociatedGroups(): Observable<any> {
return this._myService.getOriginalUserAssociatedGroups();
}
populateAvailableGroups() {
// your code
}
Note that i use observable, not promise. You might need to add a value check in the subscribe to be sure you have the value you need, but the documentation says :
This operator is best used when you have a group of observables and only care about the final emitted value of each.
If your html depends on the data your methods retrieve, you'll have to adapt it, by adding a loader for example, to wait for the subscribe to end since it is asynchronous.
Finally, your service method should return an observable, and you can do it by simply retunring that response :
myServiceMethod(): Observable<any> {
return this._http.get<Whatever>(url);
}
For your other problem, if the changes of the pick list array is made locally in your constructor then you just have to store it in a variable in the forkJoin subscription : this.mypickListArray = finalResult[0] and just work with it.
In case anyone sees the above answer from #Quentin, I thought I would post the code that I ultimately used which worked as intended and to which I am thankful for Quentin's help.
ngOnInit(): void {
forkJoin(
[
this.populateAllGroups(),
this.populateAllUserGroup()
]
)
.subscribe(([x, y]) => {
this.allGroups = x;
this.allUserGroupByUser = y;
this.populateAvailableGroups();
this.populatePickListUserAssociatedGroups();
});
}
Where, for example, the first method looks like this:
private populateAllGroups(): Observable<any> {
return this.userDataService.getAllGroups();
}
And the data service has this method:
public getAllGroups(): Observable<any> {
return this.httpClient.get<Group[]>(`${Configuration.userApiURL}/getallgroups`);
}
The secondary methods are not relevant as they just manipulate the data provided.

Determining what has been added/deleted/changed in a Firestore list

Firestore as the backend. I've managed to get through by simply using basic crud methods. However, I wanted to find out how do I determine the changes to a list of items that are returned after the initial subscription.
What I'm ultimately looking to do is :
- miminise the amount of documents that are read each time
- animate a list of items (entry animation, exit animation, change animamtion)
In the following example I have the basic crud method along with the initial subscription:
posts:post [] = [];
constructor(private db: AngularFirestore){}
ngOnInit(){
//The initial subscription to the posts
this.db.collection("Posts").valuechanges().subscribe( _posts => {
this.posts = _posts;
});
async addItem(_post:post)
{
_post.id = this.db.createId();
await this.db.collection("Posts").doc(_post.id).set(_post);
}
async update(_post:post)
{
await this.db.collection("Posts").doc(_post.id).update(_post);
}
delete (_post:post)
{
await this.db.collection("Posts").doc(_post.id).delete();
}
With the above methods, I'm subscribing to the documents in the Posts collection. Initially I'm receiving an arrray of type Post, and whenever another item is added, updated, removed i'm receiving an updated array of of type post.
How do I differentiate what has happened to the item so I can animate the changes (i.e animate the entry of the item etc...) ?
It would really help me out if you could show a sample code ?
Thanks
The valueChanges observable only exposes the actual data in the document. It has no other metadata about the document, nor the kind of change.
If you need more information, listen for documentChanges instead. That exposes a stream of DocumentChangeAction objects, which amongst others contain a type property that is the DocumentChangeType.
See https://github.com/angular/angularfire2/blob/master/docs/firestore/documents.md#the-documentchangeaction-type

How to create an array from rxjs without completion of the adding sequence

I'm trying to figure out an rxjs way of doing the following:
You have two observables, one onAddObs and onRemoveObs.
let's say onAddObs.next() fires a few times, adding "A", "B", "C".
I would like to then get ["A", "B", "C"].
.toArray requires the observable be completed...yet more could come.
That's the first part. The second part is probably obvious...
I want onRemoveObs to then remove from the final resulting array.
I don't have a plunkr cuz I can't get anything close to doing this...
Thanks in advance!
UPDATE
Based on user3743222's advice, I checked out .scan, which did the job!
If anyone else has trouble with this, I've included an angular2 service which shows a nice way of doing this. The trick is to use .scan and instead of streams of what was added/removed, have streams of functions to add/remove, so you can call them from scan and pass the state.
#Injectable()
export class MyService {
public items: Observable<any>;
private operationStream: Subject<any>;
constructor() {
this.operationStream = Subject.create();
this.items = this.operationStream
// This may look strange, but if you don't start with a function, scan will not run....so we seed it with an operation that does nothing.
.startWith(items => items)
// For every operation that comes down the line, invoke it and pass it the state, and get the new state.
.scan((state, operation:Function) => operation(state), [])
.publishReplay(1).refCount();
this.items.subscribe(x => {
console.log('ITEMS CHANGED TO:', x);
})
}
public add(itemToAdd) {
// create a function which takes state as param, returns new state with itemToAdd appended
let fn = items => items.concat(itemToAdd);
this.operationStream.next(fn);
}
public remove(itemToRemove) {
// create a function which takes state as param, returns new array with itemToRemove filtered out
let fn = items => items.filter(item => item !== itemToRemove);
this.operationStream.next(fn);
}
}
You can refer to the SO question here : How to manage state without using Subject or imperative manipulation in a simple RxJS example?. It deals with the same issue as yours, i.e. two streams to perform operations on an object.
One technique among other, is to use the scan operator and a stream of operations which operates on the state kept in the scan but anyways go have a look at the links, it is very formative. This should allow you to make up some code. If that code does not work the way you want, you can come back and ask a question again here with your sample code.

How to restart or refresh an Observable?

I've got a TypeScript/Angular 2 Observable that works perfectly the first time I call it. However, I'm interested in attaching multiple subscribers to the same observable and somehow refreshing the observable and the attached subscribers. Here's what I've got:
query(): Rx.Observable<any> {
return this.server.get('http://localhost/rawData.json').toRx().concatMap(
result =>
result.json().posts
)
.map((post: any) => {
var refinedPost = new RefinedPost();
refinedPost.Message = post.Message.toLowerCase();
return refinedPost;
}).toArray();
}
Picture that there is a refresh button that when pressed, re-executes this observable and any subscribers that are connected to it get an updated set of data.
How can I accomplish this?
I don't know so much about Angular2 and Typescript, but typescript being a superset of javascript, I made the following hypothesis (let me know if I am wrong) which should make the following javascript code work :
server.get returns a promise (or array or Rx.Observable)
posts is an array of post
You would need to create an observable from the click event on that button, map that click to your GET request and the rest should be more or less as you wrote. I cannot test it, but it should go along those lines:
// get the DOM id for the button
var button = document.getElementById('#xxx');
// create the observable of refined posts
var query$ =
Rx.Observable.fromEvent(button, 'click')
.flapMapLatest(function (ev) {
return this.server.get('http://localhost/rawData.json')
})
.map(function (result){return result.json().posts})
.map(function (posts) {
return posts.map(function(post){
var refinedPost = new RefinedPost();
refinedPost.Message = post.Message.toLowerCase();
return refinedPost;
})
})
.share()
// subscribe to the query$. Its output is an array of refinedPost
// All subscriptions will see the same data (hot source)
var subscriber = query$.subscribe(function(refinedPosts){console.log(refinedPosts)})
Some explanation :
Every click will produce a call to server.get which returns an observable-compatible type (array, promise, or observable). That returned observable is flattened to extract result from that call
Because the user can click many times, and each click generate its flow of data, and we are interested (another hypothesis I make here too) only in the result of the latest click, we use the operator flatMapLatest, which will perform the flatMap only on the observable generated by the latest click
We extract the array of post posts and make an array of refinedPost from it. In the end, every click will produce an array of refinedPost which I assume is what you want
We share this observable as you mention you will have several subscribers, and you want all subscribers to see the same data.
Let me know if my hypotheses are correct and if this worked for you.
In addition I recommend you to have a look at https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
In addition to being a very good reminder of the concepts, it addresses a refresh server call problem very similar to yours.

Categories