1

Can anyone see what may be wrong in this code, basically I want to check if a post has been shared by the current logged in user AND add a temporary field to the client side collection: isCurrentUserShared.

This works the 1st time when loading a new page and populating from existing Shares, or when adding OR removing a record to the Shares collection ONLY the very 1st time once the page is loaded.

1) isSharedByMe only changes state 1 time, then the callbacks still get called as per console.log, but isSharedByMe doesn't get updated in Posts collection after the 1st time I add or remove a record. It works the 1st time.

2) Why do the callbacks get called twice in a row, i.e. adding 1 record to Sharescollection triggers 2 calls, as show by console.log.

Meteor.publish('posts', function() {

    var self = this;
    var mySharedHandle;


    function checkSharedBy(IN_postId) {
        mySharedHandle = Shares.find( { postId: IN_postId, userId: self.userId }).observeChanges({

            added: function(id) {
                console.log("   ...INSIDE checkSharedBy(); ADDED: IN_postId = " + IN_postId );
                self.added('posts', IN_postId, { isSharedByMe: true });
            },

            removed: function(id) {
                console.log("   ...INSIDE checkSharedBy(); REMOVED: IN_postId = " + IN_postId );
                self.changed('posts', IN_postId, { isSharedByMe: false });
            }
        });
    }


    var handle = Posts.find().observeChanges({

        added: function(id, fields) {
            checkSharedBy(id);
            self.added('posts', id, fields);
        },

        // This callback never gets run, even when checkSharedBy() changes field isSharedByMe.
        changed: function(id, fields) {
            self.changed('posts', id, fields);
        },

        removed: function(id) {
            self.removed('posts', id);
        }
    });

    // Stop observing cursor when client unsubscribes
    self.onStop(function() {
        handle.stop();
        mySharedHandle.stop();
    });

    self.ready();
});
Giant Elk
  • 5,375
  • 9
  • 43
  • 56
  • 1
    You are missing a parenthesis after {isCurrentUserShared: 1}, but I assume that's not the problem. What is the behavior you are seeing? – Christian Fritz Dec 19 '13 at 18:15
  • Thanks I fixed the typo, but that's not the main issue. – Giant Elk Dec 20 '13 at 17:17
  • Here's another way of solving this using an array of shares in the posts collection. I'm not sure which is more efficient, but this way is a lot more code: http://stackoverflow.com/questions/20689113/meteor-how-to-check-if-item-in-array-field-but-exclude-that-field-in-publish/20708747#20708747 – Giant Elk Dec 20 '13 at 17:19
  • From reading the Mongo docs about max doc size 16Meg, and rethinking what would happen if millions of shares occur I'm staying away from this pattern (link below) unless the embedded array is not going to be too big i.e. less than a few thousand items: http://stackoverflow.com/questions/20689113/meteor-how-to-check-if-item-in-array-field-but-exclude-that-field-in-publish/20708747#20708747 – Giant Elk Dec 20 '13 at 23:08
  • Does the https://github.com/erundook/meteor-publish-with-relations package help you at all? – Andrew Mao Jan 23 '14 at 05:52
  • Hi Andrew, that package doesn't answer my question, of how to get the above code working. Also publish-with-relations has a memory leak bug issuehttps://github.com/erundook/meteor-publish-with-relations/issues/12, AND also the use case documentation is weak. – Giant Elk Jan 24 '14 at 02:31

3 Answers3

2

Personally, I'd go about this a very different way, by using the $in operator, and keeping an array of postIds or shareIds in the records.

http://docs.mongodb.org/manual/reference/operator/query/in/

I find publish functions work the best when they're kept simple, like the following.

Meteor.publish('posts', function() {
    return Posts.find();
});
Meteor.publish('sharedPosts', function(postId) {
    var postRecord = Posts.findOne({_id: postId});
    return Shares.find{{_id: $in: postRecord.shares_array });
});
AbigailW
  • 893
  • 12
  • 20
  • Using an array will get out of hand quick and lead to more work to change to 'reference' later. Imagine not just shares, but also, likes, comments, etc, it will quickly grow beyond 16Meg Mongo doc limit for big app. For time being I'm going to do it like in Telescope, but this is putting the load on the client, imagine all the extra trips due to client needing to update param in publish callback VS doing the join on the server. https://github.com/SachaG/Telescope/blob/master/server/publications.js – Giant Elk Jan 24 '14 at 02:28
  • Having run across something like this in project, this now seems like the best approach. – user728291 Feb 02 '14 at 00:52
