Meteor Collection Hooks... Confused!

So I have a collection file, located in /lib/collections/products.js in which I define my collection and in using Meteor Collection Hooks I was hoping to implement some cascading deletes and updates.

This would save on general “maintenance” code included in all my meteor methods that might delete a product, for example.

Products = new Mongo.Collection('products');

///////////////////////////////////////////////////////////////////
//                         HOOKS                                 //
///////////////////////////////////////////////////////////////////

Products.after.insert(function (userId, doc) {
  // Increment the product category's product count
  ProductCategories.update(doc.category, { $inc: { productCount: 1 } });
});

Products.before.remove(function (userId, doc) {
  // Decrement the product category's product count
  ProductCategories.update(doc.category, { $inc: { productCount: -1 } });

  // Ensure all print list entries that reference this product are removed
  PrintLists.update({ 'items.product': doc._id }, { $pull: { items: { product: doc._id } } }, { multi: true });
});

Now perhaps I completely misunderstood the idea of the hooks, but if I delete a product, it seems as though the Products.before.remove() function fires in both the client and the server. Rather than operating like a Meteor method, (operating in “simulation mode” on the client) it seems as though the decrement to productCount is actually called twice, one on the client, one on the server and both take effect!?

Weirdly, the same doesn’t seem to happen to the increment in the Products.after.insert() function… No idea why!?

Can someone explain what is going on, as I’m obviously a bit lost! :blush:

4 Likes

You’re right! I’ve experienced the same behavior in a similar use case. I’m essentially creating activities in an afterInsert hook.

// common.js
Products = new Meteor.Collection('products');
Activities = new Meteor.Collection('activities');

Products.after.insert(function (userId, doc) {
  Activities.insert({productId: doc._id, title: doc.name + " inserted."});
});

It ends up creating 2 activities. My workaround is to only have the hook defined in the server code. But you loose the latency compensation then. Not sure if this behavior is intended or a bug. :confused:

Actually, when you wrap your insert code into a Meteor method

// common.js
Meteor.methods({
  insertProduct: function (name) {
    Products.insert({name: name});
  },
});

and call it with

Meteor.call('insertProduct', "A Product")

then it works. This creates only one activity and makes use of latency compensation.

2 Likes

I guess that makes sense really, it’s just that I had wrongly assumed that the client side versions of these function calls were effectively the same as the latency-compensated methods… Perhaps the documentation for the package needs to give an example or at least a warning that this isn’t the case!?

I’d still go with defining hooks on the server. The problem is, collection-hooks does not track the server outcome of the hook trigger.

So let’s say you make an insert on the client and it triggers an increment update on the client. But the same insert fails on the server (eg due to a server checked constraint). What happens is, the insert ends up not happening, but the increment does go through since there is nothing against an increment.

This is the case wether or not a method or direct updates are used.

Therefore, the safest bet is, triggering your hooks on the server side where you know you’ve completed your sanity checks and you are good to go.

1 Like

Knowing what I know now, I agree and will be keeping my hooks server-side only. However, if the hooks just did simulated effects on the client, much like methods, then that issue wouldn’t arise.

The Meteor.method technique actually does work! The client will run the hook instantly (latency compensation) but if the server says something different, it will revert the changes. Take my example from above. If you define the method like this:

// common.js
Meteor.methods({
  insertProduct: function (name) {
    if (Meteor.isServer)
      return false
    else
      Products.insert({name: name});
  },
});

you will see a short flickering of the inserted product (AND activity) but that will be reverted as if nothing ever happened. Same thing should be happening with incrementing/decrementing.

3 Likes

Hmm, I wonder if this is the collection-hooks package nature or meteor methods nature and also I do wonder if this is by design or coinsidence, or perhaps even a bug :smile:

So the takeaway seems to be that if we wrap methods within other methods, latencey compensation works in such a way that unless all components of the method from the outermost parrent to innermost child run without error on the server, anything done on the client will have just been canceled away.

And I’m also guessing what constitutes an error is a proper Meteor.error object that is properly thrown from within a callback.

Can anyone confirm/argue this?

1 Like

Man…I was diggin this for few hours today :smile: First I thought the problem was with my for loop but then I understood that was a bug or smth. It should be really added to the docs. Will wrap it into a server method then…

nice workaround, thanks

I am trying to do the same, I am doing an insert of a default row with this relation key value, when I observe what happens during my insert in Assesments collection, I just see a flicker of ChairAssesments insert and later it disappears. Not sure what is happening here I tried it with both with Meteor.isServer and without it, both gives same effect.

if(Meteor.isServer){
    
Assesments.after.insert(function (userId, doc) {
  ChairAssesments.insert({ assesmentId: doc._id });
});
    
}