41

Having trouble full understanding this example from the docs... I tried running it a bunch of different ways so I could observe how it works, etc.

How do you subscribe to this? Can we include the client side code needed to make this work?

Is there a collection called messages-count? Is a Room a collection of messages? Can we include the collection definitions in the example?

Any tips on this would be great!

NOTE: this is the code as it appeared when this question was initially posted (May 2012). It's simpler now.

// server: publish the current size of a collection
Meteor.publish("messages-count", function (roomId) {
  var self = this;
  var uuid = Meteor.uuid();
  var count = 0;

  handle = Room.find({room_id: roomId}).observe({
    added: function (doc, idx) {
      count++;
      self.set("messages-count", uuid, "count", count);
      self.flush();
    },
    removed: function (doc, idx) {
      count--;
      self.set("messages-count", uuid, "count", count);
      self.flush();
    }
    // don't care about moved or changed
  });

  // remove data and turn off observe when client unsubs
  self.onStop(function () {
    handle.stop();
    self.unset("messages-count", uuid, "count");
    self.flush();
  });
});
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
Mike Bannister
  • 1,424
  • 1
  • 13
  • 16

3 Answers3

52

Thanks for prompting me to write a clearer explanation. Here's a fuller example with my comments. There were a few bugs and inconsistencies that I've cleaned up. Next docs release will use this.

Meteor.publish is quite flexible. It's not limited to publishing existing MongoDB collections to the client: we can publish anything we want. Specifically, Meteor.publish defines a set of documents that a client can subscribe to. Each document belongs to some collection name (a string), has a unique _id field, and then has some set of JSON attributes. As the documents in the set change, the server will send the changes down to each subscribed client, keeping the client up to date.

We're going to define a document set here, called "counts-by-room", that contains a single document in a collection named "counts". The document will have two fields: a roomId with the ID of a room, and count: the total number of messages in that room. There is no real MongoDB collection named counts. This is just the name of the collection that our Meteor server will be sending down to the client, and storing in a client-side collection named counts.

To do this, our publish function takes a roomId parameter that will come from the client, and observes a query of all Messages (defined elsewhere) in that room. We can use the more efficient observeChanges form of observing a query here since we won't need the full document, just the knowledge that a new one was added or removed. Anytime a new message is added with the roomId we're interested in, our callback increments the internal count, and then publishes a new document to the client with that updated total. And when a message is removed, it decrements the count and sends the client the update.

When we first call observeChanges, some number of added callbacks will run right away, for each message that already exists. Then future changes will fire whenever messages are added or removed.

Our publish function also registers an onStop handler to clean up when the client unsubscribes (either manually, or on disconnect). This handler removes the attributes from the client and tears down the running observeChanges.

A publish function runs each time a new client subscribes to "counts-by-room", so each client will have an observeChanges running on its behalf.

// server: publish the current size of a collection
Meteor.publish("counts-by-room", function (roomId) {
  var self = this;
  var count = 0;
  var initializing = true;

  var handle = Messages.find({room_id: roomId}).observeChanges({
    added: function (doc, idx) {
      count++;
      if (!initializing)
        self.changed("counts", roomId, {count: count});  // "counts" is the published collection name
    },
    removed: function (doc, idx) {
      count--;
      self.changed("counts", roomId, {count: count});  // same published collection, "counts"
    }
    // don't care about moved or changed
  });

  initializing = false;

  // publish the initial count. `observeChanges` guaranteed not to return
  // until the initial set of `added` callbacks have run, so the `count`
  // variable is up to date.
  self.added("counts", roomId, {count: count});

  // and signal that the initial document set is now available on the client
  self.ready();

  // turn off observe when client unsubscribes
  self.onStop(function () {
    handle.stop();
  });
});

Now, on the client, we can treat this just like a typical Meteor subscription. First, we need a Mongo.Collection that will hold our calculated counts document. Since the server is publishing into a collection named "counts", we pass "counts" as the argument to the Mongo.Collection constructor.

// client: declare collection to hold count object
Counts = new Mongo.Collection("counts");

