Permanent function

I have some users in my web application.

Each user has a score and a rank.

I would like to use a function that updates the rank according to the score each time the score is updated.

Which side should I run this code on, client or server side? So that it is constantly called when the score changes, permanently.

(ranking() => {
for (let user in Meteor.users.find().fetch()) {
	if(users[user].score > 5) {
		Meteor.users.update({_id:this.users[user]._id}, {$inc: {rank:1} })
	}
}
})

I’d advise against this, for a couple of reasons.

  1. you’re updating every user whenever one users score changes (assuming you’ve got reactivity)
  2. Unless you run this in an autorun block, it will never run after the first.

A much better approach would be to update a users ranking whenever you update their score, however - if you don’t want to do that for whatever reason, look into https://github.com/matb33/meteor-collection-hooks which allows you to run some function whenever an update occurs.

If you need to update the rank when some external application modifies the score directly in the database, you’d have to write something like this (untested):

Meteor.users.find({}, { fields: { score: 1 } }).observeChanges({
changed(_id, { score } ) {
  Meteor.users.update(...);
})
1 Like

Instead of storing the ranks in the db, I would compute it instead. Something like:

const myScore = Meteor.user().score;
const rank = 1 + Meteor.users.find({ score: { $gt: myScore }}).count();

Since your rank is equal to the number of people with a higher score + 1
With an index on score, this should be almost as fast as having it in the document, and you don’t have the write overhead of updating each individual user in separate db calls

To make this work you might need to publish a subset of the users collection (ie. just the scores):

Meteor.publish('scores' function () {
  return Meteor.users.find({}, { fields: { score: 1 } });
});

And query that on the client

OR a fake collection that just stores your rank:

// Server
Meteor.publish("myRank", function() {
  const user = Meteor.user();
  if (!user) return;
  const myScore = user.score;
  let rank = 1;
  let initializing = true;

  // `observeChanges` only returns after the initial `added` callbacks have run.
  // Until then, we don't want to send a lot of `changed` messages—hence
  // tracking the `initializing` state.
  const handle = Meteor.users.find({ score: { $gt: myScore } }).observeChanges({
    added: () => {
      // for each person with a higher score, increment the rank
      rank = rank + 1;
      if (!initializing) {
        this.changed("ranks", this.userId, { rank });
      }
    },
    removed: () => {
      // When the list of people with a higher rank shrinks, decriment the rank
      rank = rank - 1;
      if (!initializing) {
        this.changed("ranks", this.userId, { rank });
      }
    },
  });

  initializing = false;
  // Add the initial computed rank
  this.added("ranks", this.userId, { rank });
  this.ready();

  // Stop observing the cursor when the client unsubscribes. Stopping a
  // subscription automatically takes care of sending the client any `removed`
  // messages.
  this.onStop(() => handle.stop());
});

// Client
const ranks = Meteor.collection('ranks');
Meteor.subscribe('myRank', function () {
  const myRank = rank.findOne({ _id: Meteor.userId })
});

Which will be totally reactive and only send the client data about your rank.

The second one will be more efficient for many users (don’t need to send everyone’s scores to determine your rank)

EDIT: Just realised that the publication won’t re-run when the user’s score changes… In which case you’d need to observe that query as well and invalidate the main one on a change and re-run the cursor. That might be a bit intensive

Not just when there score changes but when any users score changes, which I just realised applies to my suggestions too! You’d have to update all users ranks, or at least a large subset, whenever one users score changes.

1 Like

Yeah it seems this is expensive to do reactively, so it might be better to do it with a method:

Meteor.methods({
  myRank() {
    const myScore = Meteor.user().score;
    const rank = 1 + Meteor.users.find({ score: { $gt: myScore }}).count();
    return rank;
  }
});

I need to store the rank in the user database because the rank will allow rights to the user.

If it’s access related I would almost always want fresh information, in which case I would always compute the values. Since access rights should be determined on the server, that makes the querying easy as well

The other thing is that I want other users to see the rank of other users, and that’s not allowed through a constant.

My web app, look the ladder : http://52.47.162.25/