1

I am not sure how far this gets you towards solving your actual problems but I will start with a few oddities in your code and the questions you ask.

1) You ask about a Phrases collection but the publish function would never publish anything to that collection as all added calls send to minimongo collection named 'posts'.

2) You ask about a 'Reposts' collection but none of the code uses that name either so it is not clear what you are referring to. Each element added to the 'Posts' collection though will create a new observer on the 'Shares' collection since it calls checkSharedId(). Each observer will try to add and change docs in the client's 'posts' collection.

3) Related to point 2, mySharedHandle.stop() will only stop the last observer created by checkSharedId() because the handle is overwritten every time checkSharedId() is run.

4) If your observer of 'Shares' finds a doc with IN_postId it tries to send a doc with that _id to the minimongo 'posts' collection. IN_postId is passed from your find on the 'Posts' collection with its observer also trying to send a different doc to the client's 'posts' collection. Which doc do you want on the client with that _id? Some of the errors you are seeing may be caused by Meteor's attempts to ignore duplicate added requests.

From all this I think you might be better breaking this into two publish functions, one for 'Posts' and one for 'Shares', to take advantage of meteors default behaviour publishing cursors. Any join could then be done on the client when necessary. For example:

//on server
Meteor.publish('posts', function(){
  return Posts.find();
});

Meteor.publish('shares', function(){
  return Shares.find( {userId: this.userId }, {fields: {postId: 1}} );
});

//on client - uses _.pluck from underscore package
Meteor.subscribe( 'posts' );
Meteor.subscribe( 'shares');

Template.post.isSharedByMe = function(){  //create the field isSharedByMe for a template to use
  var share = Shares.findOne( {postId: this._id} );
  return share && true;
};

Alternate method joining in publish with observeChanges. Untested code and it is not clear to me that it has much advantage over the simpler method above. So until the above breaks or becomes a performance bottleneck I would do it as above.