Then we can subscribe. (You can actually subscribe before declaring the collection: Meteor will queue the incoming updates until there's a place to put them.) The name of the subscription is "counts-by-room", and it takes one argument: the current room's ID. I've wrapped this inside Deps.autorun so that as Session.get('roomId') changes, the client will automatically unsubscribe from the old room's count and resubscribe to the new room's count.

// client: autosubscribe to the count for the current room
Tracker.autorun(function () {
  Meteor.subscribe("counts-by-room", Session.get("roomId"));
});

Finally, we've got the document in Counts and we can use it just like any other Mongo collection on the client. Any template that references this data will automatically redraw whenever the server sends a new count.

// client: use the new collection
console.log("Current room has " + Counts.findOne().count + " messages.");
Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
debergalis
  • 11,870
  • 2
  • 49
  • 43
  • 2
    Clear as bell! Thanks so much for taking the time to clarify this for me! – Mike Bannister May 12 '12 at 23:46
  • 2
    Note that `self.flush();` within `added` will push that subscription down to the client as the collection is populated. Imagine you have 1,000,000 "Messages" in that "room_id". You'll be sent 1,000,000 subscriptions starting at count 1 and ending at count 1,000,000. This will lock up your browser for quite some time! Not to mention the amount of data flying over the wire... – matb33 Aug 15 '12 at 01:41
  • @matb33, is there a better solution for the flush problem? – Leonhardt Wille Sep 12 '12 at 01:29
  • 1
    As a temporary fix, you can throttle the call to `self.flush();` within `added` using a `setTimeout` trick, such as: clearTimeout(t); t = setTimeout(function () { self.flush(); }, 10); – matb33 Sep 22 '12 at 03:16
  • 1
    Nevermind, just saw your code below! Looks like you've figured it out – matb33 Sep 22 '12 at 03:18
  • What would happen if you declared the named client side collection without the publication? – Merlin -they-them- May 10 '17 at 17:53
2

As Leonhardt Wille said, the downside of this solution is that meteor downloads the whole collection of items from Mongo server just to count them. His solution at gist.github.com/3925008 is better, but counter will not update when new items inserted.

Here is my reactive solution

Collections:

Players = new Meteor.Collection("players");
PlayersCounts = new Meteor.Collection("players_counts")

Server:

Meteor.publish("players_counts", function(){
    var uuid = Meteor.uuid()
    var self = this;

    var unthrottled_setCount = function(){
        cnt = Players.find({}).count()
        self.set("players_counts", uuid, {count: cnt})
        self.flush()
    }

    var setCount = _.throttle(unthrottled_setCount, 50)

    var handle = Meteor._InvalidationCrossbar.listen({collection: "players"}, function(notification, complete){
        setCount();
        complete();
    })

    setCount();
    self.complete()
    self.flush()

    self.onStop(function(){
        handle.stop();
        self.unset("players_counts", uuid, ["count"]);
        self.flush();
    });
});

Client:

Meteor.subscribe("players_counts")

Template.leaderboard.total = function(){
    var cnt = PlayersCounts.findOne({})
    if(cnt) {
        return cnt.count;
    } else {
        return null;
    }
}
Pechkin
  • 655
  • 5
  • 5
  • As of Meteor 0.6.6.3 (maybe earlier) this code fails: `Exception from sub CfuTiQGacmWo5xMsb TypeError: Cannot call method 'listen' of undefined` – russellfeeed Dec 10 '13 at 16:22
  • 1
    Just FYI, this is pre Meteor 0.6 code. See @debergalis' updated answer above. – Andrew Mao Jan 18 '14 at 19:12
0

Just found a solution to the problem where self.flush() is sending thousands of updates to the client - just use _.debounce when counting:

count = 0
throttled_subscription = _.debounce =>
  @set 'items-count', uuid, count: count
  @flush()
, 10
handle = Items.find(selector).observe
  added: =>
    count++
    throttled_subscription()
  removed: =>
    count--
    throttled_subscription()

This will only set the count and flush the subscription after 10ms of no change.

Thanks to @possibilities on #meteor for the hint.

Leonhardt Wille
  • 557
  • 5
  • 20
  • 1
    The downside of this solution is that meteor downloads the whole collection into the server, so if you're using a relatively slow remote connection to your mongoDB there will be a notable delay after your app starts (at least if you have 10k docs in your DB like me). – Leonhardt Wille Oct 20 '12 at 16:02