Modeling and Publishing Follow-up Based Material with Meteor
I am working on a simple application where a User can follow other users. Users can post messages. And a custom feed consists of posts that have been tagged by users that they follow. Pretty simple. However, this all gets complicated in Mongo and Meteor ...
There are basically two ways to model this that I can think of:
-
User has a property
following
, which is an array of userIds that follows the user. In addition, the post has a propertystarrers
that is an array of userIds that this post took. The good thing about this method is that posting is relatively straightforward:Meteor.publish 'feed', (limit) -> Posts.find({starrers: {$in: Meteor.users.findOne(@userId).following}}, {sort: {date: -1}, limit:limit})
We don’t react to who is following the user, but at the moment it’s not that bad. The main problem with this approach is that (1) individual documents will become large and ineffective if 100,000 people become public. Another problem is that (2) it would be painful to keep track of information, such as when a user started following another user or when a user took a post.
-
Another way to do this is to have two more collections
Stars
andFollows
. If a user posts a message, we create a document with propertiesuserId
andpostId
. If a user follows another user, we create a document with propertiesuserId
andfollowId
. This gives us the advantage of smaller document sizes forUsers
andPosts
, but tricky things when it comes to queries, especially since Mongo doesn't handle connections!
Now, I've done some research, and people seem to agree that the second choice is the right way. Now the problem I'm running into is efficient query and post. Based on Open the Meteor chapter on Advanced Publishing , I created a post that posts that are flagged by Follower users - sorted and restricted.
# a helper to handle stopping observeChanges
observer = (sub, func) ->
handle = null
sub.onStop ->
handle?.stop?()
() ->
handle?.stop?()
handle = func()
Meteor.publish 'feed', (limit) ->
sub = this
userId = @userId
followIds = null
eventIds = null
publishFollows = observer sub, () ->
followIds = {}
Follows.find({userId:userId}).observeChanges
added: (id, doc) ->
followIds[id] = doc.followId
sub.added('follows', id, doc)
publishStars()
removed: (id) ->
delete followIds[id]
sub.removed('follows', id)
publishStars()
publishStars = observer sub, () ->
eventIds = {}
Stars.find({userId: {$in: _.keys(followIds)}).observeChanges
added: (id, doc) ->
eventIds[id] = null
sub.added('stars', id, doc)
publishEvents()
removed: (id) ->
delete eventIds[id]
sub.removed('stars', id)
publishEvents()
publishEvents = observer sub, () ->
Events.find({_id: {$in: _.keys(eventIds)}}, {sort: {name:1, date:-1}, limit:limit}).observeChanges
added: (id, doc) ->
sub.added('events', id, doc)
changed: (id, fields) ->
sub.changed('events', id, fields)
removed: (id) ->
sub.removed('events', id)
While it works, it appears to be very limited in scope. In particular, we have to make a list of all featured posts by each follower. The size of this list will grow very quickly. Then we make a huge $in
request for all posts.
Another annoyance is the feed request on the client after signing:
Meteor.subscribe("feed", 20)
posts = null
Tracker.autorun ->
followers = _.pluck(Follows.find({userId: Meteor.userId()}).fetch(), "followId")
starredPostIds = _.pluck(Stars.find({userId: {$in: followers}}).fetch(), "postId")
posts = Posts.find({_id: {$in: starredPostIds}}, {sort: {date: -1}, limit: 20}).fetch()
It's like we're doing this job twice. First, we do all the work on the server to publish the feed. Then we need to repeat the same logic again on the client to get these messages ...
My question here is a design question over everything. How can I efficiently design this feed based on persistent subscriber records? What collection / collection schemes should be used? How do I create a related post? How can I request a feed on the client?
source to share
meteor add reywood:publish-composite
Meteor.publishComposite('tweets', function(username) {
return {
find: function() {
// Find the current user following users
return Relationships.find({ follower: username });
},
children: [{
find: function(relationship) {
// Find tweets from followed users
return Tweets.find({user: relationship.following});
}
}]
}
});
Meteor.publish('ownTweets', function(username) {
return Tweets.find({user: username});
});
source to share