Is it possible to simply display a number of online users (currently logged in) with meteor-presence?
I see that documents in Meteor.presences collection do not delete themselves by default, so simple Meteor.presences.find().count() is not the value I'm looking for... Should I delete them manually or should I check if each document has userID field somehow?
EDIT:
To illustrate a bit more what is happening to me, I tried to log in with 2 different users in 2 different browsers. After calling Meteor.presences I can see two documents in my collection, e.g.
docs: {
djF3noxe3AhxDRfZw: {
_id: "djF3noxe3AhxDRfZw",
state: "online",
userId: "SDeLPJzoabFt4Knei"
}, {
t4r2Q7KGKji4FPS9s: {
_id: "t4r2Q7KGKji4FPS9s",
state: "online",
userId: "5zvYoC37aXSADGNEg"
}
}
After few minutes of inactivity, both docs look the same. After logging out (calling Meteor.logout()) with one user I still have 2 documents (even after few minutes), the only difference is that one of them has no more userID:
docs: {
djF3noxe3AhxDRfZw: {
_id: "djF3noxe3AhxDRfZw",
state: "online"
}, {
t4r2Q7KGKji4FPS9s: {
_id: "t4r2Q7KGKji4FPS9s",
state: "online",
userId: "5zvYoC37aXSADGNEg"
}
}
Only after closing that browser window the associated document deletes itself.
So, if I call Meteor.presences.find().count() when both windows are opened, I always get 2 in return, no matter of user's online/offline status.
All the code I am using at the moment is the one from the example:
Meteor.publish('userPresence', function() {
var filter = {};
return Meteor.presences.find(filter, {fields: {state: true, userId: true}});
});
I was seeing the same thing and it confused me as well. But when you think about it, a visitor looking at the page is a presence as well, yet doesn't have a userId. So it makes sense that any visitor would show up in the collection.
If you only want to see logged in users you could change filter to
var filter = {userId: {$exists: true}};
You can do this easily with the package I've created, https://github.com/mizzao/meteor-user-status. It doesn't double-count users, and you can just display or publish the value
Meteor.users.find({ "status.online": true }).count()
It also updates immediately when users disconnect or log off.
The documents delete themselves after a timeout period (10 seconds). https://github.com/tmeasday/meteor-presence/blob/master/presence_server.js#L1
Related
I have some data in a Firebase Realtime Database that looks as follows:
users: {
$user1: {
email: "person#gmail.com"
name: "Bob"
},
$user2: {
email: "otherperson#gmail.com"
name: "Sally"
},
...
}
investments: {
$investment1: {
user: $user1,
ticker: "GOOG",
name: "Google Inc",
...
},
$investment2: {
user: $user1,
ticker: "AAPL",
name: "Apple Inc",
...
},
$investment3: {
user: $user2,
ticker: "TSLA",
name: "Tesla Inc",
...
},
...
}
Where $user1 represents the id of user1 (generated by firebase).
In my web application, I would like to:
Fetch all investments that belong to a particular user
Then, listen for any newly added investments that belong to that user
The reason I want to fetch all of the existing investments at the same time is so my application doesn't re-render for each time the child_added listener callback is called.
For a small number of investments, the following code should work fine.
const userId = "..."; // userId of logged in user
let initialDataLoaded = false;
// For newly added investments
database
.ref("investments")
.orderByChild("user")
.equalTo(userId)
.on("child_added", (snapshot) => {
if (initialDataLoaded) {
const newlyAddedInvestment = snapshot.val();
/* add new row to table */
}
});
// For existing investments
database
.ref("investments")
.orderByChild(userId)
.equalTo(userId)
.once("value", (snapshot) => {
const existingInvestments = snapshot.val();
/* display existing investments in a table */
initialDataLoaded = true;
});
}, []);
However, I am worried about what would happen if a user had a large number of existing investments. Particularly, I am worried about two things:
The child_added callback will be called for every existing investment. I know firebase guarantees that child_added events are triggered before value events, but can it guarantee that the code inside the child_added callback is executed before the code inside the value event callback is executed? If not, then isn't it possible that initialDataLoaded is set to true before all of the if (initialDataLoaded) {} lines are executed?
The child_added callback will be called for every existing investment. This could chew up a lot of bandwith.
I read in another SO post that I could add a createdAt timestamp to all of the investments, and then use a ref.orderByChild('createdAt').startAt(Firebase.ServerValue.TIMESTAMP); query to only fetch newly added investments. I like this solution as it scales to large datasets, however, since I am already ordering by user, I don't believe I can use this method (please correct me if I am wrong). If there is a way to retrieve only the users investments that also start after Firebase.ServerValue.TIMESTAMP this would also answer my question. If this is a limitation of Realtime Database, I might try out Cloud Firestore since I know that it supports more complex queries, which would solve this problem.
I might be overthinking this, but I want to get a reliable solution that doesn't cause unecessary re-renders and does not cause unecessary event triggers.
Reading you post I can help you I think I can help you with a little information.
Regarding the first question that you ask, about if you can guarantee that the code in the child_added's call back it would be executed before the code in he values' call back. The response will be it depends, depends in the code that you put in each call back one maybe it would take longer than the other but it would depend.
Regarding the second question you can check this page that will help you to pinpoint exactly what reference (including children) was hogging the bandwidth and check that part of the code it's taking that much bandwidth.
And for the last question you might be able to use Firebase.ServerValue.TIMESTAMP with a filter for users, but after looking for a while it could be a little complicated to implement, maybe the easier solution would be to use Cloud Firestore since handle more complex queries as you say.
Hi I am rate limiting some methods in meteor.js with DDPRateLimiter and what I found out is that it limits method call for everyone not just that connection! For example for this rule:
var updateUsernameRule = {
type: 'method',
name: 'updateUsername'
};
DDPRateLimiter.addRule(updateUsernameRule, 1, 30000);
Update in one browser and then when updating in another it causes rate limit error. Documentation is kinda unclear about that and default behavior is not intuitive at all. How do I rate limit per user?
I agree, the docs need a bit of work. To make this work in your case (restrict by logged in user only), you will want something like the following:
const updateUsernameRule = {
type: 'method',
name: 'updateUsername',
userId(userId) {
if (userId) {
return true;
}
}
};
DDPRateLimiter.addRule(updateUsernameRule, 1, 30000);
This will cause the updateUsernameRule rule to only be matched for user's with a set userId (logged in users). All other not logged in users will skip this rule completely, and be able to make as many requests as they want.
I have a game built on Meteor framework. One game document is something like this:
{
...
participants : [
{
"name":"a",
"character":"fighter",
"weapon" : "sword"
},
{
"name":"b",
"character":"wizard",
"weapon" : "book"
},
...
],
...
}
I want Fighter character not to see the character of the "b" user. (and b character not to see the a's) There are about 10 fields like character and weapon and their value can change during the game so as the restrictions.
Right now I am using Session variables not to display that information. However, it is not a very safe idea. How can I subscribe/publish documents according to the values based on characters?
There are 2 possible solutions that come to mind:
1. Publishing all combinations for different field values and subscribing according to the current state of the user. However, I am using Iron Router's waitOn feature to load subscriptions before rendering the page. So I am not very confident that I can change subscriptions during the game. Also because it is a time-sensitive game, I guess changing subscriptions would take time during the game and corrupt the game pleasure.
My problem right now is the user typing
Collection.find({})
to the console and see fields of other users. If I change my collection name into something difficult to find, can somebody discover the collection name? I could not find a command to find collections on the client side.
The way this is usually solved in Meteor is by using two publications. If your game state is represented by a single document you may have problem implementing this easily, so for the sake of an example I will temporarily assume that you have a Participants collection in which you're storing the corresponding data.
So anyway, you should have one subscription with data available to all the players, e.g.
Meteor.publish('players', function (gameId) {
return Participants.find({ gameId: gameId }, { fields: {
// exclude the "character" field from the result
character: 0
}});
});
and another subscription for private player data:
Meteor.publish('myPrivateData', function (gameId) {
// NOTE: not excluding anything, because we are only
// publishing a single document here, whose owner
// is the current user ...
return Participants.find({
userId: this.userId,
gameId: gameId,
});
});
Now, on the client side, the only thing you need to do is subscribe to both datasets, so:
Meteor.subscribe('players', myGameId);
Meteor.subscribe('myPrivateData', myGameId);
Meteor will be clever enough to merge the incoming data into a single Participants collection, in which other players' documents will not contain the character field.
EDIT
If your fields visibility is going to change dynamically I suggest the following approach:
put all the restricted properties in a separated collection that tracks exactly who can view which field
on client side use observe to integrate that collection into your local player representation for easier access to the data
Data model
For example, the collection may look like this:
PlayerProperties = new Mongo.Collection('playerProperties');
/* schema:
userId : String
gameId : String
key : String
value : *
whoCanSee : [String]
*/
Publishing data
First you will need to expose own properties to each player
Meteor.publish('myProperties', function (gameId) {
return PlayerProperties.find({
userId: this.userId,
gameId: gameId
});
});
then the other players properties:
Meteor.publish('otherPlayersProperties', function (gameId) {
if (!this.userId) return [];
return PlayerProperties.find({
gameId: gameId,
whoCanSee: this.userId,
});
});
Now the only thing you need to do during the game is to make sure you add corresponding userId to the whoCanSee array as soon as the user gets ability to see that property.
Improvements
In order to keep your data in order I suggest having a client-side-only collection, e.g. IntegratedPlayerData, which you can use to arrange the player properties into some manageable structure:
var IntegratedPlayerData = new Mongo.Collection(null);
var cache = {};
PlayerProperties.find().observe({
added: function (doc) {
IntegratedPlayerData.upsert({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
changed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$set: _.object([ doc.key ], [ doc.value ])
});
},
removed: function (doc) {
IntegratedPlayerData.update({ _id : doc.userId }, {
$unset: _.object([ doc.key ], [ true ])
});
}
});
This data "integration" is only a draft and can be refined in many different ways. It could potentially be done on server-side with a custom publish method.
I have a set of users defined like this:
Accounts.createUser({
username:'Simon',
email:'simon#email.com',
profile:{
firstname:'Simon',
lastname:'Surname',
location:'Home Address',
privacy: {
location:0,
emails:0 } //Location and emails are private and should not be disclosed
}
});
My question is how can I publish this user's record for other users to view, taking into account the profile privacy settings. In this example, I have set the privacy for location and emails to zero with the intention that this information is not published for this user.
I would like to publish it using the standard method:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find();
});
But I cannot see a way to specify the selector or fields in such a way that only public information will be published.
I have tried adding additional publications of the form:
Meteor.publish("allUsers", function () {
return Meteor.users.find( {}, {fields:{username:1}} );
});
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, emails:1}} );
});
but the selector does not seem to be returning the emails as I expected. I am looking for optimal way to do this from a performance point of view.
Mongodb is not a relational database so whenever I want to join or query based on metadata I remember I have to do things differently. In your case I would make a separate Collection for user privacy if I wanted to query on user privacy. In addition, if I cared about performance I probably would never want "all of x", I would just want enough to show the user, thus paginate. With these two ideas in mind you can easily get what you want: query based on privacy settings and performance.
Privacy = new Mongo.Collection("privacy");
Whenever we want to add privacy to an account:
Privacy.insert({
emails: 1,
userId: account._id,
});
Then later, one page at a time, showing ten results each page, tracking with currentPage:
Meteor.publish("usersWithPublicEmails function (currentPage) {
var results = []
var privacyResults = Privacy.find({"emails":1}, {skip: currentPage,
limit: 10});
var result;
while (privacyResults.hasNext() ) {
result = privacyResult.next();
results.append(Meteor.users.find({_id: result.userId});
}
return result;
});
I didn't test this code, it may have errors, but it should give you the general idea. The drawback here is that you have to keep privacy and users in sync, but these are the kinds of problems you run into when you're not using a relational database.
Mongodb has a way to do this kind of reference lookup with less code, but it still happens on demand and I prefer the flexibility of doing it myself. If you're interested take a look at Database references
That's because you have a typo in your publish function's fields object, instead of email you've typed emails
So the correct function would be:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{username:1, email:1}} );
});
Furthermore, you're already publishing all usernames in your allUsers publication, therefore, in order to add the missing data for relevant public users, you'll just need this:
Meteor.publish("usersWithPublicEmails", function () {
return Meteor.users.find( {"profile.privacy.emails":1}, {fields:{email:1}} );
});
and Meteor will automatically merge those records for you.
A simple solution in the end. I had missed the additional subscription in my router:
Router.route('/users', {
name: 'userList',
waitOn: function(){
return Meteor.subscribe('allUsers') &&
Meteor.subscribe('usersWithPublicEmails');
},
data: function(){
return Meteor.users.find();
}
});
A basic mistake:-(
I have a meteor collection like this:
Cases = new Meteor.Collection('cases');
As well i have registered users (max 10). I now want to be able to "give" a single case to a registered user and be sure, that no other user is getting that specific case.
The User is working with the case (updating fields, deleting fields) and then sends it in some kind of archive after submitting the user should get a new case that is in the collection.
My thought was to have field called "locked" which initially is set to false and in the moment it is displayed at the user "locked" gets true and is not returned anymore:
return Cases.find({locked: false, done: false}, {limit: 1});
Any ideas how to do that in meteor?
Thanks
You just need to attach an owner field (or similar) to the case. That would allow you to do things like:
Only publish the case to the user who is also the owner using something like:
Meteor.publish('cases/unassigned', function() {
return Cases.find({owner: {$exists: false}});
});
Meteor.publish('cases/mine', function() {
return Cases.find({owner: this.userId});
});
Not allow a user to update or delete a case if it's not assigned to them:
Cases.allow({
update: function(userId, fieldNames, doc, modifier) {
return userId === doc.owner;
},
delete: function(userId, doc) {
return userId === doc.owner;
}
});
Obviously, these would need amending for stuff like super-users and you probably need some methods defined to allow users to take cases, but that's the general idea.
There are concurrency issues to deal with, to reliably allocate a case to only one person.
We need to solve two things:
1. Reliably assign the case to a user
2. Fetch the cases assigned to a user
Number 2. is easy, but depends on 1.
To solve 1., this should work:
var updated = Cases.update(
{_id: <case-to-assign>, version: "ab92c91"},
{assignedTo: Meteor.userId(), version: Meteor.Collection.ObjectID()._str});
if (updated) {
// Successfully assigned
} else {
// Failed to assign, probably because the record was changed first
}
Using this you can query for all of a users cases:
var cases = Cases.find({assignedTo: Meteor.userId()});
If 10 people try get a case at the same time, it should have a pre-set version field, and the MongoDB will only let the .update work once. As soon as the version field changes (due to an .update succeeding) the remaining updates will fail as the version field could no longer match.
Now that the allocation has taken place reliably, fetching is very simple.
As suggested by #Kyll, the filtering of cases should be done inside a Meteor publication.
It would also make sense to perform the case-assignment inside a Meteor method.
UPDATE:
#richsilv's solution is simpler than this one, and works fine.
This solution is useful if you need to know who won immediately, without making further requests to the server.