Understanding Meteor Publish / Subscribe
Collections, publications and subscriptions are a tricky area of Meteor, that the documentation could discuss in more detail, so as to avoid frequent confusion, which sometimes get amplified by confusing terminology.
Here's Sacha Greif (co-author of DiscoverMeteor) explaining publications and subscriptions in one slide:
To properly understand why you need to call find()
more than once, you need to understand how collections, publications and subscriptions work in Meteor:
You define collections in MongoDB. No Meteor involved yet. These collections contain database records (also called "documents" by both Mongo and Meteor, but a "document" is more general than a database record; for instance, an update specification or a query selector are documents too - JavaScript objects containing
field: value
pairs).Then you define collections on the Meteor server with
MyCollection = new Mongo.Collection('collection-name-in-mongo')
These collections contain all the data from the MongoDB collections, and you can run
MyCollection.find({...})
on them, which will return a cursor (a set of records, with methods to iterate through them and return them).This cursor is (most of the time) used to publish (send) a set of records (called a "record set"). You can optionally publish only some fields from those records. It is record sets (not collections) that clients subscribe to. Publishing is done by a publish function, which is called every time a new client subscribes, and which can take parameters to manage which records to return (e.g. a user id, to return only that user's documents).
On the client, you have Minimongo collections that partially mirror some of the records from the server. "Partially" because they may contain only some of the fields, and "some of the records" because you usually want to send to the client only the records it needs, to speed up page load, and only those it needs and has permission to access.
Minimongo is essentially an in-memory, non-persistent implementation of Mongo in pure JavaScript. It serves as a local cache that stores just the subset of the database that this client is working with. Queries on the client (find) are served directly out of this cache, without talking to the server.
These Minimongo collections are initially empty. They are filled by
Meteor.subscribe('record-set-name')
calls. Note that the parameter to subscribe isn't a collection name; it's the name of a record set that the server used in the
publish
call. Thesubscribe()
call subscribes the client to a record set - a subset of records from the server collection (e.g. most recent 100 blog posts), with all or a subset of the fields in each record (e.g. onlytitle
anddate
). How does Minimongo know into which collection to place the incoming records? The name of the collection will be thecollection
argument used in the publish handler'sadded
,changed
, andremoved
callbacks, or if those are missing (which is the case most of the time), it will be the name of the MongoDB collection on the server.
Modifying records
This is where Meteor makes things very convenient: when you modify a record (document) in the Minimongo collection on the client, Meteor will instantly update all templates that depend on it, and will also send the changes back to the server, which in turn will store the changes in MongoDB and will send them to the appropriate clients that have subscribed to a record set including that document. This is called latency compensation and is one of the seven core principles of Meteor.
Multiple subscriptions
You can have a bunch of subscriptions that pull in different records, but they'll all end up in the same collection on the client if the came from the same collection on the server, based on their _id
. This is not explained clearly, but implied by the Meteor docs:
When you subscribe to a record set, it tells the server to send records to the client. The client stores these records in local Minimongo collections, with the same name as the
collection
argument used in the publish handler'sadded
,changed
, andremoved
callbacks. Meteor will queue incoming attributes until you declare the Mongo.Collection on the client with the matching collection name.
What's not explained is what happens when you don't explicitly use added
, changed
and removed
, or publish handlers at all - which is most of the time. In this most common case, the collection argument is (unsurprisingly) taken from the name of the MongoDB collection you declared on the server at step 1. But what this means is that you can have different publications and subscriptions with different names, and all the records will end up in the same collection on the client. Down to the level of top level fields, Meteor takes care to perform a set union among documents, such that subscriptions can overlap - publish functions that ship different top level fields to the client work side by side and on the client, the document in the collection will be the union of the two sets of fields.
Example: multiple subscriptions filling the same collection on the client
You have a BlogPosts collection, which you declare the same way on both the server and the client, even though it does different things:
BlogPosts = new Mongo.Collection('posts');
On the client, BlogPosts
can get records from:
a subscription to the most recent 10 blog posts
// server
Meteor.publish('posts-recent', function publishFunction() {
return BlogPosts.find({}, {sort: {date: -1}, limit: 10});
}
// client
Meteor.subscribe('posts-recent');a subscription to the current user's posts
// server
Meteor.publish('posts-current-user', function publishFunction() {
return BlogPosts.find({author: this.userId}, {sort: {date: -1}, limit: 10});
// this.userId is provided by Meteor - http://docs.meteor.com/#publish_userId
}
Meteor.publish('posts-by-user', function publishFunction(who) {
return BlogPosts.find({authorId: who._id}, {sort: {date: -1}, limit: 10});
}
// client
Meteor.subscribe('posts-current-user');
Meteor.subscribe('posts-by-user', someUser);a subscription to the most popular posts
- etc.
All these documents come from the posts
collection in MongoDB, via the BlogPosts
collection on the server, and end up in the BlogPosts
collection on the client.
Now we can understand why you need to call find()
more than once - the second time being on the client, because documents from all subscriptions will end up in the same collection, and you need to fetch only those you care about. For example, to get the most recent posts on the client, you simply mirror the query from the server:
var recentPosts = BlogPosts.find({}, {sort: {date: -1}, limit: 10});
This will return a cursor to all documents/records that the client has received so far, both the top posts and the user's posts. (thanks Geoffrey).
Publish and subscribe to a single object Meteor js
As you said "This code snippet does not display anything on the view." well, inside Meteor.publish
you need to return cursor
, not array
or any other object
.
So use this code:
Meteor.publish('SingleSchool', function (schoolSlug) {
check( schoolSlug, Match.OneOf( String, null, undefined ) );
var user = Meteor.users.findOne({_id:this.userId});
if(!user || !user.emails[0].verified) {
throw new Meteor.Error('Not authorized');
}
return SchoolDb.find({ slug: schoolSlug, userId: {$lt: this.userId}},{limit:1});
});
I would definitely recommend you to go through How to avoid Common Mistakes
Diff between meteor method and meteor pub/sub
Meteor.publish
is the pub part of pub-sub obviously. As data gets added or changed that is being published the server will automatically send it over to any client that has subscribed to that publication.
Meteor.call
is request-response. You make a request, you get a response. Party's over. If data changes on the server that the method uses your client won't know about it until you make another call.
Publish/subscribe Meteor returns a whole Collection
Autopublish package publishes all data to client (it is like you subscribed to a publication that does Collection.find({})
on all collections). It means, that your custom publication/subscribtion actually does nothing in your example, and if you query data like this:
getMeteorData: function() {
return {
messages: Messages.find({}).fetch()
}
},
you will just get all messages, and that's correct behavior.
Anyway, you should always make the explicit query in data query like this:
getMeteorData: function() {
return {
messages: Messages.find({gameId: gameId}, {sort: {createdAt: -1}}).fetch()
}
},
because if you happen to have two different messages subscriptions at the same time (for different purposes, i.e. one subscription for one message with all details and another for different messages with less details/fields), this data query Messages.find({})
will get you union of the documents subscribed in both subscriptions, and it probably won't be what you would expect.
So, in your case, remove autopublish package, and add this explicit query in data access (messages: Messages.find({gameId: gameId}, {sort: {createdAt: -1}}).fetch()
).
Meteor publish subscribe is not reactive
This (excellent) article might help. To paraphrase:
...on the server, Meteor’s reactivity is limited to cursors returned by Meteor.publish() functions. The direct consequence of this is that unlike on the client, code will not magically re-run whenever data changes.
Related Topics
JavaScript Thousand Separator/String Format
What Is "Export Default" in JavaScript
Doesn't JavaScript Support Closures with Local Variables
Benefits of Prototypal Inheritance Over Classical
How to Dynamically Insert a <Script> Tag via Jquery After Page Load
How to Share Code Between Node.Js and the Browser
Escaping Strings in JavaScript
How to Create a Jquery Function (A New Jquery Method or Plugin)
Using Jquery to Replace One Tag with Another
Check If String Contains Only Digits
How Does the (Function() {})() Construct Work and Why Do People Use It
How to Extract the Hostname Portion of a Url in JavaScript
Filtering Array of Objects with Arrays Based on Nested Value
Javascript: Class.Method VS. Class.Prototype.Method
Using Address Instead of Longitude and Latitude with Google Maps API
Get First and Last Date of Current Month with JavaScript or Jquery