How to deal with dynamic subscriptions in Meteor? - meteor

I have a publication whose scope depends on a element property from another collection. Basically it looks like this on the server:
Meteor.publish('kids', function (parent) {
return Kids.find({ _id: { $in: parent.childrenIds } });
}
In the example above parent.childrenIds is an array containing the _id's of all the kids that are children of the parent. This works fine until I want to add a new child to the parent:
newKidId = Kids.insert({ name: 'Joe' });
Parents.update({ _id: parentId }, { $push: { childrenIds: newKidId } });
This works on the server for the Kids collection (i.e., the new kid is added) and it updates the parent's childrenIds array with the newKidId. BUT it does not update the above 'kids' publication (the cursor is not updated/modified). As a result, the client's Kids collection is not updated (and it looks like the change to Kids is rolled back on the client).
When the client refreshes, all publications are stopped/restarted and the new kid (Joe) is finally published to the client.
Is there a way to avoid refreshing the client and forcing the re-publication of the Kids collection (ideally only sending the new kid Joe to the client)?

One of the things that is often misunderstood in Meteor is that there is no reactivity on the server. Dynamic descriptions need to be handled by Deps.autorun blocks on the client. To do this, first make sure you are not including the autopublish package by using this command in project directory:
$ meteor remove autopublish
Second, set up an autorun block on the client like:
Meteor.startup(function(){
Meteor.subscribe('parents');
Deps.autorun(function() {
parent = Parents.findOne();
if (!parent) return;
Meteor.subscribe('kids', parent);
});
});
This will tear down and set up subscriptions as the parent object changes.
You can see a full working example at https://gist.github.com/jagill/5473599 .

In meantime there were quite some packages made to deal with reactive publish functions. I am an author of meteor-related, and at the end of the package's README I compare my package with few other packages:
meteor-reactive-publish
meteor-publish-with-relations
meteor-smart-publish
reywood:publish-composite
copleyjk:simple-publish

These days you can simply use reactive-publish package (I am one of authors):
Meteor.publish('kids', function (parentId) {
this.autorun(function (computation) {
var parent = Parents.findOne(parentId, {fields: {childrenIds: 1}});
return Kids.find({_id: {$in: parent.childrenIds}});
});
}
It is important to limit Parents' query fields only to childrenIds so that autorun does not rerun for any other changes to Parents document.

I think you need to use observe in the publish function if a published query relies on a second query. Deps.autorun on the client is not necessary.
See discussions on Meteor server reactivity and Reactive updates when query is filtered by another query.
This is some code based on http://docs.meteor.com 'counts-by-room' example.
Meteor.publish( "kids", function(parent_id){
var self = this;
Parents.find({_id: parent_id}, { childrenIds: 1 }).observe({
added: function (document){
document.childrenIds.forEach( function(kidId){
self.added("kids", kidId, Kids.findOne( { _id: kidId}, {name: 1, _id: 1} ));
});
},
removed: function (oldDocument){
oldDocument.childrenIds.forEach( function(kidId){
self.removed("kids", kidId, Kids.findOne( { _id: kidId}, {name: 1, _id: 1} ));
});
},
changed: function (newDocument, oldDocument){
var oldLength = oldDocument.childrenIds.length;
var newLength = newDocument.childrenIds.length;
if (newLength > oldLength){
self.added("kids",
newDocument.childrenIds[newLength-1],
Kids.findOne( { _id: newDocument.childrenIds[newLength-1] }, {name:1, _id:1}) );
}
else{
self.removed("kids",
oldDocument.childrenIds[oldLength-1],
Kids.findOne( { _id: oldDocument.childrenIds[oldLength-1] }, {name:1, _id:1}) );
}
}
});
self.ready();
});

Related

Where do I set Session defaults so that they are available in my subscriptions?

I have a helper function that depends on a collection document lookup, the result of which it passes to a subscription via a Session. It then needs to query the documents from that subscription.
The code explains it better than I could.
Helper:
var selection = Selections.findOne()
var itemIds = function() {
return selection && selection.itemIds
}
var itemIdsArray = itemIds()
Session.set('itemIdsArray', itemIdsArray)
console.log(Session.get('itemIdsArray'))
_.each(Items.find({_id: {$in: itemIdsArray}}).fetch(), function(element, index, list) {
//doing stuff
})
Subscription:
Meteor.subscribe('itemsById', Session.get('itemIdsArray'))
Publication:
Meteor.publish('itemsById', function(itemIdsArray) {
return Items.find({_id: {$in: itemIdsArray}})
})
My console.log returns an undefined value before it returns the array of IDs. So undefined gets passed all the way to the publication, which complains of a null value (which is weird in itself) after $in and breaks.
My solution was to set the Session to default to [],
Session.setDefault(`itemIdsArray`, [])
which I honestly had high hopes that it'd work, but alas, it did not.
I've tried putting it inside IronRouter's onBeforeAction, I've tried putting it at the top of the helper, I've tried putting it pretty much anywhere but it still logs and returns undefined once before it gets the correct value.
I've also tried to move around my subscription, from waitOn to subscriptions to onAfterAction to onRendered, but those attempts have been utterly fruitless.
What should I do?
That's fairly typical behavior in Meteor. Session variables are not always ready at the time. The usual way of dealing with this is to introduce a guard in the helper that checks the variable is defined before doing anything else with it.
In your case something like this would work: itemsIdArray = itemIds() || [];
To answer the actual question you are asking, where do you set Session defaults that they are available in your subscriptions: it's not important where you set them, but when you access them. You can wait for the subscription to be ready using iron router's waitOn() function, or you can check the subscription handle's ready() function (see https://github.com/oortcloud/unofficial-meteor-faq#user-content-how-do-i-know-when-my-subscription-is-ready-and-not-still-loading)
If you return a subscription in your waitOn option of Iron Router you should have the data in your template then:
Router.route('/yourRoutePath/:_id', {
// this template will be rendered until the subscriptions are ready
loadingTemplate: 'loading',
waitOn: function () {
// return one handle, a function, or an array
return Meteor.subscribe('itemsById', this.params._id);
},
action: function () {
this.render('myTemplate');
}
});
Your template helper:
Template.myTemplate.helpers({
items: function() {
return Items.find();
}
});
I noticed that you publish Items collection and you want to use Selections collection in your helper. If you need more than one subscription, you can return an array of subscriptions in waitOn:
waitOn: function () {
// return one handle, a function, or an array
return [
Meteor.subscribe('itemsById', this.params._id),
Meteor.subscribe('selections')
];
}
WaitOn ensures that your template will be rendered when all subscriptions are ready.

Publish documents in a collection to a meteor client depending on the existence of a specific document in another collection (publish-with-relations)

I have two collections
Offers (relevant fields: _id)
ShareRelations (relevant fields: receiverId and offerId)
and I'd like to publish only Offers to the logged in user which have been shared to him.
Actually, I'm doing this by using a helper array (visibleOffers) which I fill by looping for each ShareRelations and use this array later on the Offers.find as $in selector.
I wonder if this might be the meteor way to do this, or if I could do with less and/or prettier code?
My actual code to publish the Offers is the following:
Meteor.publish('offersShared', function () {
// check if the user is logged in
if (this.userId) {
// initialize helper array
var visibleOffers = [];
// initialize all shareRelations which the actual user is the receiver
var shareRelations = ShareRelations.find({receiverId: this.userId});
// check if such relations exist
if (shareRelations.count()) {
// loop trough all shareRelations and push the offerId to the array if the value isn't in the array actually
shareRelations.forEach(function (shareRelation) {
if (visibleOffers.indexOf(shareRelation.offerId) === -1) {
visibleOffers.push(shareRelation.offerId);
}
});
}
// return offers which contain the _id in the array visibleOffers
return Offers.find({_id: { $in: visibleOffers } });
} else {
// return no offers if the user is not logged in
return Offers.find(null);
}
});
Furthermore, the actual solution has the downside that if a new share relations is being created, the Offers collection on the client doesn't get updated with the newly visible offer instantly (read: page reload required. But I'm not sure if this is the case because of this publish method or because of some other code an this question is not primary because of this issue).
What you are looking for is a reactive join. You can accomplish this by directly using an observe in the publish function, or by using a library to do it for you. Meteor core is expected to have a join library at some point, but until then I'd recommend using publish-with-relations. Have a look at the docs, but I think the publish function you want looks something like this:
Meteor.publish('offersShared', function() {
return Meteor.publishWithRelations({
handle: this,
collection: ShareRelations,
filter: {receiverId: this.userId},
mappings: [{collection: Offers, key: 'offerId'}]
});
});
This should reactively publish all of the ShareRelations for the user, and all associated Offers. Hopefully publishing both won't be a problem.
PWR is a pretty legit package - several of us use it in production, and Tom Coleman contributes to it. The only thing I'll caution you about is that as of this writing, the current version in atmosphere (v0.1.5) has a bug which will result in a fairly serious memory leak. Until it gets bumped, see my blog post about how to run an updated local copy.
update 2/5/14:
The discover meteor blog has an excellent post on reactive joins which I highly recommend reading.
The way to do this is along the lines of this Question using observeChanges(). Still trying to figure out how to get it all working for my example, see Meteor, One to Many Relationship & add field only to client side collection in Publish?
You can use the reactive-publish package (I am one of authors):
Meteor.publish('offersShared', function () {
// check if the user is logged in
if (this.userId) {
this.autorun(function (computation) {
// initialize helper array
var visibleOffers = [];
// initialize all shareRelations which the actual user is the receiver
var shareRelations = ShareRelations.find({receiverId: this.userId}, {fields: {offerId: 1}});
// loop trough all shareRelations and push the offerId to the array if the value isn't in the array actually
shareRelations.forEach(function (shareRelation) {
if (visibleOffers.indexOf(shareRelation.offerId) === -1) {
visibleOffers.push(shareRelation.offerId);
}
});
// return offers which contain the _id in the array visibleOffers
return Offers.find({_id: { $in: visibleOffers } });
});
} else {
// return no offers if the user is not logged in
return Offers.find(null);
}
});
You can simply wrap your existing non-reactive code into an autorun and it will start to work. Just be careful to be precise which fields you query on because if you query on all fields then autorun will be rerun on any field change of ShareRelations, not just offerId.

How to get a published collection's total count, regardless of a specified limit, on the client?

I'm using the meteor-paginated-subscription package in my app. On the server, my publication looks like this:
Meteor.publish("posts", function(limit) {
return Posts.find({}, {
limit: limit
});
});
And on the client:
this.subscriptionHandle = Meteor.subscribeWithPagination("posts", 10);
Template.post_list.events = {
'click #load_more': function(event, template) {
template.subscriptionHandle.loadNextPage();
}
};
This works well, but I'd like to hide the #load_more button if all the data is loaded on the client, using a helper like this:
Template.post_list.allPostsLoaded = function () {
allPostsLoaded = Posts.find().count() <= this.subscriptionHandle.loaded();
Session.set('allPostsLoaded', allPostsLoaded);
return allPostsLoaded;
};
The problem is that Posts.find().count() is returning the number of documents loaded on the client, not the number available on the server.
I've looked through the Telescope project, which also uses the meteor-paginated-subscription package, and I see code that does what I want to do:
allPostsLoaded: function(){
allPostsLoaded = this.fetch().length < this.loaded();
Session.set('allPostsLoaded', allPostsLoaded);
return allPostsLoaded;
}
But I'm not sure if it's actually working. Porting their code into mine does not work.
Finally, it does look like Mongo supports what I want to do. The docs say that, by default, cursor.count() ignores the effects of limit.
Seems like all the pieces are there, but I'm having trouble putting them together.
None of the answers do what you really want becase none provide solution that is reactive.
This package does exactly what you want and also reactive.
publish-counts
I think you can see the demo: counts-by-room in meteor doc
It can help you publish the counts of your posts at server and get it at client
You can simply write this:
// server: publish the current size of your post collection
Meteor.publish("counts-by-room", function () {
var self = this;
var count = 0;
var initializing = true;
var handle = Posts.find().observeChanges({
added: function (id) {
count++;
if (!initializing)
self.changed("counts", 'postCounts', {count: count});
},
removed: function (id) {
count--;
self.changed("counts", postCounts, {count: count});
}
});
initializing = false;
self.added("counts", 'postCounts', {count: count});
self.ready();
self.onStop(function () {
handle.stop();
});
});
// client: declare collection to hold count object
Counts = new Mongo.Collection("counts");
// client: subscribe to the count for posts
Tracker.autorun(function () {
Meteor.subscribe("postCounts");
});
// client: simply use findOne, you can get the count object
Counts.findOne()
The idea of sub.loaded() is to help you with exactly this problem.
Posts.count() isn't going to return the right thing because, as you've guessed, on the client, Meteor has no way of knowing the real number of posts that live on the server. But what the client knows is how many posts it's tried to load. That's what that .loaded() tells you, and is why the line this.fetch().length < this.loaded() will tell you if there are more posts on the server or not.
What I would do is write a Meteor server side method that retrieves the count like so:
Meteor.methods({
getPostsCount: function () {
return Posts.find().count();
}
});
Then call it on the client, in observe to make it reactive:
function updatePostCount() {
Meteor.call('getPostsCount', function (err, count) {
Session.set('postCount', count);
});
}
Posts.find().observe({
added: updatePostCount,
removed: updatePostCount
});
Although this question is old, I thought I would provide an answer that ended up working for me. I did not create the solution, I found the basis for it here (so credit where credit is due): Discover Meteor
Anyway, in my case I was trying to get "size" of the database from client side, so I can determine when to hide the "load more" -button. I was using template level subscriptions. Oh and for this solution to work, you need to add reactive-var -package. Here is my (in short):
/*on the server we define the method which returns
the number of posts in total in the database*/
if(Meteor.isServer){
Meteor.methods({
postsTotal: function() {
return PostsCollection.find().count();
}
});
}
/*In the client side we first create the reactive variable*/
if(Meteor.isClient){
Template.Posts.onCreated(function() {
var self = this;
self.totalPosts = new ReactiveVar();
});
/*then in my case, when the user clicks the load more -button,
we call the postsTotal-method and set the returned value as
the value of the totalPosts-reactive variable*/
Template.Posts.events({
'click .load-more': function (event, instance){
Meteor.call('postsTotal', function(error, result){
instance.totalPosts.set(result);
});
}
});
}
Hope this helps someone (I recommend checking the link first). For template level subscriptions, I used this as my guide Discover Meteor - template level subscriptions. This was my first stacked-post and I am just learning Meteor, so please have mercy...:D
Ouch this post is old, anyway maybe it will help someone.
I had exactly the same issue. I managed to solve it with 2 simple lines...
Remember the :
handle = Meteor.subscribeWithPagination('posts', 10);
Well I used in client handle.loaded() and Posts.find().count(). Because when they are different it means that all the posts are loaded. So here is my code :
"click #nextPosts":function(event){
event.preventDefault();
handle.loadNextPage();
if(handle.loaded()!=Posts.find().count()){
$("#nextPosts").fadeOut();
}
}
I had the same problem, and using the publish-counts package didn't work with the subs-manager package. I created a package that can set a reactive server-to-client session, and keep the document count in this session. You can find an example here:
https://github.com/auweb/server-session/#getting-document-count-on-the-client-before-limit-is-applied
I'm doing something like this:
On cliente
Template.postCount.posts = function() {
return Posts.find();
};
Then you create a template:
<template name="postCount">
{{posts.count}}
</template>
Then, whatever you want to show the counter: {{> postCount}}
Much easier than any solution i have seen.

Why is Meteor removing an object when observing a collection?

Background
I have "Lists" and "Products" collections, Products belong to a List
A List has a description, from which products are generated
On startup, a new List is created that's unique for that visitor
The List id is stored in the Session
What I Want
I want Products to be generated when the description of a List changes.
The first step is that when the list for the current visitor is changed, I want a new product to be inserted.
I get the feeling I'm going about this totally wrong...
The Problem
The product is inserted, appears in the browser for a split second, then vanishes. It's been removed by Meteor.
Code
Products = new Meteor.Collection("products");
Lists = new Meteor.Collection("lists");
if (Meteor.isClient) {
Meteor.startup(function () {
var my_list_id = Lists.insert({description: "Default list"});
Session.set("my_list", my_list_id);
var observed = Lists.find({_id: my_list_id}).observe({
changed: function (newDocument, oldDocument) {
Products.insert({list: newDocument._id, name: newDocument.description});
}
});
});
toggleElement = function (elementName) {
if(editedElementIs(elementName)) {
var newListDescription = $('textarea').val();
Lists.update(Session.get("my_list"), {description: newListDescription});
setEditedElement("");
} else {
setEditedElement(elementName);
}
};
// Including the rest in case I've misunderstood something.
// I don't see how any of this could cause the issue.
setEditedElement = function (elementName) {
return Session.set("edited_element", elementName);
};
editedElementIs = function (elementName) {
return Session.get("edited_element") == elementName;
};
Handlebars.registerHelper('editedElementIs', editedElementIs);
Handlebars.registerHelper('products', function() {
return Products.find({list: Session.get("my_list")});
});
Template.list_form.listDescription = function () {
return Lists.findOne({_id: Session.get("my_list")}).description;
};
Template.adminbar.events({
'click a#editlist' : function () {
toggleElement("list");
},
'click a#editsidebar' : function () {
toggleElement("sidebar");
}
});
}
if (Meteor.isServer) {
Meteor.startup(function () {
});
}
What I've Tried
Obviously, I can just do this:
if(editedElementIs(elementName)) {
var newListDescription = $('textarea').val();
Products.insert({list: Session.get("my_list"), name: newListDescription});
Lists.update(Session.get("my_list"), {description: newListDescription});
...
But that's writing clumsy update code that I'd like to house in an observer.
It looked like the product was being removed. So I've observed when a product is removed thus:
Products.find({list:my_list_id}).observe({
removed: function (oldDocument) {
throw error("wow");
console.log("Removed Product" + oldDocument);
}
})
and this observer is called immediately after the Product is inserted.
I get the stack trace:
at Object.Products.find.observe.removed (http://localhost:3000/ListyMeteor.js?2d867b7481df6389658be864b54d864151e87da5:22:15)
at Object.cursor.observeChanges.removed (http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:909:52)
at http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:275:15
at _.extend.runTask (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:30:11)
at _.extend.flush (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:58:10)
at _.extend.drain (http://localhost:3000/packages/meteor/fiber_stubs_client.js?52687e0196bc1d3184ae5ea434a8859275702d94:66:12)
at LocalCollection.remove (http://localhost:3000/packages/minimongo/minimongo.js?daa88dc39d67b40b11d6d6809d72361f9ef6a760:500:22)
at Object.self._connection.registerStore.update (http://localhost:3000/packages/mongo-livedata/collection.js?682caa185350aa26968d4ffc274579a33922f0e6:109:32)
at Object.store.(anonymous function) [as update] (http://localhost:3000/packages/livedata/livedata_connection.js?5d09753571656c685bb10c7970eebfbf23d35ef8:404:48)
at http://localhost:3000/packages/livedata/livedata_connection.js?5d09753571656c685bb10c7970eebfbf23d35ef8:984:19
It looks like Meteor is flushing the Products collection on the client side.
I'm clearly misunderstanding how Meteor works.
Any ideas on why this is happening?
Update 1
It looks like this is happening because insert is being called within an observer:
Why does meteor undo changes to collections nested in an observer method?
I'll post back here once I confirm.
Is autosubscribe turned on or off?
If you turn autosubscribe off, it could happen that your client updates the server copy and then on a subsequent update from the server - does not get all the items because its not subscribed to that collection.
Easiest way to check is to query the mongo db -
meteor mongo
Query the mongo db if your product has been added to the document.
If it has, then it is an autosubscribe issue -
You will have to create publish (on server) and subscribe (on client) methods as given here http://docs.meteor.com/#meteor_publish

How to make dynamic subscriptions secure in Meteor?

This question builds on a previous one (see here).
The dynamic subscription is set up with this code (slightly modified from the previous question):
Meteor.startup(function(){
Meteor.subscribe('parents');
Deps.autorun(function() {
parent = Parents.findOne({ _id: Session.get('parentId') });
if (!parent) return;
Meteor.subscribe('kids', parent);
});
});
The problem is that the server side must trust the parent object that is passed by the client. Ideally, one would want to pass only the _id of the parent object like this:
Deps.autorun(function() {
parentId = Session.get('parentId');
if (!parentId) return;
Meteor.subscribe('kids', parentId);
});
But, in this case, the dynamic subscription behavior breaks (e.g., the kids collection is not updated on the client when the parent's children array is updated).
Why is Session.get('parentId') less reactive than Parents.findOne({ _id: Session.get('parentId') }), or has this to do with Meteor.subscribe('kids', parent) vs. Meteor.subscribe('kids', parentId)?
What would be the best pattern to coding this right?
It seems like what you want to do is the following:
Deps.autorun(function() {
parent = Parents.findOne({ _id: Session.get('parentId') }, {fields: {_id: 1}});
if (!parent) return;
Meteor.subscribe('kids', parent._id);
});
However, this still isn't exactly secure; it's just checking the Parents collection to make sure the referenced Session variable exists before attempting to subscribe to it - and this depends on the parents subscription. If you want it to be properly secured, you'll want to not send any parents over on the parent subscription to the client, if the client shouldn't be able to see them.

Resources