Iron router + observechanges = repeated observechanges handler calls? - meteor

I'm attempting to use observechanges with iron router but they don't seem to be compatible at all.
Router.route('/gaming', {
waitOn: function() {
return Meteor.subscribe('chat', function() {
window.chatmessagesCache = new ReactiveVar;
chatmessagesCache.set([]);
return chat.find().observeChanges({
added: function(id, doc) {
var tmpArr;
tmpArr = chatmessagesCache.get();
tmpArr.push(doc);
return chatmessagesCache.set(tmpArr);
}
});
});
}
If I leave the route and come back to it, observechanges begins being handled as many times as I've navigated away and returned, for each new record. What's the deal?
If I use subs manager it works as expected, but I don't understand why Meteor.subscribe inside waitOn is so cache/subscription unaware when it ALREADY gets called multiple times per load. Why!? I can't decipher what's causing this behavior at all.
Also, what I'm trying to accomplish is simple. I want to let chat messages that the user's client has received remain on the page even if the chat cursor is no longer publishing them (I'm publishing the last 10 chat messages)

Iron router has reactivity built in, which means when something inside your route function is invalidated, it will repeat the function as well as anything reactive with a Router.current(). These unexpected invalidation runs are a primary reason why folks made the exodus to flow router.
To solve this, you'll want to abstract your code away from the router. You can leave the sub, but I'd suggest you remove the sub's callback from the waitOn and move it into an onRendered callback. If you don't want the history loaded in chunks, you can first do a var collectionCount = chat.find({},{reactive:false}).count() on how many docs are in the collection & then in the added callback you can do something like if (++currentCount === collectionCount) /* add stuff */ to add al the records to the history when it reaches the last record.
On a bigger picture level, consider eliminating the observeChanges & just do an #each over the chat collection in spacebars to show your messages. Fewer cycles, cleaner code.

Iron router just has no management of observations you created yet it manages subscriptions itself, hence the multiple additions.
I figured this out by using a window level variable to check if I'm observing. Even in cases when the subscription is unhooked by iron, if I go back and never re-add the handler, the original observation hook still runs (!). ALSO, if you navigate away and drop the subscription, the handler is no longer called--which is the behavior I want in this scenario (This is all very insane behavior but at least it's now predictable to me )
This is caused by the fact that subscriptions != collections and the API for observations doesn't seem to expose any metadata, unfortunately, so I don't know how the iron router maintainers would account for this. Not to mention you return iron router a subscription, not a collection.
#Matt K if you were correct, this would always be an infinite loop (which admittedly I had a bunch of while trying to solve this) but the posted code is adding too many handlers, not looping indefinitely. Thanks for the post though.
This is what I settled on
Router.route('/gaming',
waitOn: ->
Meteor.subscribe('chat', ->
window.chatmessagesCache = new ReactiveVar(chat.find().fetch().reverse())
if !window.chatListening
window.chatListening = true
after = chat.find().count()
chat.find().observe(
added: _.after(after + 1,(doc) ->
tmpArr = chatmessagesCache.get()
tmpArr.push(doc)
chatmessagesCache.set(tmpArr))
changed : (id, doc) ->
))
I really just wanted to test out a pattern of locally "disconnected" documents. I still may use subs manager because it keeps subscriptions and their handlers alive without rerunning them constantly (which was rerunning the sub handler, which was adding multiple observations)

Related

How can I use collection.find as a result of a meteor method?

I'm trying to follow the "Use the return value of a Meteor method in a template helper" pattern outlined here, except with collections.
Essentially, I've got something like this going:
(server side)
Meteor.methods({
queryTest: function(selector) {
console.log("In server meteor method...");
return MyCollection.find(selector);
}
});
(client side)
Meteor.call('queryTest', {}, function(error, results) {
console.log("in queryTest client callback...");
queryResult = [];
results.forEach(function(result) {
// massage it into something more useful for display
// and append it to queryResult...
});
Session.set("query-result", queryResult);
});
Template.query_test_template.helpers({
query_test_result: function() {
return Session.get("query-result");
}
});
The problem is, my callback (from Meteor.call) doesn't even get invoked.
If I replace the Method with just 'return "foo"' then the callback does get called. Also, if I add a ".fetch()" to the find, it also displays fine (but is no longer reactive, which breaks everything else).
What gives? Why is the callback not being invoked? I feel like I'm really close and just need the right incantation...
If it at all matters: I was doing all the queries on the client side just fine, but want to experiment with the likes of _ensureIndex and do full text searches, which from what I can tell, are basically only available through server-side method calls (and not in mini-mongo on the client).
EDIT
Ok, so I migrated things publish/subscribe, and overall they're working, but when I try to make it so a session value is the selector, it's not working right. Might be a matter of where I put the "subscribe".
So, I have a publish that takes a parameter "selector" (the intent is to pass in mongo selectors).
On the client, I have subscribe like:
Meteor.subscribe('my-collection-query', Session.get("my-collection-query-filter"));
But it has spotty behaviour. On one article, it recommended putting these on Templates.body.onCreate. That works, but doesn't result in something reactive (i.e. when I change that session value on the console, it doesn't change the displayed value).
So, if I follow the advice on another article, it puts the subscribe right in the relevant helper function of the template that calls on that collection. That works great, but if I have MULTIPLE templates calling into that collection, I have to add the subscribe to every single one of them for it to work.
Neither of these seems like the right thing. I think of "subscribing" as "laying down the pipes and just leaving them there to work", but that may be wrong.
I'll keep reading into the docs. Maybe somewhere, the scope of a subscription is properly explained.
You need to publish your data and subscribe to it in your client.
If you did not remove "autopublish" yet, all what you have will automatically be published. So when you query a collection on client (in a helper method for example), you would get results. This package is useful just for quick development and prototyping, but in a real application it should be removed. You should publish your data according to your app's needs and use cases. (Not all users have to see all data in all use cases)

In Meteor, is there a way to detect when data is fully loaded to the browser?

This problem probably has been asked before, but I cannot seem to figure out an easy way to do it.
I have a Meteor page that shows messages posted by users in chronological order (newest message at the bottom). I want the page to:
#1) Subscribe to the messages (using publish/subscribe) based on a parameter supplied in the URL
#2) Render the page
#3) Display the messages on the page
#4) Scroll to the bottom.
There's no apparent way to know when 1, 2, and 3 are complete before initiating the scroll to bottom. Meteor does have an observe/added function to do an event when new messages are added to a subscription, however that's only when documents are insertted into Mongo, and it does not trigger when displaying results to the initial subscription.
I'll show code for the steps above:
#1: Subscribe to the messages using publish/subscribe: in /client/messages.js
Template.messages.created = function() {
this.autorun( function() {
this.subscription = Meteor.subscribe("messages", Router.current().params.category);
}.bind(this));
};
#2 Render the page, in /client/messages.html
<template name="messages">
{{#each messages}}
{{messageText}}<br><br>
{{/each}}
</template>
#3: Display the mssages on the page: /client/messages.js
Template.messages.helpers({
messages: function() {
var category = Router.current().params.category;
return Messages.find({category: category}, { sort: { messageDate: 1 } });
},
});
All this works, but does not automatically scroll to the bottom.
I cannot add a jquery scroll command to the Meteor onRendered section because it runs BEFORE the data is written to the DOM. I can do a Meteor.setTimeout to wait 1 second before scrolling to the bottom, but does not work if it takes longer than a second to fill the DOM with subscribed data.
Here's another thing complicating the matter. I am supplying the category variable in the URL. When the client selects another category, using Meteor/Blaze pathFor,
{{pathFor 'messages' channelid='new'}}
the browser does not reload/rerender the page, it simply updates the URL parameter which triggers the autorun to change what messages it has subscribed to. It simply writes the new data to the DOM. Because of this, I cannot to a jquery $(document).ready to detect whether the page is ready (because the page is always ready), and I cannot use some fancy handlebars thing like {{scrollToBottom}} at the end of my {{#each}} in messages.html because that it not re-run.
So, is there a simple way to detect when Meteor/Blaze completely finishes writing new data to the browser?
If I understand correctly, you really just want to know when all of your data is published from the server to the client (so you can then do something, in this case, scroll to the bottom).
All calls to Meteor.subscribe() return a subscription handle. This has a ready() method, that tells you all the data has been sent. This includes those done at a template level (which you might want to consider if appropriate for your use-case). More information here:
http://docs.meteor.com/#/full/Blaze-TemplateInstance-subscribe
There are several ways to use this - you need to decide what is appropriate for you. As ready() is reactive, you can just use this. However, you might find it easier to implement an onReady callback within your subscription (see documentation above).
I suspect you will need to implement an autorun within your template rendered. This is to avoid the reverse scenario where the data arrives before the rendering (and so the callback fires too early). Something like this should do the trick (noting you have already set your subscription handle in your template creation function to this.subscription):
Template.messages.onRendered(function() {
var self = this;
this.autorun(function(computation) {
if(self.subscription.ready()){
$(document).scrollTop($(document).height());
computation.stop()
}
});
});
This should ensure both that the function is only called after rendering and the data from the subscription is complete. The autorun is also stopped after executing the scroll to stop continued calling should new documents cause ready() to no longer be truthy (and then become ready again) - which might surprise people with the page re-scrolling to the bottom!

Efficient pattern for updating a subscription limit argument

I'm using subs-manager, but the answer to this may be independent of that lib.
I have a subscription with a single limit argument. Currently, when I call subs.subscribe 'subname', newLimit, another subscription is added.
The old subscriptions are still there. I don't want Meteor to spend time maintaining the old, lower-limit subscriptions. Instead of adding a new subscription, I want to update the argument of the old subscription. What is the best way to do this?
Note that I also don't want to completely tear down eg 'subname', 20 before subscribing to 'subname', 40, because I don't want Meteor to do the extra work of resending the first 20 docs – I want it to just send docs 21 - 40.
You could have a look at your subscription's stop() method. According to the docs:
stop() [cancels] the subscription. This will typically result in the server directing the client to remove the subscription's data from the client's cache.
So the way I see it, you could maybe manage to do:
// globals or whatever
var previousSub, newSub;
// somewhere else
newSub = Meteor.subscribe('subname', newLimit, function () {
if (previousSub)
previousSub.stop();
previousSub = newSub;
});

Meteor - how can I empty out the collection of 10,000 objects I subscribed to after I no longer need it?

I have a template in which a user should be able to click on a button to bring up a modal and in the modal choose a handful of items out of a list of about 10,000 items which are displayed there to search or scroll through.
Since this collection is so big, I don't want to keep it around in memory when I don't absolutely need it.
So I would like to subscribe to this collection only when the modal is being viewed and I would like to ensure that I am unsubscribed if the modal is not being viewed.
Is there a way to explicitly unsubscribe from a collection?
There are a couple of ways you can do this:
Use the subscription handle
subscribe returns a handle you can call stop on. For example:
var handle = Meteor.subscribe('stuff');
handle.stop();
Use an autorun
Because an autorun will automatically start and stop subscriptions when its reactive dependencies change, this will work:
Tracker.autorun(function () {
if (Session.get('showingModal'))
Meteor.subscribe('stuff');
});
Side note - it may make more sense to use a method call for searching such a large data set rather than publishing the entire thing to the client. For example you can set a session variable whenever the user's query changes, then use an autorun to update the result set based on the method's return value.
https://docs.meteor.com/#/full/meteor_subscribe
Quoting the docs :
Meteor.subscribe returns a subscription handle, which is an object
with the following methods:
stop() Cancel the subscription. This will typically result in the
server directing the client to remove the subscription's data from the
client's cache.
So basically what you need to do is storing the subscription handle in a variable and call the stop method when you don't need those published documents anymore.
Note that if you're using iron:router (and you probably should), this is taken care of automatically for you on each route change, which is convenient but has the side effect of provoking a lot of sometimes unnecessary calls to Meteor.publish calls which are non trivial for the server and bandwidth... to address this matter you can use meteorhacks:subs-manager but it's another topic anyway.

Is there any way to know when an meteor subscription is 'valid'?

If I change a Session var and trigger a re-subscription via autosubscribe, is there any callback mechanism to wait until the 'latest' data is down from the server? [1]
If you take a look at this gist you'll see some code that logs the content of a collection over time as the subscription changes. A relevant section of the output:
at Subscribed; comments are: first post on #1 - second post on #1
at Flushed; comments are: first post on #1 - second post on #1
at Subscription complete; comments are: first post on #1 - second post on #1 - first post on #2 - second post on #2
So, even after (a) calling .subscribe, (b) calling Meteor.flush (c) being inside the onReady callback for the .subscribe; there is still stale data in the collection, and only in the 3rd case is the 'correct' data in there.
I realise that reactive templates and .observe will eventually receive the correct data and things will 'settle' into the correct state. But is there some way we can tell that we aren't there yet?
For instance, most of the meteor example apps (and my own apps) are prone to jerking around a bit (similar to a FOUC) while data is added + removed from a subscribed collection. If we could tell that a subscription was 'loading' we could do something about this.
[1] Obviously the data on the server is constantly changing, but as you'll see in the gist, I can't (without a timeout) find a point where it's even correct. Thus my use of 'valid' in the question.
A very simple and common use case
Take the madewith app; when you first load it up it appears that there are no apps registered, until after the data comes down the wire and the apps suddenly appear.
The reason for this is that Meteor.subscribe has been called, but the data has not yet come down the wire. But there's no easy way for the template to tell that the data is pending and that it should show a 'loading' template. In madewith they actually do something when the data has loaded, but this is a callback and thus breaks out of the normal meteor way of doing things (i.e. reactive coding).
It would be much nicer (IMO) to be able to write something like:
{{unless apps_loaded}}{{> loading}}{{/unless}}
and
Template.madewith.apps_loaded = function() { return !Apps.isComplete(); }
Interesting question.
The right place to be notified for new subscriptions is the onReady callback. Notice the logging that happens there always includes your new data. Checking at (a) and (b) aren't useful, because there's latency between calling subscribe and when all the data arrives from the server.
The underlying problem is there's no equivalent onRemove callback that runs once the data for a just-stopped subscription has been removed. Moreover, autosubscribe deliberately starts new subs before stopping old ones, to avoid flicker.
What's the real use case? Most of the time such a callback isn't necessary, because templates can also restrict their queries to the data that should be in scope. In your example, the template helper that renders comments might query only for comments with the current post_id in the Session, so there's no harm having extra comments in the database.
Something like this:
Template.post.comments = function () {
return Comments.find({post_id: Session.get('post_id')});
};
That permits strategies more sophisticated that the generic autosubscribe function, like subscribing to comments for the last three posts the user has viewed. Or, subscribing to the first few comments for each post, and then separately subscribing to a post's full set of comments only when that post is selected.
I've come a bit late to the meteor party, so I don't know the history behind when meteor features have been added, but just for completeness it is now possible to do this using template-level subscriptions and 'subscriptionsReady' :
[js]
Template.myTemplate.onCreated(function() {
this.subscribe('myData');
});
[html]
<template name="myTemplate">
<h2>my Template</h2>
{{#if Template.subscriptionsReady}}
// code to loop/display myData
{{else}}
<p>Please wait..</p>
{{/if}}
</template>

Resources