ReactiveX: Group and Buffer only last item in each group - javascript

How to group an Observable, and from each GroupedObservable keep in memory only the last emitted item?
So that each group would behave just like BehaviorSubject.
Something like this:
{user: 1, msg: "Anyone here?"}
{user: 2, msg: "Hi"}
{user: 2, msg: "How are you?"}
{user: 1, msg: "Hello"}
{user: 1, msg: "Good"}
So in memory we'd have only have the last item for each user:
{user: 2, msg: "How are you?"}
{user: 1, msg: "Good"}
And when a subscriber subscribes, these two items were issued right away (each in it's own emission). Like we had BehaviorSubject for each user.
onCompleted() is never expected to fire, as people may chat forever.
I don't know in advance what user values there can be.

I assume your chatlog observable is hot. The groupObservables emitted by #groupBy will consequently also be hot and won't keep anything in memory by themselves.
To get the behavior you want (discard everything but the last value from before subscription and continue from there) you could use a ReplaySubject(1).
Please correct me if I'm wrong
see jsbin
var groups = chatlog
.groupBy(message => message.user)
.map(groupObservable => {
var subject = new Rx.ReplaySubject(1);
groupObservable.subscribe(value => subject.onNext(value));
return subject;
});

You can write the reducing function that turns out the latest emitted items of grouped observables, pass that to a scan observable, and use shareReplay to recall the last values emitted for new subscribers. It would be something like this :
var fn_scan = function ( aMessages, message ) {
// aMessages is the latest array of messages
// this function will update aMessages to reflect the arrival of the new message
var aUsers = aMessages.map(function ( x ) {return x.user;});
var index = aUsers.indexOf(message.user);
if (index > -1) {
// remove previous message from that user...
aMessages.splice(index, 1);
}
// ...and push the latest message
aMessages.push(message);
return aMessages;
};
var groupedLatestMessages$ = messages$
.scan(fn_scan, [])
.shareReplay(1);
So what you get anytime you subscribe is an array whose size at any moment will be the number of users who emitted messages, and whose content will be the messages emitted by the users ordered by time of emission.
Anytime there is a subscription the latest array is immediately passed on to the subscriber. That's an array though, I can't think of a way how to pass the values one by one, at the same time fulfilling your specifications. Hope that is enough for your use case.
UPDATE : jsbin here http://jsfiddle.net/zs7ydw6b/2

Related

Discord.js Role Assign and Looping Issues

I am not very efficient with my code which may be the reasons why this keeps failing. I am trying to remove and assign roles to "verified" users. The basic gist of the code is to loop through all "verified" users and assign them appropriate roles according to the data received from the API.
const fetch = require("node-fetch");
var i = 0;
function mainLoop(
guild,
redisClient,
users,
main_list,
Pilot,
Astronaut,
Cadet,
main_guild,
cadet_guild,
guest
) {
setTimeout(function () {
redisClient.GET(users[i], async function (err, reply) {
if (reply != null) {
var json = await JSON.parse(reply);
var uuid = Object.keys(json).shift();
if (Object.keys(main_list).includes(uuid)) {
var tag = users.shift();
var rank = main_list[uuid];
console.log(`${tag}: ${rank}`);
var role = guild.roles.cache.find(
(role) => role.name === `| ✧ | ${rank} | ✧ |`
);
await guild.members.cache.get(tag).roles.remove(guest);
await guild.members.cache.get(tag).roles.remove(Astronaut);
await guild.members.cache.get(tag).roles.remove(Cadet);
await guild.members.cache.get(tag).roles.remove(Pilot);
await guild.members.cache.get(tag).roles.remove(cadet_guild);
await guild.members.cache.get(tag).roles.add(main_guild);
await guild.members.cache.get(tag).roles.add(role);
} else {
var tag = users.shift();
console.log(`${tag}: Guest`);
await guild.members.cache.get(tag).roles.remove(Astronaut);
await guild.members.cache.get(tag).roles.remove(Cadet);
await guild.members.cache.get(tag).roles.remove(Pilot);
await guild.members.cache.get(tag).roles.remove(main_guild);
await guild.members.cache.get(tag).roles.remove(cadet_guild);
await guild.members.cache.get(tag).roles.add(guest);
}
}
i++;
if (i < users.length) {
mainLoop(
guild,
redisClient,
users,
main_list,
Pilot,
Astronaut,
Cadet,
main_guild,
cadet_guild,
guest
);
}
});
}, 5000);
}
The code will fetch api data, map the "verified" users and api data into an array. Then, when it starts looping through the users array, it will only log 3 times and not assign any roles. Any help would be appreciated.
I can provide extra explanation/code if needed.
One possible issue I see here is that you are both incrementing the index i and calling .shift() on the users array. This may be the cause of the problem you are experiencing, as this will entirely skip some of the users in the array. Array.shift() doesn't just return the first element of the array; it removes it from the array.
Consider, for example, that your users array looks like this:
var users = ["Ted", "Chris", "Ava", "Madison", "Jay"];
And your index starts at 0 like so:
var i = 0;
This is what is happening in your code:
Assign roles for users[i]; the index is currently 0, so get users[0] (Ted).
Get Ted's tag via users.shift(). users is now: ["Chris", "Ava", "Madison", "Jay"]
Increment the index with i++. i is now: 1.
Assign roles for users[i]; the index is currently 1, so get users[1] (now Ava, skips Chris entirely).
Get Ava's tag via users.shift() (actually gets Chris' tag). users is now: ["Ava", "Madison", "Jay"]
Increment the index with i++. i is now: 2.
Assign roles for users[i]; the index is currently 2, so get users[2] (now Jay, skips Madison entirely).
And so on, for the rest of the array; about half of the users in the users array will be skipped.
I don't know how many users are supposed to be in your users array, but this could be the reason why so few logs are occurring. Note, however, that this is just one cause of the problem you are experiencing; it is possible that there are more reasons why you are having that issue, such as rate limits.
My recommendation on how to fix this is to not use users.shift() to get the user's tag. Simply use users[i], which will return the proper tag value without messing with the length of the array. Another way to fix this would be to remove the index incrementation, and always use 0 as your index. Use one or the other, but not both.

