How to stop reactive updates on subscribed cursor?

Once my subscription fetches its data, I want to lock the cursor. I still want the data in mini-Mongo for optimistic UI, otherwise I could have fetched via a method.

So, is there any way to do this?

Collection.find({},{reactive: false}) on the client would disable reactivity. Did you try this?

Actually, For a better performance, I want to lock at the subscription stage, as in, the subscription shouldn’t even get any new data from the publication. With ‘{reactive: false}’ the minimongo still receives updates of new data in the subscription, just the cursor query, being non reactive, doesn’t pass the data further down the pipeline.

And, not just the performance, since Subscription runs under Tracker.autorun, any update from the client in the subscribed data forces component rerender which picks up the remote updates sitting in mini-mongo! I want to avoid exactly that behaviour.

Basically, what I want is, once the initial data is sent to the client, server shouldn’t interfere for further reads but the client’s writes should be with optimistic UI. I don’t know if that’s possible!

@mohitag19 Maybe https://atmospherejs.com/ground/db could be a solution. You can ground the database, start the subscription (receive/sync data), pause it after receiving data (data is still available on client because it’s saved in local storage) and then open it again if you want (to send the updates to the server / sync data).

You can use the low level publish api.

1 Like

You could use a null collection. This is a bit convoluted but does what you want.

export const Links = new Mongo.Collection('links');
export const LinksLocal = new Mongo.Collection(null);

Template.links.onCreated(function () {
  var template = this;
  template.subscribe('links.all', function links_subscribed(){
      console.log('links subscribed');
      LinksLocal.remove({});
      Links.find({},{reactive:false}).forEach((link) => {
        LinksLocal.insert(link);
      })
  });
});

Template.links.events({
  'click button.update'(event,template) {
    var id = event.currentTarget.id;
    var updated = new Date();
    var link = LinksLocal.findOne(id);
    // update back on the server, and if success update local
    Meteor.call('links.update', id, updated, (error, res) => {
      if(error) console.error(error);
      else LinksLocal.update(id,{$set:{updated: updated}});
    });
  },
}

@mohitag19 use low-level api as suggested by @copleykj

A pseudo code to elaborate:

Meteor.publish('nonReactivePublish', function (...args) {
  collection.find(selector).forEach(
    doc => this.added(collection._name, doc._id, doc)
  );
  this.ready();
})

Details: http://docs.meteor.com/api/pubsub.html#Subscription-added

How do you stay reactive within your subscription though? This approach indeed doesn’t push any changes from the server to the client after the first subscription, but the client also can’t change anything.

Thanks everyone for replying.

@gaurav7 Could you please confirm if using a low-level API for publishing can keep the optimistic UI for client updates as asked by @jamgold? If no, is this approach any better than fetching through a method?

By the way, for the time being, I am fetching the _id array with a method and then calling subscription of each doc through an iterator. It keeps my cursor (list of documents) in control and allows optimistic UI on the document data (like a ‘LIKE’ button is there which is reactive). It would have been better if I could just lock the cursor in one go!

The approach I shared will populate minimongo. So, if you display via the local minimongo collection reactively (i.e. not specifying { reactive: false } option), and if your method does local simulation, then that should be sufficient for “optimistic UI”.

i.e. I expect it to work as follows: (note the console.log statements)

// lib/collections/Likeables.js
Likeables = new Mongo.Collection('likeables');  // isomorphic

// lib/methods/like.js
Meteor.methods({
  like(_id) { // isomorphic; local simulation for "optimistic UI"
    return Likeables.update(_id, { $inc: { likes: 1 } });
  }
})

// server/publications/test.js
Meteor.publish('nonReactiveLikeable', function ({ _id }) {
  Likeables.find(_id).forEach(
    doc => this.added(Likeables._name, _id, doc)
  );
  this.ready();
})

// client/test.js
// assume it's running within a reactive context (tracker autorun)
const sub = Meteor.subscribe('nonReactiveLikeable', { _id: 'abc' });
if (sub.ready()) {
  const likeable = Likeables.findOne('abc');
  console.log('likes:', likeable.likes); // likes: 0
  Meteor.call('like', 'abc',
    (err, result) => err ? console.error(err) : console.log('result:', result)
  );
  console.log('likes:', likeable.likes) // likes: 1
}

That is what I did in my code and Likeables.update doesn’t work on the client, and a call to a method updates the collection on the server, which does not come back to the client.

@jamgold that approach has been working for me. You can try out the same example I pushed to github:

1 Like

I see why it works for you, because you use a method to update and it runs on client and server.

1 Like

Yeah that’s what I meant by

1 Like

Totally understand now. The only thing I don’t understand is why the method on the client doesn’t run into permission problems when trying to modify the collection.

1 Like

On the client, methods are part of trusted means for mutations. The permission problems are when you are mutating inside your event handlers.

1 Like

Permissions don’t come into picture for the collection mutator methods invoked within a simulation, because these invocations don’t call their remote counterparts, i.e. their execution is limited to the stubs only.

@gaurav7 thank you for this. Have been working with Meteor since 0.5 days and never knew this :grin: I also was not able to find this documented anywhere (which doesn’t mean it doesn’t exist somewhere)

1 Like

My pleasure @jamgold I have had the privilege (compulsion) to familiarize myself with many undocumented features of Meteor in the last year :wink:

2 Likes