How does the messages-count example in Meteor docs work?

Mike Bannister picture Mike Bannister · May 12, 2012 · Viewed 8.3k times · Source

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();
  });
});

Answer

debergalis picture debergalis · May 12, 2012

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.");