Multiple Firebase listeners in useEffect and pushing new event into state

I want to retrieve a list of products in relation to the user's position, for this I use Geofirestore and update my Flatlist
When I have my first 10 closest collections, I loop to have each of the sub-collections.
I manage to update my state well, but every time my collection is modified somewhere else, instead of updating my list, it duplicates me the object that has been modified and adds it (updated) at the end of my list and keep the old object in that list too.
For example:
const listListeningEvents = {
A: {Albert, Ducon}
B: {Mickael}
}
Another user modified 'A' and delete 'Ducon', I will get:
const listListeningEvents = {
A: {Albert, Ducon},
B: {Mickael},
A: {Albert}
}
And not:
const listListeningEvents = {
A: {Albert},
B: {Mickael},
}
That's my useEffect:
useEffect(() => {
let geoSubscriber;
let productsSubscriber;
// 1. getting user's location
getUserLocation()
// 2. then calling geoSubscriber to get the 10 nearest collections
.then((location) => geoSubscriber(location.coords))
.catch((e) => {
throw new Error(e.message);
});
//Here
geoSubscriber = async (coords) => {
let nearbyGeocollections = await geocollection
.limit(10)
.near({
center: new firestore.GeoPoint(coords.latitude, coords.longitude),
radius: 50,
})
.get();
// Empty array for loop
let nearbyUsers = [];
// 3. Getting Subcollections by looping onto the 10 collections queried by Geofirestore
productsSubscriber = await nearbyGeocollections.forEach((geo) => {
if (geo.id !== user.uid) {
firestore()
.collection("PRODUCTS")
.doc(geo.id)
.collection("USER_PRODUCTS")
.orderBy("createdDate", "desc")
.onSnapshot((product) => {
// 4. Pushing each result (and I guess the issue is here!)
nearbyUsers.push({
id: product.docs[0].id.toString(),
products: product.docs,
});
});
}
});
setLoading(false);
// 4. Setting my state which will be used within my Flatlist
setListOfProducts(nearbyUsers);
};
return () => {
if (geoSubscriber && productsSubscriber) {
geoSubscriber.remove();
productsSubscriber.remove();
}
};
}, []);
I've been struggling since ages to make this works properly and I'm going crazy.
So I'm dreaming about 2 things :
Be able to update my state without duplicating modified objects.
(Bonus) Find a way to get the 10 next nearest points when I scroll down onto my Flatlist.
In my opinion the problem is with type of nearbyUsers. It is initialized as Array =[] and when you push other object to it just add new item to at the end (array reference).
In this situation Array is not very convenient as to achieve the goal there is a need to check every existing item in the Array and find if you find one with proper id update it.
I think in this situation most convenient will be Map (Map reference). The Map indexes by the key so it is possible to just get particular value without searching it.
I will try to adjust it to presented code (not all lines, just changes):
Change type of object used to map where key is id and value is products:
let nearbyUsersMap = new Map();
Use set method instead of push to update products with particular key:
nearbyUsersMap.set(product.docs[0].id.toString(), product.docs);
Finally covert Map to Array to achieve the same object to use in further code (taken from here):
let nearbyUsers = Array.from(nearbyUsersMap, ([id, products]) => ({ id, products }));
setListOfProducts(nearbyUsers);
This should work, but I do not have any playground to test it. If you get any errors just try to resolve them. I am not very familiar with the geofirestore so I cannot help you more. For sure there are tones of other ways to achieve the goal, however this should work in the presented code and there are just few changes.

