Meteor: Tracker.autorun and dep.changed causing infinite loop - meteor

I am using a new Tracker.Dependency for tracking several things, but it causes the autorun in the code below to run infinitely. What is wrong? The code below is okay once I separate the getSong and getSongId to depend on dep and dep2, instead of just dep.
SongManager = {
dep: new Tracker.Dependency,
dep2: new Tracker.Dependency,
init: function(songId) {
var self = this;
this.setSongId(songId);
Meteor.subscribe('song', songId);
Tracker.autorun(function(){
var songs = self.getSongCursor().fetch();
if (songs.length > 0) {
self.song = songs[0];
self.dep.changed();
}
})
},
getSongCursor: function(){
return Songs.find({_id: this.getSongId()});
},
getSong: function(){
this.dep.depend();
return this.song;
},
getSongId: function(){
this.dep2.depend();
return this.songId;
},
setSongId: function(arg){
this.songId = arg;
this.dep2.changed();
},
};

The problem is that you're creating a circular dependency. I would recommend using ReactiveVar for this rather than working with the lower-level dependency API.
meteor add reactive-var
Then you can just do this:
SongManager = {
song: new ReactiveVar(),
songId: new ReactiveVar(),
init: function(songId) {
this.songId.set(songId);
this.computation = Tracker.autorun(_.bind(this.update, this));
},
update: function() {
var songId = this.songId.get();
Meteor.subscribe('song', songId);
this.song.set(Songs.findOne(songId));
},
stop: function() {
this.computation.stop();
}
};
SongManager.init(oldSongId);
SongManager.songId.set(newSongId);
// After enough time has passed for the subscription to update and tracker to flush:
var currentSong = SongManager.song.get();
console.log(currentSong._id === newSongId); // true
I also added a way for you to stop your autorun computation so it doesn't keep running in the background when it's no longer necessary. Note that since the subscription is run within an autorun, it will automatically be stopped and restarted when the songId changes. The update function will actually be run twice, but Meteor knows not to send two identical subscription requests.

Related

METEOR - Show loader and run methods at specific time

I wanna build a loader circle what goes from 1 to 100% and in the meantime to run some methods.
loader circle
The scenario is:
load the page and start counting.
When the counter is at 50% pause counting and run the first method and when I have the result to start counting from where I was left.
count until 90% and run the second method.
I was trying something with Meteor.setInterval on onCreated but I'm not sure if it's the right method to do this.
Can someone give me some ideas about how to approach this?
Thanks!
There are several ways you can do this depending on your specific needs and you might even want to use one of the many Reactive Timer packages that are out there.
Here is one working example that only uses the Meteor API (no packages). Note, I did not actually incorporate the loader circle animation since it wasn't specifically part of the question.
Template definition
<template name="loader">
<h1>Loading...({{loadingPercentage}})</h1>
</template>
Template logic
Template.loader.onCreated(function() {
// used to update loader circle and triggering method invocation
this.elapsedTime = new ReactiveVar(0);
// handle for the timer
var timerHandle;
// starts a timer that runs every second that can be paused or stopped later
const startTimer = () => {
timerHandle = Meteor.setInterval(() => {
this.elapsedTime.set(this.elapsedTime.get() + 1);
}, 1000);
};
// pauses/stops the timer
const pauseTimer = () => {
Meteor.clearInterval(timerHandle);
};
// let's first start our timer
startTimer();
// runs every second and triggers methods as needed
this.autorun(() => {
const elapsedTime = this.elapsedTime.get();
// call methods based upon how much time has elapsed
if (elapsedTime === 5) {
pauseTimer();
// call method and restart the timer once it completes
Meteor.call('methodOne', function(error, result) {
// do stuff with the result
startTimer();
});
} else if (elapsedTime === 9) {
pauseTimer();
// call method and restart the timer once it completes
Meteor.call('methodOne', function(error, result) {
// do stuff with the result
// DO NOT RESTART TIMER!
});
}
});
});
Template.loader.helpers({
// helper used to show elapsed time on the page
loadingPercentage: function() {
return Template.instance().elapsedTime.get();
},
});
Let me know if you have any questions.
This is what i was trying to do:
Template.onboarding.onCreated(function(){
var instance = this;
instance.progress = new ReactiveVar(0);
instance.autorun(function(){
var loader = {
maxPercent: 100,
minPercent: instance.progress.get(),
start: function(){
var self = this;
this.interval = Meteor.setInterval(function(){
self.minPercent += 1;
if(self.minPercent >= self.maxPercent) {
loader.pause();
}
if( self.minPercent == 25) {
loader.pause();
Meteor.call('runMethodOne', (err,res)=>{
if (!err) loader.resume();
});
}
if(self.minPercent == 75) {
loader.pause();
Meteor.call('runMethodTwo', (err,res) =>{
if(res) loader.resume();
});
}
}
});
}
instance.progress.set(self.minPercent)
},50);
},
pause: function(){
Meteor.clearInterval(this.interval);
delete this.interval;
},
resume: function(){
if(!this.interval) this.start();
}
};
loader.start();
}
});
});

