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.
Related
I have an observable that gets data from an API, this is hooked up to a paginating feature. How do I add the response data object on to the variable which holds the first pages data. Essentially what Array push does.
Right now each pagination will send a request to the API, even the previous pages since I am overwriting the data. I would like to add the array of objects (data.articles) on to the end of the category that is selected.
data = {
general: undefined,
business: undefined,
entertainment: undefined,
health: undefined,
science: undefined,
sports: undefined,
technology: undefined
};
this.newsService.getNews(endpoint, params).subscribe(data => {
this.data[category] = data.articles;
});
I expect the data to work like this:
If i show 1 article on the general tab, data.general will hold an array with one object in it. When i click next page, data.general should hold an array with 2 objects in it, and so on.
Solution
Array.prototype.push worked, for some reason when i initially tried this it didn't work. But i think that was because it was undefined initially. The below however works fine.
if (!this.data[category]) {
this.data[category] = data.articles;
} else {
Array.prototype.push.apply(this.data[category], data.articles);
}
Try this, maybe this is what you want.
Array.prototype.push.apply(this.data[category], data.articles);
Above code will merge to array of objects into one and set to this.data[category]
I know you have already accepted an answer, but if you were interested in retaining the data in streams instead of in arrays, you can do something like this:
// Action Stream
private productInsertedSubject = new Subject<Product>();
productInsertedAction$ = this.productInsertedSubject.asObservable();
// Merge the streams
productsWithAdd$ = merge(
this.products$, // Original stream of data
this.productInsertedAction$ // New data
)
.pipe(
scan((acc: Product[], value: Product) => [...acc, value]),
catchError(err => {
console.error(err);
return throwError(err);
})
);
And if you were adding an array instead of a single value, the technique would be similar.
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
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);
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.
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.