Splicing array to add to an existing array

I am using rss2json to consume an rss feed. There is not a page param to enable pagination. There is a count parameter that I can pass to the request. I am able to load the feed and get results back. I have created a service using ionic to make a request to get the feed:
getRssFeed(rssUrl: string, count: number) {
return new Promise<any>(resolve => {
this.http.get(`${ environment.podcast.baseUrl }?rss_url=${ rssUrl }&api_key=${ environment.podcast.apiKey }&count=${ count }`)
.subscribe(data => {
resolve(data);
}, error => {
console.error('Something really bad happened trying to get rss feed.');
console.error(error);
});
});
}
This works great. I can get the data back - all is well. I am using an infinite scroll component to handle pagination. Again, all is well. I am starting with 10 podcast episodes. I am logging out when I want to load more episodes:
When I scroll, the service makes the correct call, but because the rss2json service does not have a page param, It will return the entire array when I update the count.
So I need to do something like this:
episodes: Array<any>;
count = 10;
...
this.episodes.splice(this.episodes.length, this.count, data.items);
I need to find out how many episodes I already have. The first time I get to the bottom of my list, I'll have 10 (I want to increment +10 each load). So I need to:
Find out how many episodes I currently have (10, 20, 30 etc.)
Make the request to get more episodes
Service returns 20 episodes -- but it will always start at zero.
Slice the first 10, 20, ?? episodes that are returned, add the remaining 10 to the end of the list.
I am not sure how to achieve this and could use some direction.
Here is how I am requesting more episodes:
this.myPodcastService.getRssFeed(this.rssUrl, this.count)
.then(data => {
if (data) {
// console.log('data', data.items);
// data is an object
// data.items is an array of episodes
// this.episodes.splice(this.episodes.length, this.count, data.items);
} else {
...
}
...
});
For example, the first time I get to the end of my episodes, I'll have 10 on the page. I want to go out, get 10 more episodes. So I need to increment my count variable to 20 and pass that in as the count param.
The service will return 20 items. The first 10 I want to delete (They are already on screen). I only need the last 10 episodes...
Now I'll have 20 episodes. The next time I scroll, I'll need to increment my count to 30. The service will return an array of 30 items. I will need to delete (splice) the first 20; leaving only the last 10 -- then add that to the episodes array.
The logging should show something like:
this.episodes[10]
this.episodes[20]
this.episodes[30]
I hope that makes sense. I know what I'm trying to achieve, I'm struggling how to actually do it. Thank you for any suggestions!
EDIT/SOLUTION
Thank you so much for the suggestion! In case someone else comes across this, here is what I came up with that is doing exactly what I need.
// load more episodes using infinite scroll.
loadMoreEpisodes(event) {
console.log('--> loading more episodes');
this.count = (this.count + this.count); // 10, 20, 30...
this.myPodcastService.getRssFeed(this.rssUrl, this.count)
.then(data => {
if (data) {
// append the new episodes to the existing array
this.episodes.push(...data.items.splice(-this.episodes.length, this.count));
event.target.complete();
console.log('this.episodes', this.episodes);
} else {
this.alertCtrl.create({
header: 'Error',
subHeader: 'Something bad happened',
message: 'Something internet related happened & we couldn\'t load the playlist.',
buttons: [{ text: 'Ok', role: 'cancel' }]
}).then(alert => {
alert.present();
});
}
});
}
Given the API does not provide a means to get specific data, where the client has to request duplicate data, you can .splice() from the end of the array
this.episodes.push(...data.splice(-10, 10))

RxJS mix combineLatest and zip

I have two sources of streams that I want to listen to them. The requirements are:
If one emits give me also the last value from the second.
If two of them emits at the same time, don't call the subscribe two times.
The first case is combineLatest, but the second is zip. I need a way to mix combineLatest and zip into one operator.
const { Observable, BehaviorSubject} = Rx;
const movies = {
ids: [],
entities: {}
}
const actors = {
ids: [],
entities: {}
}
const storeOne = new BehaviorSubject(movies);
const storeTwo = new BehaviorSubject(actors);
const movies$ = storeOne.map(state => state.entities).distinctUntilChanged();
const actors$ = storeTwo.map(state => state.entities).distinctUntilChanged();
const both$ = Observable.zip(
movies$,
actors$,
(movies, actors) => {
return {movies, actors};
}
)
both$.subscribe(console.log);
storeOne.next({
...storeOne.getValue(),
entities: {
1: {id: 1}
},
ids: [1]
});
storeTwo.next({
...storeTwo.getValue(),
entities: {
1: {id: 1}
},
ids: [1]
});
The above code works fine when both emits one after the other, but I need to support also a case where one of them emits. (combineLatest)
How can I do that?
Yes, as advised by #cartant you can use Observable.combineLatest(movies$, actors$, (movies, actors) => ({ movies, actors })).auditTime(0)
To elaborate the above,
auditTime(n) will wait for n milliseconds and emit the latest value.
auditTime(0) is similar to setTimeout(0), it actually waits for nothing (executes immediately), but waits for the current event/execution loop to complete.
Here values B & 2 are emitted together, so when you use combineLatest you would get either B1,B2 or A2, B2 (which is based on the internal clock). Regardless B2 is the latest value in that execution loop. Since we are waiting for 0 milliseconds i.e. for the current execution loop to get the latest value via auditTime(0), the observable would emit only B2.

Why do I lose the current object state when 'this.props.actions.updateComment(event.target.value)' is called?

I trying trying to achieve the following: There is a textfield and once a user enters in a text, an object is created with the text assigned to a state property called 'commentText' which is located inside the 'comments' array which is inside the object (todo[0]) of 'todos' array. 'commentInput' is just a temporary storage for the input entered in the textfield, to be assigned to the 'commentText' of 'todo[0]' object's 'comments' array.
I retrieve the current state object via following:
const mapStateToProps=(state, ownProps)=>({
todo:state.todos.filter(todo=>todo.id==ownProps.params.id)
});
and dispatch and actions via:
function mapDispatchToProps(dispatch){
return{
actions: bindActionCreators(actions, dispatch)
}
So the retrieved object 'todo' has an array property named comments. I have a text field that has:
onChange={this.handleCommentChange.bind(this)}
which does:
handleCommentChange(event){
this.props.actions.updateComment(event.target.value)
}
Before handleCommentChange is called, the object 'todo[0]' is first fetched correctly:
But as soon as a text is inputted into the text field, onChange={this.handleCommentChange.bind(this)} is called and all of a sudden, 'todo[0]' state is all lost (as shown in the 'next state' log):
What may be the issue? Tried solving it for hours and hours but still stuck. Any guidance or insight would be greatly appreciated. Thank you in advance.
EDIT **:
{
this.props.newCommentsArray.map((comment) => {
return <Comment key={comment.id} comment={comment} actions={this.props.actions}/>
})
}
EDIT 2 **
case 'ADD_COMMENT':
return todos.map(function(todo){
//Find the current object to apply the action to
if(todo.id === action.id){
//Create a new array, with newly assigned object
return var newComments = todo.comments.concat({
id: action.id,
commentTxt: action.commentTxt
})
}
//Otherwise return the original array
return todo.comments
})
I would suspect that your reducer is not correctly updating the todo entry. It's probably replacing the contents of the entry entirely, rather than merging the incoming value in in some fashion.
edit:
Yup, after seeing your full code, your reducer is very much at fault. Here's the current code:
case 'ADD_COMMENT':
return todos.map(function(todo){
if(todo.id === action.id){
return todo.comments = [{
id: action.id,
commentTxt: action.commentTxt
}, ...todo.comments]
}
})
map() should be returning one item for every item in the array. Instead, you're only returning something if the ID matches, and even then, you're actually assigning to todo.comments (causing direct mutation) and returning the result of that statement (which might be undefined?).
You need something like this instead (which could be written shorter, but I've deliberately written it out long-form to clarify what's happening):
case 'ADD_COMMENT':
return todos.map(function(todo) {
if(todo.id !== action.id) {
// if the ID doesn't match, just return the existing objecct
return todo;
}
// Otherwise, we need to return an updated value:
// Create a new comments array with the new comment at the end. concat() will
// You could also do something like [newComment].concat(todo.comments) to produce
// a new array with the new comment first depending on how you want it ordered.
var newComments = todo.comments.concat({
id : action.id,
commentTxt : action.commentTxt
});
// Create a new todo object that is a copy of the original,
// but with a new value in the "comments" field
var newTodo = Object.assign({}, todo, {comments : newComments});
// Now return that instead
return newTodo;
});

Categories