Why is data set with Meteor Iron Router not available in the template rendered callback?

This is a bit puzzling to me. I set data in the router (which I'm using very simply intentionally at this stage of my project), as follows :
Router.route('/groups/:_id',function() {
this.render('groupPage', {
data : function() {
return Groups.findOne({_id : this.params._id});
}
}, { sort : {time: -1} } );
});
The data you would expect, is now available in the template helpers, but if I have a look at 'this' in the rendered function its null
Template.groupPage.rendered = function() {
console.log(this);
};
I'd love to understand why (presuming its an expected result), or If its something I'm doing / not doing that causes this?
From my experience, this isn't uncommon. Below is how I handle it in my routes.
From what I understand, the template gets rendered client-side while the client is subscribing, so the null is actually what data is available.
Once the client recieves data from the subscription (server), it is added to the collection which causes the template to re-render.
Below is the pattern I use for routes. Notice the if(!this.ready()) return;
which handles the no data situation.
Router.route('landing', {
path: '/b/:b/:brandId/:template',
onAfterAction: function() {
if (this.title) document.title = this.title;
},
data: function() {
if(!this.ready()) return;
var brand = Brands.findOne(this.params.brandId);
if (!brand) return false;
this.title = brand.title;
return brand;
},
waitOn: function() {
return [
Meteor.subscribe('landingPageByBrandId', this.params.brandId),
Meteor.subscribe('myProfile'), // For verification
];
},
});
Issue
I was experiencing this myself today. I believe that there is a race condition between the Template.rendered callback and the iron router data function. I have since raised a question as an IronRouter issue on github to deal with the core issue.
In the meantime, workarounds:
Option 1: Wrap your code in a window.setTimeout()
Template.groupPage.rendered = function() {
var data_context = this.data;
window.setTimeout(function() {
console.log(data_context);
}, 100);
};
Option 2: Wrap your code in a this.autorun()
Template.groupPage.rendered = function() {
var data_context = this.data;
this.autorun(function() {
console.log(data_context);
});
};
Note: in this option, the function will run every time that the template's data context changes! The autorun will be destroyed along with the template though, unlike Tracker.autorun calls.

Meteor collection lookup on router level

I'm building an app that has versions of pages. I'd like users to be able to visit the page without having to specify the version they wish to start working with. In case no version is specified in the URL, I'd like to look up the latest version of that page and redirect to it.
The problem I'm having is that I can't do any collection lookups on the router level to figure out what version I'm looking for because the collections simply aren't there.
Here's my code so far:
Router.route('page', {
path: '/page/:slug/:version?',
onBeforeAction: function() {
var versionId = this.params.version;
if (!versionId) {
console.log('no version! oh noes!');
var p = Pages.find({slug: this.params.slug});
var newestVersion = Versions.findOne({page: p._id}, {sort: {createdAt: -1}});
versionId = newestVersion._id;
Router.redirect('page', this.params.slug, versionId);
}
this.next();
},
waitOn: function() {
return Meteor.subscribe('page', this.params.slug, this.params.state);
},
data: function() {
return {
page: Pages.findOne({slug: this.params.slug}),
version: Versions.findOne(this.params.version)
}
}
});
Any help or insights are very much appreciated!
Here's the solution I found. The problem is that the onBeforeAction didn't actually wait for the waitOn unless you tell it to.
onBeforeAction: function() {
if (this.ready()) {
if (!this.params.version) {
var p = Pages.findOne({slug: this.params.slug}),
sortedVersions = _.sortBy(Versions.find({page: p._id}).fetch(), function(s) {
return v.createdAt;
});
var newest_version_id = sortedVersions[0]._id;
this.redirect('page', {slug: this.params.slug, state: newest_version_id});
}
this.next();
}
}
I think the key is in the this.ready() check which is only true when all subscriptions in the waitOn are true. I assumed all this happened automagically by simply using the waitOn.

Meteor: reactive helper using Tracker.Dependency

I have a helper
Template.home.helpers({
songId: function(){
return SongManager.getSongId();
},
});
that uses the getSongId method from the SongManager
SongManager = {
init: function(songId){
this.dep = new Tracker.Dependency;
},
getSongId: function(){
this.dep.depend();
return this.songId;
},
setSongId: function(arg){
this.songId = arg;
this.dep.changed();
},
}
But it doesn't cause the template to update reactively when setSongId is called. Am I doing anything wrong? If I use Session.get and Session.set, then everything works properly.
SongManager = {
init: function(songId){
},
getSongId: function(){
return Session.get('songId');
},
setSongId: function(arg){
Session.set('songId', arg);
},
}
I solved it but I'm quite surprised but the results: Placing inside Meteor.startup() doesn't work. I need to investigate why, there's is something about Blaze that I don't know yet.
Check the repo I created for this: https://github.com/mariorodriguespt/stack-song-manager

How do I make a collection reactive based on the contents of another?

In short, I want to do:
Meteor.publish('items', function(){
return Item.find({categoryId: Categories.find({active: true} });
});
The flag 'active' as part of 'Categories' changes regularly.
I also tried unsub/resub to the Items collection by leveraging reactivity on the Categories collections, and it works, unfortunately it re-triggers on ANY modification to the Categories collection, regardless if it affected the 'active' flag or not.
What are my options?
Nothing solved the issue of the items not being 'deleted' locally when the category is flagged as inactive on the server. Solution (ish) is to:
Client:
Categories.find({active: true}).observeChanges({
added: function(){
itemsHandle && itemsHandle.stop();
itemsHandle = Meteor.subscribe("items");
}
});
Server:
Meteor.publish('items', function(){
var category = Categories.findOne({active: true});
return category && Items.find({categoryId: Categories.findOne({active: true}._id);
});
I realize this isn't perfect (still uses client side code), but it works and its the cleanest I could think of. I hope it helps someone!
A possible solution is to create a dependency object, watch for all categories change, and trigger the dep change if the active flag was toggled. Something along these lines:
var activeCount = Categories.find({active: true}).count();
var activeDep = new Deps.Dependency();
Deps.autorun(function() {
var activeCountNow = Categories.find({active: true}).count();
if(activeCountNow !== activeCount) {
activeCount = activeCountNow;
activeDep.changed();
}
});
Meteor.publish('items', function(){
activeDep.depend();
return Item.find({categoryId: Categories.find({active: true} });
});
Note: I'm only verifying whether the number of active categories have changes so that I don't have to keep the active list in the memory. This may or may not be appropriate depending on how your app works.
Edit: Two-sided flavor mentioned in the comments:
Client:
var activeCount = Categories.find({active: true}).count();
var activeDep = new Deps.Dependency();
Deps.autorun(function() {
var activeCountNow = Categories.find({active: true}).count();
if(activeCountNow !== activeCount) {
activeCount = activeCountNow;
activeDep.changed();
}
});
Deps.autorun(function(){
activeDep.depend();
Meteor.subscribe('items', new Date().getTime());
});
Server:
Meteor.publish('items', function(timestamp) {
var t = timestamp;
return Item.find({categoryId: Categories.find({active: true} });
});
Meteor.startup(function() {
Categories.find().observe({
addedAt: function(doc) {
trigger();
},
changedAt: function(doc, oldDoc) {
if(doc.active != oldDoc.active) {
trigger();
}
},
removedAt: function(oldDoc) {
trigger();
}
});
});
Now, the trigger function should cause the publish to rerun. This time it's easy when it's on the client (change subscription param). I'm not sure how to do this on the server - perhaps run publish again.
I use the following publish to solve a similar issue. I think it is only the one line nesting of queries that limits the reactivity. Breaking one query out inside the publish function seems to avoid the issue.
//on server
Meteor.publish( "articles", function(){
var self= this;
var subscriptions = [];
var observer = Feeds.find({ subscribers: self.userId }, {_id: 1}).observeChanges({
added: function (id){
subscriptions.push(id);
},
removed: function (id){
subscriptions.splice( subscriptions.indexOf(id)) , 1);
}
});
self.onStop( function() {
observer.stop();
});
var visibleFields = {_id: 1, title: 1, source: 1, date: 1, summary: 1, link: 1};
return Articles.find({ feed_id: {$in: subscriptions} }, { sort: {date: -1}, limit: articlePubLimit, fields: visibleFields } );
});
//on client anywhere
Meteor.subscribe( "articles" );
Here is another SO example which gets the search criteria from the client through subscribe if you decide that is acceptable.
Update: Since the OP struggled to get this going I made a gist and launched a working version on meteor.com. If you just need the publish function it is as above.

Resources