Reactive method calls

I have a method call that queries several collections looking for a flag-field and returns a object that contains these values. On the callback I store said object in a Session variable for use throughout my application.

But I need it to be reactive. So that if one of the collections changes this flag-field the method reruns and the Session variable is updated accordingly.

The method looks something like this:

// both/methods.js

Meteor.methods({
  check_flag: function () {
    var userId = Meteor.userId();

    if (! userId) throw new Meteor.Error(404, 'need permission to do that');

    var select = { _id: userId }
    var filter = { _id: 0, some_flag: 1 }

    result_object = {
      flag_field: Collection1.findOne(select, filter) ? true : false,
      flag_field: Collection2.findOne(select, filter) ? true : false
    };

    return result_object;
  }
});

And I have this in a autorun like so:

// client/trackers.js

  Tracker.autorun(function(){
    Meteor.call('check_flag', function (err, results) {
      if (err) throw new Meteor.Error(err);
      Session.set('config.results', results);
    });
  });

At that point client already got callback, so method run is finished.

I would see possible solution doing .observe on server if it is possible and reacting on it somehow that client notice. For example adding some document to collection for this purpose so client will be notified and can rerun method.

But that looks like ugly solution :smiley:

Autorun sometimes feels like magic, but it’s not that magic :wink:

Autorun runs when a dependency inside it changes, this could be session variables, collection data, reactive vars etc. A meteor call is not a dependency so it won’t rerun if anything inside the method changes.
You could trigger the autorun to run on changes by simply including the find queries on the collection inside the autorun. Something along these lines:

Tracker.autorun(function(){
  var collection1 = Collection1.findOne({ _id: Meteor.userId()});
  var collection2 = Collection2.findOne({ _id: Meteor.userId()});

  Meteor.call('check_flag', function (err, results) {
    if (err) throw new Meteor.Error(err);
    Session.set('config.results', results);
  });
});

This would run the method if either documents in collection1 or 2 change

This would require collection1 and 2 be fully published to the client, though.

Thanks @nlammertyn, this would work all right. So in your example I’d be calling collections twice on a change? For example:

// client/trackers.js

Tracker.autorun(function(){
  var collection1 = Collection1.findOne({ _id: Meteor.userId()});
  var collection2 = Collection2.findOne({ _id: Meteor.userId()});

  Meteor.call('check_flag', function (err, results) {
    if (err) throw new Meteor.Error(err);
    Session.set('config.results', results);
  });
});

// both/methods.js

Meteor.methods({
  check_flag: function () {
    var userId = Meteor.userId();

    if (! userId) throw new Meteor.Error(404, 'need permission to do that');

    var select = { _id: userId }
    var filter = { _id: 0, some_flag: 1 }

    result_object = {
      flag_field: Collection1.findOne(select, filter) ? true : false,
      flag_field: Collection2.findOne(select, filter) ? true : false
    };

    return result_object;
  }
});

The client would make the query to mini mongo for the collection, but the collection would be empty on the client since there’s no publications/subscriptions for the collections. Then the method call would run and be called on the server, where it would actually pick up the data, filling the session with the object.

Since you’re using the collections just to trigger the reactivity, would they actually trigger since they’ll be empty on the client?

Ah indeed, if the method runs solely on the server and the collections aren’t published/subscribed, this would not work of course.

You’ll probably have to give some more info on what data we’re talking about and what you’re trying to do.

One solution would be to return that flag as a field in the user’s object, in other words store it in the db on a per-user basis. Then you could react to changes from those collections by using .observechanges() or collection-hooks or anything that can run code when a collection is changed/updated.

If you publish the Meteor.user collection to the client, you can listen to changes there (again through autorun or observeChanges) and if the field changes, set the session variable.

That is of course just one possible solution, but again it’ll probably be easier to answer if you gave slightly more info/context.

Thanks for the feedback, but I don’t follow. It sounds like you’re trying to solve ‘my problem’ now that we’ve run into a wall on making the method reactive. At this point it seems observe changes may be the way to go.

Well if you give your use-case we could probably advise on other ways to approach what you’re trying to do instead of using methods and trying to make them reactive, but if you’re adamant about using them you could try this package: https://atmospherejs.com/simple/reactive-method

these reactive-method suggestions are quite popular, but it has nothing to do with server side :smiley:

I was going to say the same thing.

Also, I’m not tied to doing a method, it just seemed the right way to go about adding the data to a session. The down side is no reactivity.

Without getting too deep, I need to query a bunch of collections that don’t live client side, and set a session variable with the resulting object for use on the client. I need to update the session reactively if the data in one of the collections changes. I would think this is a common scenario.

2 Likes

The only way how you can notify the Client and pass any data from Server is Meteor.publish and Meteor.subscribe

Is there a reason you can’t publish the results of the queries and the build the resulting object client side? Because as has been stated in this thread already, if you wrap the Session.set() in an autorun it will update when the data changes. So you could simply also do the object construction in the autorun and it should update reactively.

You could always hook into the publish function itself and observe the relevant collections manually and then push the data as a new psudo-collection to the client. See this part of the Meteor docs. http://docs.meteor.com/#/full/meteor_publish

