I currently have a template where I am querying the database with the following query.
allMessages = Messages.find({$or: [{type: "user_message"}, {type: "system_message", time: {$gt: (Date.now() - 180000)} }]}, {sort: {time: 1 }}).fetch()
Now obviously the template helper gets re-run whenever something new goes into or is removed from this set of data, which is exactly what I want. The issue arises when a system_message gets older than 2 minutes and I no longer want that message to by apart of my query. The data does not update when this happens, and only updates when a new message comes in or a for some reason a message is removed.
Does anyone know why this might be the case? It seems to me that there shouldn't be an issue as the data on the query is changing so it should be re-running but it isn't.
It isn't working because Date.now() isn't a reactive variable. If you were to set the date limit in something like a session variable or a ReactiveDict, it would cause your helper to recompute. Here's an example using the Session:
Template.myTemplate.allMessages = function() {
var oldestMessageDate = Session.get('oldestMessageDate');
var selector = {
$or: [
{type: "user_message"},
{type: "system_message", time: {$gt: oldestMessageDate}}
]
};
return Messages.find(selector, {sort: {time: 1}});
};
Template.myTemplate.created = function() {
this.intervalId = Meteor.setInterval(function() {
Session.set('oldestMessageDate', new Date - 120000);
}, 1000);
};
Template.myTemplate.destroyed = function() {
Meteor.clearInterval(this.intervalId);
};
Every second after the template is created, it changes oldestMessageDate to a new date which is two minutes in the past. Note that the intervalId is stored in the template instance and later cleaned up in the destroyed callback so it won't keep running after the template is no longer in use. Because oldestMessageDate is a reactive variable, it should cause your allMessages helper to continually rerun.
Related
I currently havea widget which grabs hundreds of documents from the DB via subscription and then keep listening for new documents, so it can update a stock chart.
There is a problem tough, which is every time the data is updated the chart is updated, which causes a redraw.
This is a problem cause it's calling redraw hundreds of time at the beginning even tough it just need to "fetch all data then draw and wait for updates", the updates will then happen not very often, so then it would be ok to redraw.
my current code:
Template.nwidget.onRendered(function() {
return this.autorun(function() {
var data;
data = {};
data = Data.find({
type: 'my_type'
});
data = data.fetch();
return update(data);
});
});
For doing some after data subscription you can do like this:
Meteor.subscribe( 'collection', {
onStop: function( error /* optional */ ) {
// when the sub terminates for any reason,
// with an error argument if an error triggered the stop
},
onReady: function() {
// when ready
}
});
If you want to render page after the data subcribe then you can add waitOn in your router.
There is one more way to check where subscription is ready or not. If subscription is not ready you can show something else like a loading screen.
var handle = Meteor.subscribe( 'collection');
Tracker.autorun(function() {
if (handle.ready())
//write whatever you want to do here.
});
For the auto update in your view you can store the date in a reactive thing its may reactive var, Session or collection.
Then you can return there values from helper to view. And that will auto update your view.
I am following this answer on stackoverflow. In console, I get the correct aggregated sum.
Meteor.publish('hoursTotal', function() {
var self = this;
var pipeline = [
{$group: {_id: "$userId", hours: {$sum: "$hours" }}}
];
var result = Reports.aggregate(pipeline);
_.each(result, function(result) {
self.added("hoursTotalSum", result._id, {
userId: result._id,
hours: result.hours
});
});
console.log(result);
self.ready();
});
I've subscribed to the new collection created in client:
Meteor.subscribe('hoursTotalSum');
My template helper:
Template.statsBrief.helpers({
hoursTotalSum: function() {
var currentUserId = Meteor.userId();
return Reports.find({userId: currentUserId});
},
});
My template:
{{#each hoursTotalSum}} {{hours}} {{/each}}
Meteor console retunrs this:
[ { _id: 'F8ZEWeKMoRfXaEGNa', hours: 30 },
I20151221-09:57:09.097(0)? { _id: 'ufALZHfhWyQ8pMkgD', hours: 85 } ]
Meteor templates return this:
50 20 15
Although the summation is happening right, and the backend console confirms that, I'm struggling to get the value into the template.
I successfully use aggregates via the meteorhacks:aggregate package however I do it a bit differently from your example. Rather than debug your code I will show how I would achieve the same goal.
Note :The code below is in coffeescript
Write a Meteor Method
Create a server side Meteor Method to perform the aggregation. You do not need to publish your aggregation as all the processing happens on the server and the results are under your control via the $project step.
Meteor.methods
getHoursTotal: ->
return null unless #userId
check #userId, String
pipeline = [
{$group:{'_id': #userId, 'hours':{$sum:'$hours'}}},
{$project:
_id:false,
userId:'$_id',
hours: '$hours',
}
]
return reports.aggregate pipeline
In your code you were processing each item in the result. It looked like you were just wanting rename _id to userId and include the calculated hours field. I did this in the $project step instead.
Call the Meteor Method
Your code depends on the userId being available, so we only want to run it after the user has signed in. We can make this reactive using tracker.
Tracker.autorun ->
return unless Meteor.userId()?
Meteor.call 'getHoursTotal', (err, res) ->
Session.set 'hoursTotal', res
Now the code will only tun if the Meteor.userId is not null and when it does run the hoursTotal will be stored in the Session ready for use.
Expose the Results
To expose the results nicely it is a good idea to put it on a Template helper.
Template.statsBrief.helpers
hoursTotal: -> Session.get 'hoursTotal'
I think performing the aggregate and exposing the results in this way is simpler whilst also benefiting from being fully reactive.
I'm trying to use a client side collection as a site configuration system. I insert documents representing my different pages, and the iron-router and navigation tabs all use them to determine what pages they are and what templates are represented by them. Each page uses a {{> contentTemplate}} inclusion helper to load it's relevant template.
It all works great, when the data has all loaded. When I restart the app on certain pages, the data hasn't loaded yet, and I receive the Exception from Deps recompute function: Error: Expected null or template in return value from inclusion function, found: undefined error.
Here's my javascript:
StoriesArray = [
{ category: 'teaching', contentTemplate: 'teachingHome', title: 'Teaching Home'},
...
];
Stories = new Meteor.Collection(null);
StoriesArray.forEach(function (story, index) {
story._id = index + '';
Stories.insert(story);
});
// in main.js
Template.teachingPost.contentTemplate = function() {
console.log(this);
console.log(this.contentTemplate);
return Template[this.contentTemplate];
};
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() { return Stories.findOne({contentTemplate: 'teachingHome', category: 'teaching'}); }
});
The console logs in the contentTemplate helper above log twice, the first time as this:
Object {} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
undefined main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
and the second time as this:
Object {category: "teaching", contentTemplate: "teachingHome", title: "Teaching Home"} main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:45
teachingHome main.js?1f560c50f23d9012c6b6dd54469bb32b99aa4285:46
so the router is simply trying to load this data too early.
I've tried putting the StoriesArray loading process into different files all over my app, including lib, and even tried putting it into Meteor.startup, but it's always the same result.
The normal iron-router waitOn/subscription pattern doesn't really apply here, since this is a client side collection built with null, that has no server representation. I don't want this to have server representation, because this is static content that there's no need to go to my server for.
How do I ensure this information is done before continuing?
Untested, but per Iron Router's docs on waitOn:
Returning a subscription handle, or anything with a ready method from the waitOn function will add the handle to a wait list.
Also in general it's better to use find with data, rather than findOne, as find will return an empty cursor when the collection is empty as opposed to findOne returning undefined. So try this:
// in router.js
this.route('teaching', {
layoutTemplate: 'teachingPost',
data: function() {
return Stories.find({contentTemplate: 'teachingHome', category: 'teaching'});
},
waitOn: function() {
var handle = {};
handle.ready = function() {
if (Stories.find().count() !== 0)
return true;
else
return false;
}
return handle;
}
});
And adjust your Template.teachingPost.contentTemplate function to work with a cursor rather than an object.
I have a publication whose scope depends on a element property from another collection. Basically it looks like this on the server:
Meteor.publish('kids', function (parent) {
return Kids.find({ _id: { $in: parent.childrenIds } });
}
In the example above parent.childrenIds is an array containing the _id's of all the kids that are children of the parent. This works fine until I want to add a new child to the parent:
newKidId = Kids.insert({ name: 'Joe' });
Parents.update({ _id: parentId }, { $push: { childrenIds: newKidId } });
This works on the server for the Kids collection (i.e., the new kid is added) and it updates the parent's childrenIds array with the newKidId. BUT it does not update the above 'kids' publication (the cursor is not updated/modified). As a result, the client's Kids collection is not updated (and it looks like the change to Kids is rolled back on the client).
When the client refreshes, all publications are stopped/restarted and the new kid (Joe) is finally published to the client.
Is there a way to avoid refreshing the client and forcing the re-publication of the Kids collection (ideally only sending the new kid Joe to the client)?
One of the things that is often misunderstood in Meteor is that there is no reactivity on the server. Dynamic descriptions need to be handled by Deps.autorun blocks on the client. To do this, first make sure you are not including the autopublish package by using this command in project directory:
$ meteor remove autopublish
Second, set up an autorun block on the client like:
Meteor.startup(function(){
Meteor.subscribe('parents');
Deps.autorun(function() {
parent = Parents.findOne();
if (!parent) return;
Meteor.subscribe('kids', parent);
});
});
This will tear down and set up subscriptions as the parent object changes.
You can see a full working example at https://gist.github.com/jagill/5473599 .
In meantime there were quite some packages made to deal with reactive publish functions. I am an author of meteor-related, and at the end of the package's README I compare my package with few other packages:
meteor-reactive-publish
meteor-publish-with-relations
meteor-smart-publish
reywood:publish-composite
copleyjk:simple-publish
These days you can simply use reactive-publish package (I am one of authors):
Meteor.publish('kids', function (parentId) {
this.autorun(function (computation) {
var parent = Parents.findOne(parentId, {fields: {childrenIds: 1}});
return Kids.find({_id: {$in: parent.childrenIds}});
});
}
It is important to limit Parents' query fields only to childrenIds so that autorun does not rerun for any other changes to Parents document.
I think you need to use observe in the publish function if a published query relies on a second query. Deps.autorun on the client is not necessary.
See discussions on Meteor server reactivity and Reactive updates when query is filtered by another query.
This is some code based on http://docs.meteor.com 'counts-by-room' example.
Meteor.publish( "kids", function(parent_id){
var self = this;
Parents.find({_id: parent_id}, { childrenIds: 1 }).observe({
added: function (document){
document.childrenIds.forEach( function(kidId){
self.added("kids", kidId, Kids.findOne( { _id: kidId}, {name: 1, _id: 1} ));
});
},
removed: function (oldDocument){
oldDocument.childrenIds.forEach( function(kidId){
self.removed("kids", kidId, Kids.findOne( { _id: kidId}, {name: 1, _id: 1} ));
});
},
changed: function (newDocument, oldDocument){
var oldLength = oldDocument.childrenIds.length;
var newLength = newDocument.childrenIds.length;
if (newLength > oldLength){
self.added("kids",
newDocument.childrenIds[newLength-1],
Kids.findOne( { _id: newDocument.childrenIds[newLength-1] }, {name:1, _id:1}) );
}
else{
self.removed("kids",
oldDocument.childrenIds[oldLength-1],
Kids.findOne( { _id: oldDocument.childrenIds[oldLength-1] }, {name:1, _id:1}) );
}
}
});
self.ready();
});
I am iterating over a moment-range daterange and trying to insert documents. I am getting the following error:
Exception while simulating the effect of invoking '/carpool_events/insert'
Error
Error: Sorting not supported on Javascript code
at Error (<anonymous>)
at Object.LocalCollection._f._cmp (http://localhost:3000/packages/minimongo/selector.js? 5b3e1c2b868ef8b73a51dbbe7d08529ed9fb9951:251:13)
at Object.LocalCollection._f._cmp (http://localhost:3000/packages/minimongo/selector.js? 5b3e1c2b868ef8b73a51dbbe7d08529ed9fb9951:226:36)
at LocalCollection._f._cmp (http://localhost:3000/packages/minimongo/selector.js?5b3e1c2b868ef8b73a51dbbe7d08529ed9fb9951:218:33)
at _func (eval at <anonymous> (http://localhost:3000/packages/minimongo/sort.js?08a501a50f0b2ebf1d24e2b7a7f8232b48af9057:63:8), <anonymous>:1:51)
at Function.LocalCollection._insertInSortedList (http://localhost:3000/packages/minimongo/minimongo.js?7f5131f0f3d86c8269a6e6db0e2467e28eff6422:616:9)
at Function.LocalCollection._insertInResults (http://localhost:3000/packages/minimongo/minimongo.js?7f5131f0f3d86c8269a6e6db0e2467e28eff6422:534:31)
at LocalCollection.insert (http://localhost:3000/packages/minimongo/minimongo.js?7f5131f0f3d86c8269a6e6db0e2467e28eff6422:362:25)
at m.(anonymous function) (http://localhost:3000/packages/mongo-livedata/collection.js?3ef9efcb8726ddf54f58384b2d8f226aaec8fd53:415:36)
at http://localhost:3000/packages/livedata/livedata_connection.js?77dd74d90c37b6e24c9c66fe688e9ca2c2bce679:569:25
This is my loop with the insert. I have tested the loop by just writing to console.log instead of inserting and the loop works fine
'click button.save-addEventDialogue': function(e, tmpl) {
var start = Session.get("showAddEventDialogue_dateRangeStart");
var end = Session.get("showAddEventDialogue_dateRangeEnd");
var dateRange = moment().range(moment(start),moment(end));
var dateLoopIncrement = moment().range(moment(start),moment(start).add('days',1));
console.log(dateRange);
console.log(dateLoopIncrement);
// Loop through the date range
dateRange.by(dateLoopIncrement, function(moment) {
// Do something with `moment`
var dateToSave = dateRange.start;
// Insert the record
Carpool_Events.insert({
owner: Meteor.user().profile.name,
owner_id: Meteor.userId(),
original_owner: Meteor.user().profile.name,
original_owner_id: Meteor.userId(),
declined: 0,
date: dateToSave.toDate()
});
dateToSave.add('days',1);
});
// Clear the Session
Session.set("showAddEventDialogue_dateRangeStart","");
Session.set("showAddEventDialogue_dateRangeEnd","");
// Close the dialogue
Session.set("showAddEventDialogue", false);
}
What is the right way to do this? Thanks.
The error message Sorting not supported on Javascript code is a result of inserting a JavaScript function (!) into a collection -- for example, by doing something like Carpool_Events.insert({x: function () { ... }}); JavaScript functions should not normally go into collections.
Somewhere in your code there is probably a typo where you are not calling a function (for example, writing Meteor.userId on the client instead of Meteor.userId().) My guess would be that in the process of making your code run on the server, you coincidentally fixed or avoided this.
I wasn't able to visually find the problem in your code -- if I'm wrong, to make more progress it would be helpful to have a reproduction.
It looks like the issue occurs when doing a bulk inserts (inserts in a loop) from the client. What I ended up doing was using a Meteor.methods to execute the insert on the server side. This seemed to workaround whatever the issue of doing this on the client is.
I also realized that I don't need to iterate over the dates using moment-range. Instead i just use moment to get the difference in days and iterate over that.
JS code in the client:
'click button.save-addEventDialogue': function (e, tmpl) {
var start = moment(Session.get("showAddEventDialogue_dateRangeStart"));
var end = moment(Session.get("showAddEventDialogue_dateRangeEnd"));
var days = end.diff(start, 'days');
var count = 0;
var dateToSave = moment(start);
// Loop through the date range
for (count; count <= days; count++) {
Meteor.call('bulkInsertCarpoolEvent', Meteor.user(), dateToSave.toDate());
dateToSave.add('days', 1);
};
// Clear the Session
Session.set("showAddEventDialogue_dateRangeStart", "");
Session.set("showAddEventDialogue_dateRangeEnd", "");
// Close the dialogue
Session.set("showAddEventDialogue", false);
}
On the server:
Meteor.startup(function () {
Meteor.methods({
bulkInsertCarpoolEvent: function (user, date) {
return Carpool_Events.insert({
owner: user.profile.name,
owner_id: this.userId,
original_owner: user.profile.name,
original_owner_id: this.userId,
declined: 0,
date: date
});
}
});
});