Meteor minimongo dynamic cursor

964 Views Asked by At

In my client UI I have a form with differents search criterias, and I'd like to reactively update the results list. The search query is transformed into a classical minimongo selector, saved in a Session variable, and then I have observers to do things with the results:

// Think of a AirBnb-like application
// The session variable `search-query` is updated via a form
// example: Session.set('search-query', {price: {$lt: 100}});

Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
resultsCursor = Offers.find(Session.get('search-query'));

// I want to add and remove pins on a map
resultCursor.observe({
  added: Map.addPin,
  removed: Map.removePin
});

Deps.autorun(function() {
  // I want to modify the cursor selector and keep the observers
  // so that I would only have the diff between the old search and
  // the new one
  // This `modifySelector` method doesn't exist
  resultsCursor.modifySelector(Session.get('search-query'));
});

How could I implement this modifySelector method on the cursor object?

Basically I think this method needs to update the compiled version of the cursor, ie the selector_f attribute, and then rerun observers (without losing the cache of the previous results). Or is there any better solution?


Edit: Some of you have misunderstood what I'm trying to do. Let me provide a complete example:

Offers = new Meteor.Collection('offers');

if (Meteor.isServer && Offers.find().count() === 0) {
  for (var i = 1; i < 4; i++) {
    // Inserting documents {price: 1}, {price: 2} and {price: 3}
    Offers.insert({price:i})  
  }
}

if (Meteor.isClient) {
  Session.setDefault('search-query', {price:1});
  resultsCursor = Offers.find(Session.get('search-query'));

  resultsCursor.observe({
    added: function (doc) { 
      // First, this added observer is fired once with the document 
      // matching the default query {price: 1}
      console.log('added:', doc);
    }
  });

  setTimeout(function() {
    console.log('new search query');
    // Then one second later, I'd like to have my "added observer" fired
    // twice with docs {price: 2} and {price: 3}. 
    Session.set('search-query', {});
  }, 1000);
}
4

There are 4 best solutions below

1
On

This doesn't solve the problem in the way you seem to be wanting to, but I think the result is still the same. If this is a solution you explicitly don't want, let me know and I can remove the answer. I just didn't want to put code in a comment.

Offers = new Meteor.Collection('offers');
Session.setDefault('search-query', {});
Template.map.pins = function() {
   return Offers.find(Session.get('search-query'));
}

Template.map.placepins = function(pins) {
   // use d3 or whatever to clear the map and then place all pins on the map
}

Assuming your template is something like this:

<template name="map">
  {{placepins pins}}
</template>
1
On

Perhaps an acceptable solution would be to keep track of old pins in a local collection? Something like this:

Session.setDefault('search-query', {});

var Offers = new Meteor.Collection('offers');
var OldOffers = new Meteor.Collection(null);

var addNewPin = function(offer) {
  // Add a pin only if it's a new offer, and then mark it as an old offer
  if (!OldOffers.findOne({_id: offer._id})) {
    Map.addPin(offer);
    OldOffers.insert(offer);
  }
};

var removePinsExcept = function(ids) {
  // Clean out the pins that no longer exist in the updated query,
  // and remove them from the OldOffers collection
  OldOffers.find({_id: {$nin: ids}}).forEach(function(offer) {
    Map.removePin(offer);
    OldOffers.remove({_id: offer._id});
  });
};

Deps.autorun(function() {
  var offers = Offers.find(Session.get('search-query'));

  removePinsExcept(offers.map(function(offer) {
    return offer._id;
  }));

  offers.observe({
    added: addNewPin,
    removed: Map.removePin
  });
});

I'm not sure how much faster this is than your array answer, though I think it's much more readable. The thing you need to consider is whether diffing the results as the query changes is really much faster than removing all the pins and redrawing them each time. I would suspect that this might be a case of premature optimization. How often do you expect a user to change the search query, such that there will be a significant amount of overlap between the results of the old and new queries?

0
On

One solution is to manually diff the old and the new cursors:

# Every time the query change, do a diff to add, move and remove pins on the screen
# Assuming that the pins order are always the same, this use a single loop of complexity 
# o(n) rather than the naive loop in loop of complexity o(n^2)
Deps.autorun =>
  old_pins = @pins
  new_pins = []
  position = 0
  old_pin  = undefined # This variable needs to be in the Deps.autorun scope

  # This is a simple algo to implement a kind of "reactive cursor"
  # Sorting is done on the server, it's important to keep the order
  collection.find(Session.get('search-query'), sort: [['mark', 'desc']]).forEach (product) =>
    if not old_pin? 
      old_pin = old_pins.shift()

    while old_pin?.mark > product.mark
      @removePin(old_pin)
      old_pin = old_pins.shift()

    if old_pin?._id == product._id
      @movePin(old_pin, position++)
      new_pins.push(old_pin)
      old_pin = old_pins.shift()

    else
      newPin = @render(product, position++)
      new_pins.push(newPin)

  # Finish the job
  if old_pin?
    @removePin(old_pin)
  for old_pin in old_pins
    @removePin(old_pin)

  @pins = new_pins

But it's a bit hacky and not so efficient. Moreover the diff logic is already implemented in minimongo so it's better to reuse it.

0
On

I have the same problem in my own hobby Meteor project.

There is filter session var where selector is storing. Triggering any checkbox or button changes filter and all UI rerender.

That solution have some cons and the main - you can't share app state with other users.

So i realized that better way is storing app state in URL.

May be it is also better in your case?

Clicking button now change URL and UI rendering based on it. I realize it with FlowRouter.

Helpful reading: Keeping App State on the URL