Meteor.publish("posts", function(){
  var self = this;
  var sharesHandle;
  var publishedPosts = [];
  var initialising = true;  //avoid starting and stopping Shares observer during initial publish

  //observer to watch published posts for changes in the Shares userId field
  var startSharesObserver = function(){
    var handle = Shares.find( {postId: {$in: publishedPosts}, userId === self.userId }).observeChanges({

      //other observer should have correctly set the initial value of isSharedByMe just before this observer starts.
      //removing this will send changes to all posts found every time a new posts is added or removed in the Posts collection 
      //underscore in the name means this is undocumented and likely to break or be removed at some point
      _suppress_initial: true,

      //other observer manages which posts are on client so this observer is only managing changes in the isSharedByMe field 
      added: function( id ){
        self.changed( "posts", id, {isSharedByMe: true} );
      },

      removed: function( id ){
        self.changed( "posts", id, {isSharedByMe: false} );
      }
    });
    return handle;
  };

  //observer to send initial data and always initiate new published post with the correct isSharedByMe field.
  //observer also maintains publishedPosts array so Shares observer is always watching the correct set of posts.  
  //Shares observer starts and stops each time the publishedPosts array changes
  var postsHandle = Posts.find({}).observeChanges({
    added: function(id, doc){
      if ( sharesHandle ) 
        sharesHandle.stop();
      var shared = Shares.findOne( {postId: id});
      doc.isSharedByMe = shared && shared.userId === self.userId;
      self.added( "posts", id, doc);
      publishedPosts.push( id );
      if (! initialising)
        sharesHandle = startSharesObserver();
    },
    removed: function(id){
      if ( sharesHandle ) 
        sharesHandle.stop();
      publishedPosts.splice( publishedPosts.indexOf( id ), 1);
      self.removed( "posts", id );
      if (! initialising)
        sharesHandle = startSharesObserver();
    },
    changed: function(id, doc){
      self.changed( "posts", id, doc);
    }
  });

  if ( initialising )
    sharesHandle = startSharesObserver();

  initialising = false;
  self.ready();

  self.onStop( function(){
    postsHandle.stop();
    sharesHandle.stop();
  });
});
user728291
  • 4,138
  • 1
  • 23
  • 26
  • Edited Q to keep collection names consistent. Hoping to get simpler Join so it didn't publish ALL my users Shares, only the shares that are displayed i.e. I will eventually add pagination to this, so a Join that is more closely tied to each published Post is what is needed. Regarding your point 3) do you mean I should not use mySharedHandle.stop(), or put that line inside checkSharedId()? Your second #3) or 4th point I thought this was the way to add a new field to miniMongo. My ideas are from: http://stackoverflow.com/questions/10565654/how-does-the-messages-count-example-in-meteor-docs-work – Giant Elk Jan 12 '14 at 04:51
  • Is the join of Shares and Posts the basic gist of what you want to end up with on the client? If you want to limit what is published by 'posts' or 'shares' you can have the [client pass criteria to the publish function](http://stackoverflow.com/questions/20115722/pub-sub-criteria-issue). – user728291 Jan 12 '14 at 11:09
  • Yes, but then the subscription will have to check every post that is shown to the client to see if it's been shared. In other words this way will be far slower, imagine a list of 100 posts, before each post can be properly displayed in the browser, the client will have to wait for 100 calls of Shares.find( postId). That's not a real DB join, or at least it's pushing the logic to the client when it should be on the server, and makes for ugly use of Session vars for every DB join in my app. Meteor recommends using observeChanges, I just can't get my code working, it's probably something simple. – Giant Elk Jan 12 '14 at 19:52
  • I have edited my answer to use observes and manage the join solely on the server. I don't think this is "recommended" anywhere by Meteor though it may have performance benefits in some instances such as when the published set is large and there are few clients. I am curious about the performance differences you observe from using the two different approaches in the wild. – user728291 Jan 13 '14 at 02:33
  • Thanks for your update, but there has to be a better way, that's a mess of code. Btw you have minor error in Shares.findOne - missing userId, and Shares.find - userId not in object notation. I'm still clueless why my example works on initialiaztion then stops && why the console.log statements get called twice in a row after initial publication, I.e. When adding record to Shares collection. – Giant Elk Jan 15 '14 at 04:43
  • Use pageination re: > I don't think this is "recommended" anywhere by Meteor though it may have performance benefits in some instances such as when the published set is large and there are few clients. – Giant Elk Jan 26 '14 at 19:18
  • Could you elaborate more on your latest comment? It is difficult to take much from a two word fragment followed by a quote. If you have a second question on how to do pagination I would follow [the link](http://stackoverflow.com/questions/20115722/pub-sub-criteria-issue) I gave in my first comment. Both methods I have shown will only do the join for whatever 'posts' are published. I am returning all 'posts' in the examples because that is what was specified in your question. – user728291 Jan 27 '14 at 00:01
  • I guess I misunderstood what you meant by not recommended by Meteor, I'm just saying pagination will prevent performance issues. Currently Meteor doesn't have an elegant solution for Joins, beyond really basic publish'es where you pass in the doc Id. There are people working on it, but I don't think it's as high priority for them as getting other core features solidified. That's why I'm trying to hack something together myself. – Giant Elk Jan 27 '14 at 03:29
0

myPosts is a cursor, so when you invoke forEach on it, it cycles through the results, adding the field that you want but ending up at the end of the results list. Thus, when you return myPosts, there's nothing left to cycle through, so fetch() would yield an empty array.

You should be able to correct this by just adding myPosts.cursor_pos = 0; before you return, thereby returning the cursor to the beginning of the results.

richsilv
  • 7,993
  • 1
  • 23
  • 29
  • I updated the question/code, so this answer doesn't apply, check the latest edit, see if you can figure this one out. Thanks. – Giant Elk Jan 11 '14 at 05:30