Martimatix
Martimatix

Reputation: 1643

How do I publish two random items from a Meteor collection?

I'm making an app where two random things from a collection are displayed to the user. Every time the user refreshes the page or clicks on a button, she would get another random pair of items.

For example, if the collection were of fruits, I'd want something like this:

apple vs banana

peach vs pineapple

banana vs peach

The code below is for the server side and it works except for the fact that the random pair is generated only once. The pair doesn't update until the server is restarted. I understand it is because generate_pair() is only called once. I have tried calling generate_pair() from one of the Meteor.publish functions but it only sometimes works. Other times, I get no items (errors) or only one item.

I don't mind publishing the entire collection and selecting random items from the client side. I just don't want to crash the browser if Items has 30,000 entries.

So to conclude, does anyone have any ideas of how to get two random items from a collection appearing on the client side?

var first_item, second_item;

// This is the best way I could find to get a random item from a Meteor collection
// Every item in Items has a 'random_number' field with a randomly generated number between 0 and 1
var random_item = function() {
  return Items.find({
    random_number: {
      $gt: Math.random()
    }
  }, {
    limit: 1
  });
};

// Generates a pair of items and ensure that they're not duplicates.
var generate_pair = function() {
  first_item = random_item();
  second_item = random_item();

  // Regenerate second item if it is a duplicate
  while (first_item.fetch()[0]._id === second_item.fetch()[0]._id) {
    second_item = random_item();
  }
};

generate_pair();

Meteor.publish('first_item', function() {
  return first_item;
});

// Is this good Meteor style to have two publications doing essentially the same thing?
Meteor.publish('second_item', function() {
  return second_item;
});

Upvotes: 2

Views: 639

Answers (3)

Andy Lorenz
Andy Lorenz

Reputation: 3084

Here's another approach, uses the excellent publishComposite package to populate matches in a local (client-only) collection so it doesn't conflict with other uses of the main collection:

if (Meteor.isClient) {
  randomDocs = new Mongo.Collection('randomDocs');
}

if (Meteor.isServer) {
  Meteor.publishComposite("randomDocs",function(select_count) {
    return {
      collectionName:"randomDocs",
      find: function() {
        let self=this;
        _.sample(baseCollection.find({}).fetch(),select_count).forEach(function(doc) {
          self.added("randomDocs",doc._id,doc);
        },self);
        self.ready();
      }
    }
  });
}

in onCreated: this.subscribe("randomDocs",3);
(then in a helper): return randomDocs.find({},{$limit:3});

Upvotes: 0

sunstory
sunstory

Reputation: 181

Meteor.publish 'randomDocs', ->
  ids = _(Docs.find().fetch()).pluck '_id'
  randomIds = _(ids).sample 2
  Docs.find _id: $in: randomIds

Upvotes: 1

saimeunt
saimeunt

Reputation: 22696

The problem with your approach is that subscribing to the same publication with the same arguments (no arguments in this case) over and over in the client will only get you subscribed only once to the server-side logic, this is because Meteor is optimizing its internal Pub/Sub mechanism.

To truly discard the previous subscription and get the server-side publish code to re-execute and send two new random documents, you need to introduce a useless random argument to your publication, your client-side code will subscribe over and over to the publication with a random number and each time you'll get unsubscribed and resubscribed to new random documents.

Here is a full implementation of this pattern :

server/server.js

function randomItemId(){
  // get the total items count of the collection
  var itemsCount = Items.find().count();
  // get a random number (N) between [0 , itemsCount - 1]
  var random = Math.floor(Random.fraction() * itemsCount);
  // choose a random item by skipping N items
  var item = Items.findOne({},{
    skip: random
  });
  return item && item._id;
}

function generateItemIdPair(){
  // return an array of 2 random items ids
  var result = [
    randomItemId(),
    randomItemId()
  ];
  //
  while(result[0] == result[1]){
    result[1] = randomItemId();
  }
  //
  return result;
}

Meteor.publish("randomItems",function(random){
  var pair = generateItemIdPair();
  // publish the 2 items whose ids are in the random pair
  return Items.find({
    _id: {
      $in: pair
    }
  });
});

client/client.js

// every 5 seconds subscribe to 2 new random items
Meteor.setInterval(function(){
  Meteor.subscribe("randomItems", Random.fraction(), function(){
    console.log("fetched these random items :", Items.find().fetch());
  });
}, 5000);

You'll need to meteor add random for this code to work.

Upvotes: 3

Related Questions