How would I return all the flags from all the collections in one (1) publish method? Because I think having a lot of publish methods for one field in each collection feels wrong.

// server/publish.js

Meteor.publish('flag_check', function () {
  var self = this;
  var userId = self.userId;

  if (userId) {
   var select = { _id: userId }
   // returns _id and flag_field
   var filter = { flag_field: 1 }

   var collection1 = collection1.find(select, filter);
   var collection2 = collection2.find(select, filter);
   var collection3 = collection3.find(select, filter);
     
   /** Question: would returning an array of cursors like this work? **/
   return [
      collection1,
      collection2,
      collection3
   ];
  }
   
  return self.ready();
});

Question: Since we’ve subscribed on the client will this be reactive? Since we’re not passing in anything reactive we’ll need need a reactive data source to trigger the callback on changes to the collection right?

// client/trackers.js

var subscription = Meteor.subscribe('flag_check');

Tracker.autorun(function() {
  if (subscription.ready()) {

    /** IMPORTANT **/
    /** DO WE NEED THESE QUERIES HERE IN ODER TO TRIGGER REACTIVITY?? **/
    // Note: these collections will only contain the '_id' and 'flag_field' as published
    var collection1 = collection1.findOne({ _id: Meteor.userId()});
    var collection2 = collection2.findOne({ _id: Meteor.userId()});

    Meteor.call('check_flag', function (err, results) {
      if (err) throw new Meteor.Error(err);
      
      Session.set('config.results', results);
    });
  }
});

Note: now that we’ve subscribe on the client, now we can query mini-mongo.

// both/stubs/methods.js

Meteor.methods({
  check_flag: function () {
    var userId = Meteor.userId();

    if (! userId) throw new Meteor.Error(404, 'need permission to do that');

    var select = { _id: userId }
    var filter = {  flag_field: 1 }

    result_object = {
      flag_field: collection1.findOne(select, filter) ? true : false,
      flag_field: collection2.findOne(select, filter) ? true : false
    };

    return result_object;
  }
});

So there is a pretty hacky but fast way to do what you want to achieve.

  • Store a new field on each document of the collections with the ‘flag_1’ name.
    -specify to only return that field, i.e. collection1.find(select, {flag_1: 1}) (limit the selection to 1 if you want to mimic findOne
  • return an array of all the cursors.
  • on the client side now query every one of the published collections, and if it contains any values (should be 1 value if you used limit) you will get flag_1: true or similar and no result for the ones that didn’t match the filter.
  • You can wrap all this in an autorun with the session.set() and now it should be reactive.

Note this does mean returning the id of at least one of the flag objects, I originally thought you might be able to exclude it, but it seems that meteor doesn’t allow this. (Which makes sense in hindsight)

Let me know if any of this doesn’t make sense.

The other solution is to attach observeChanges to all your collections and manually generate the new collection with the data you want.

Also publish can only return a cursor and not an object. This is because the communication is done over DDP which be definition needs to be a cursor (otherwise it won’t know how to update minimongo if the server data changes)

1 Like

Thanks, and what would returning an array of cursors look like in my case?

Would it just look something like this?

return [
    collection1,
    collection2,
    collection3
  ];
 return [
     collection1.find(select, filter, limit, fields),
     collection2.find(select, filter, limit, fields),
     collection3.find(select, filter, limit, fields)
   ];

Something like this.

1 Like

I think this is the only part I’m not sure of, the reactivity.

I modified post #14 above.

My question is, do I need to add the collection calls inside the autorun to make the reactivity work (since I’m not passing in a reactive variable to the publish method for example)?

Something like this.

// In publications
Meteor.publish('allFlags') function() {
  return [
  collection1.find({$selector}, {fields: {_id: 1}, limit: 1})
  collection2.find({$selector}, {fields: {_id: 1}, limit: 1})
  collection3.find({$selector}, {fields: {_id: 1}, limit: 1})
  ]
});

// On relevant template
Template.XXX.onRendered(function () {
  Tracker.autorun(function(){
    var collections = [collection1, collection2, collection3]
    
    var flags = _.map(collections, function(collection) {
      if (collection.findOne()) {
        return {collection._name: true};
      } else {
      return {collection._name: false};
      }
    })
    Session.set('flags', flags);
  });
});

I guess it is not the true. DDP isn’t transfer cursors. If I publish cursor Collection.find({}} it would not send cursor or array of documents. DDP is an architecture - it sends messages. When your return a cursor from Meteor.publish livedata package handles changes from previous snapshot and send it’s differences with defined format. It means that you can emulate that behaviour. Thats way you can do:

Meteor.isServer
    Meteor.publish 'myPub', ->
        this.added 'mycollection', 'docid', {field1: 1, field2: 2}

Meteor.isClient
    MyDummyCollection = new Mongo.Collection('mycollection')
    Meteor.subscribe 'myPub'

Note, that I have MyDummyCollection on client only. But I published a data and marked that a document is added into mycollection collection.

That statement was in response to a previous question which has since been deleted where @aadams asked if a publish function could return an object, which it can’t only a cursor or array or cursors according to the API docs (key word here being return as in the literal return value, not whether you can manually construct a DDP publication).

And I did mention you could manually constuct the DDP message. However your explanation is better. :slight_smile: