Tracker.autorun(function() {
DATA.find().observeChanges({
added: function(id, doc) {
console.log(doc);
}
});
});
This code is being called on the server. Every time the meteor server starts, the added function fires for every single item in the database. Is there a way to have the added callback fire only when new items are added?
added will be called for every document in the result set when observeChanges is first run. The trick is to ignore the callback during this initialization period. I have an expanded example in my answer to this question, but this code should work for you:
(function() {
var initializing = true;
DATA.find().observeChanges({
added: function(id, doc) {
if (!initializing) {
console.log(doc);
}
}
});
initializing = false;
})();
Note that Tracker.autorun is a client-only function. On the server I think it only ever executes once.
I struggled with this for a long time. For some reason, David's answer did not work for me - it was firing after the initializing variable was set to false.
This pattern from Avi was successful for me:
var usersLoaded = false;
Meteor.subscribe("profiles", function () {
// at this point all new users sent down are legitimately new ones
usersLoaded = true;
});
Meteor.users.find().observe({
added: function(user) {
if (usersLoaded) {
console.log("New user created: ", user);
}
}
});
Since it is initialization issue, you can do this.
var observerOfMessages = Messages.find({}).observe({
added: function(doc){
if(!observerOfMessages) return;
console.log(doc)
}
});
This is more elegant actually.
Provide a selector for the query which does not match old items. If using mongo ObjectID as _id you could query for items that have _id greater than the latest item's:
const latest = DATA.findOne({}, {sort: {_id: -1}})
DATA.find({_id: {$gt: latest._id}}).observeChanges({
added: function() { ... }
})
Or with createdAt timestamp:
const currentTime = new Date()
DATA.find({createdAt: {$gt: currentTime}}).observeChanges({
added: function() { ... }
})
Here's another way to solve this:
Meteor.subscribe('messages', function() {
var messages = Messages.find();
var msgCount = messages.count();
messages.observe({
addedAt: function(doc, atIndex) {
if(atIndex > (msgCount - 1)) console.log('added');
}
});
});
Should only fire for docs added after the existing amount is delivered. It's important that this goes in an onReady callback for Meteor.subscribe so that the msgCount changes as your subscription does... if for example, you're paginating your subscriptions.
cursor.observe() documentation
Related
In a example about pagination, different number is passed to the route in terms of how many results should be displayed.
// lib/router.js
Router.route('/:postsLimit?', {
name: 'postsList',
waitOn: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return Meteor.subscribe('posts', {sort: {submitted: -1}, limit: limit});
},
data: function() {
var limit = parseInt(this.params.postsLimit) || 5;
return {
posts: Posts.find({}, {sort: {submitted: -1}, limit: limit})
};
}
});
I am wondering if it's necessary to still give the Posts.find() arguments in the data attribute. It seems kinda redundant here because the subscribe in waitOn already limit the dataset that i am getting back from the server. I know that data is used to provide data context for different parts of my templates. However, providing the same arguments here to data Posts.find() just seems redundant. I have tried just using Posts.find() without the argument and it worked.
Also, I am wondering what is a way to access all the data in router? I am accessing the Posts collections outside the individual route and I thought that I would be able to access all the data in Posts collections. However, it returned with a count 0.
// lib/router.js
var totalPostsCount = Posts.find().count()
console.log('total count?')
console.log(totalPostsCount) // output 0
PostsListController = RouteController.extend({
template: 'postsList',
increment: 5,
postsLimit: function() {
return parseInt(this.params.postsLimit) || this.increment;
},
findOptions: function() {
return {sort: {submitted: -1}, limit: this.postsLimit()};
},
subscriptions: function() {
this.postsSub = Meteor.subscribe('posts', this.findOptions());
},
posts: function() {
// console.log("is find limited to subscriptions too??");
// console.log(Posts.find().count())
return Posts.find({}, this.findOptions());
},
data: function() {
// console.log('is data limited to this?')
// console.log(Posts.find().count())
var hasMore = this.posts().count() === this.postsLimit();
var adjustOrNot = this.posts()
if (!hasMore){
var nextPath = this.route.path({postsLimit: this.posts().count()});
}else{
var nextPath = this.route.path({postsLimit: this.postsLimit() + this.increment});
}
return {
posts: this.posts(),
ready:this.postsSub.ready(),
nextPath: hasMore ? nextPath : null
};
}
});
//...
Router.route('/:postsLimit?', {
name: 'postsList'
});
Edit 1: code for getting all posts:
// server
Meteor.publish('allPosts',function(){
return Posts.find({});
})
// lib/router.js
if (Meteor.isClient){
var handle = Meteor.subscribe('allPosts');
if (handle.ready()) {
// This logs the actual count of posts
console.log(Posts.find().count());
} else {
console.log('not ready yet');
}
}
This only outputs 'not ready yet' in the console and it's not changing even when the page finish loading.
Thanks a lot for the help.
Edit 2: possible solutions
I tried wrapping the reactive source ready() inside a computation such as Tracker.autorun() and now it worked.
if (Meteor.isClient){
var handle = Meteor.subscribe('allPosts');
Tracker.autorun(function(){
var status = handle.ready();
if (status){
console.log(Posts.find().count())
}else{
console.log('not ready yet')
}
})
}
Yes, you should pass limit and other query options to the front-end query too, regardless of your subscription. This is because if you were to have multiple subscriptions to the same collection active at the same time (as is often done in larger apps) the subscribed records end up all in the same Minimongo collection in the front-end. In this case, if you had omitted the query parameters, you would end up with unpredictable results. See more in this excellent explanation. Note also that while iron:router supports doing subscriptions in the route handlers, it's encouraged to handle them in the template lifecycle methods.
For your second question, the reason the Posts collection appears empty is that the code in the top level of the file is run immediately as the file is loaded. At this point, the collection subscription isn't loaded yet, so the front-end collection is empty. You need to check for subscription readiness in some reactive context, such as a template helper or Tracker.autorun, to ensure that the data is loaded:
var handle = Meteor.subscribe('posts');
if (handle.ready()) {
// This logs the actual count of posts
console.log(Posts.find().count());
} else {
// This logs 0 (unless there are other subscriptions to this collection)
console.log(Posts.find().count());
}
I want to have a function that checks every 5 seconds for all entries in database for some value is false and if finds then checks some logic condition and changes the value to true if the logic condition is met.
My function works well until I have something with isItReady: false in my collection. When I don't have, it obviously doesn't find anything and I start getting errors.
How should I do this correctly? I don't want to stop my interval because maybe something will be entered into the collection soon and then my inverval is stopped?
How can I do something like this:
If nothing matches my search criterea - productDate = Products.findOne({isItReady: false}); the interval is stopped and as soon as something new gets inserted I will start the inverval again?
var logicCheck = Meteor.setInterval( function () {
productDate = Products.findOne({isItReady: false}); //query to find all entries with isItReady: false
var timeNow = Date();
var timeCreated = productDate.startOfCountdown;
timeCreated = timeCreated.toString(); //converts timeCreated from object to String(in Mongo its a object)
var productId = productDate._id;
console.log(typeof timeNow) //string
console.log(typeof timeCreated) //string
console.log(timeNow + "timeNow")
console.log(timeCreated + "timeCreated")
if (timeCreated <= timeNow) {
console.log("check") //this works well
Products.update({_id: productId}, {$set: {isItReady: true}}, function(error, result) {
console.log(productId) //all good
if (error){
console.log(error.reason) //check the error
} else{
console.log("File with the id: " + result + " just get update")
}
});
}
}, 5000);
Your approach of polling MongoDB every 5 seconds is very non-Meteoric. You'd be much better off creating a Tracker.autorun function to instantly react to any product that has isItReady == false
For example:
Tracker.autorun(function(){
var notReadyProducts = Products.find({ isItReady: false });
notReadyProducts.forEach(function(p){
if ( your logic ... ) Products.update({ _id: p._id },{ $set: { isItReady: true }});
});
});
This assumes by the way that you're publishing (on the server) and subscribing to (on the client) a set of Products that is going to include these not ready products.
With this pattern no code will be running 99.99% of the time and then at the precise moment that a product is made not ready this code will kick in.
You might want to take a look at this video to learn more about reactive programming and how it completely changes the way you approach common problems. There are many other resources available as well.
I want to use a time log in my database entries and I want that the time is always taken from the server.
So far I set this up:
Meteor.methods({
getTimeFromServer: function () {
var serverTime = new Date().toLocaleTimeString();
return serverTime;
}
});
I want to use them like this:
Template.myTemplate.events({
'click .btn': function(e) {
var propsOfObject = {
name: $('#idName').val(),
email: $('#idEmail').val(),
time: Meteor.call('getTimeFromServer')
}
Meteor.call('addItemToDataBase', propsOfObject)
}
});
Can't figure out what I am doing wrong. I don't get any error messages. The 'time'-property is simply empty.
First of all, your approach is not working because Meteor.call on the client is always asynchronous : it doesn't return anything immediately, you must provide a callback to fetch the result some time later.
More in this here : Meteor.methods returns undefined
I think you should get the server time directly in the insertion method instead of trying to fetch it from the server before inserting : it will save you a method call and it's simpler too.
client :
Template.myTemplate.events({
'click .btn': function(e) {
var propsOfObject = {
name: $('#idName').val(),
email: $('#idEmail').val()
};
Meteor.call('addItemToDataBase', propsOfObject)
}
});
server :
Meteor.methods({
addItemToDatabase:function(fields){
check(fields,{
name:String,
email:String
});
//
_.extend(fields,{
time:new Date().toLocaleTimeString()
});
//
MyCollection.insert(fields);
}
});
On my client side, I display a list of users and a small chart for each user's points stored in the DB (using jQuery plugin called sparklines).
Drawing the chart is done on Template.rendered method
// client/main.js
Template.listItem.rendered = function() {
var arr = this.data.userPoints // user points is an array of integers
$(this.find(".chart")).sparkline(arr);
}
Now I have a Meteor method on the server side, that is called on a regular basis to update the the user points.
Meteor.methods({
"getUserPoints" : function getUserPoints(id) {
// access some API and fetch the latest user points
}
});
Now I would like the chart to be automatically updated whenever Meteor method is called. I have a method on the template that goes and calls this Meteor method.
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id);
}
});
How do I turn this code into a "reactive" one?
You need to use reactive data source ( Session, ReactiveVar ) together with Tracker.
Using ReactiveVar:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
var instance = Template.instance();
Meteor.call("getUserPoints", this._id, function(error, result) {
instance.userPoints.set(result)
});
}
});
Template.listItem.created = function() {
this.userPoints = new ReactiveVar([]);
};
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = self.userPoints.get();
$(self.find(".chart")).sparkline(arr);
})
}
}
Using Session:
if (Meteor.isClient) {
Template.listItem.events({
"click a.fetchData": function(e) {
e.preventDefault();
Meteor.call("getUserPoints", this._id, function(error, result) {
Session.set("userPoints", result);
});
}
});
Template.listItem.rendered = function() {
var self = this;
Tracker.autorun(function() {
var arr = Session.get("userPoints");
$(self.find(".chart")).sparkline(arr);
})
}
}
Difference between those implementation :
A ReactiveVar is similar to a Session variable, with a few
differences:
ReactiveVars don't have global names, like the "foo" in
Session.get("foo"). Instead, they may be created and used locally, for
example attached to a template instance, as in: this.foo.get().
ReactiveVars are not automatically migrated across hot code pushes,
whereas Session state is.
ReactiveVars can hold any value, while Session variables are limited
to JSON or EJSON.
Source
Deps is deprecated, but still can be used.
The most easily scalable solution is to store the data in a local collection - by passing a null name, the collection will be both local and sessional and so you can put what you want in it and still achieve all the benefits of reactivity. If you upsert the results of getUserPoints into this collection, you can just write a helper to get the appropriate value for each user and it will update automatically.
userData = new Meteor.Collection(null);
// whenever you need to call "getUserPoints" use:
Meteor.call("getUserPoints", this._id, function(err, res) {
userData.upsert({userId: this._id}, {$set: {userId: this._id, points: res}});
});
Template.listItem.helpers({
userPoints: function() {
var pointsDoc = userData.findOne({userId: this._id});
return pointsDoc && pointsDoc.points;
}
});
There is an alternative way using the Tracker package (formerly Deps), which would be quick to implement here, but fiddly to scale. Essentially, you could set up a new Tracker.Dependency to track changes in user points:
var pointsDep = new Tracker.Dependency();
// whenever you call "getUserPoints":
Meteor.call("getUserPoints", this._id, function(err, res) {
...
pointsDep.changed();
});
Then just add a dummy helper to your listItem template (i.e. a helper that doesn't return anything by design):
<template name="listItem">
...
{{pointsCheck}}
</template>
Template.listItem.helpers({
pointsCheck: function() {
pointsDep.depend();
}
});
Whilst that won't return anything, it will force the template to rerender when pointsDep.changed() is called (which will be when new user points data is received).
I used that "counts-by-room" example from Meteor's docs to count the number of "reviews" for an "entry" on my project. Now, when I click through to an entry, the template isn't loading. It requires a refresh. Did I mess the example up somewhere? Here's my code. I don't really understand the "initializing" part of this, and don't know what to play with to get it to work correctly.
Server:
Meteor.publish("counts-by-entry", function (entryId) {
var self = this;
check(entryId, String);
var count = 0;
var initializing = true;
var handle = Reviews.find({entry: entryId}).observeChanges({
added: function (id) {
count++;
if (!initializing)
self.changed("counts", entryId, {count: count});
},
removed: function (id) {
count--;
self.changed("counts", entryId, {count: count});
}
});
initializing = false;
self.added("counts", entryId, {count: count});
self.ready();
self.onStop(function () {
handle.stop();
});
});
Client (iron-router):
this.route('entry', {
path: '/entry/:_id',
layoutTemplate: 'layout',
yieldTemplates: {
'navigation': {to: 'navigation'},
'pt_entry': {to: 'content'}
},
waitOn: function () {
Meteor.subscribe('singleEntry', this.params._id),
Meteor.subscribe('entryReviews', this.params._id),
Meteor.subscribe('counts-by-entry', this.params._id);
},
data: {
singleEntry: function () {return Entries.findOne()},
reviews: function () {return Reviews.find({entry: Session.get('entryId')}, {sort: {date: -1}})},
count: function () {return Counts.findOne(Session.get("entryId")).count + " reviews."}
},
before: function() {
Session.set("activeNav", Router.current().route.name),
Session.set("entryId", this.params._id);
},
notFoundTemplate: 'notFoundTemplate'
});
and also:
Counts = new Meteor.Collection("counts");
I just tried recreating the minimum amount required of your example to get it to work and I don't have the same issue as you.
Do you have the javascript console open in your browser? I would look for an error there, usually this kind of stuff happens when a helper is called upon when the collection data isn't available to the client. This is what iron-router's waitOn fixes which you have made use of.
In my reproduction I only have the one subscription (counts-by-entry) so maybe there is a issue with the other ones.
As for what the initializing part does:
The publish block is a piece of code that will be run for each client subscription. It does 2 things, it provides the client with the initial payload of data which in a traditional publication would be all the documents from a collection query then it reacts to changes that affects the result of the query and sends just those changes to the client.
Here is the most common publication you will see:
Meteor.publish("all-reviews", function() {
return Reviews.find({});
});
Meteor hides the complexities of what is really going on in this publication. This is closer to what is really going on:
Meteor.publish("all-reviews", function() {
var self = this;
//this is the query we want realtime updates for
//when relevant additions, removals or changes happen the correct callbacks will fire...
var handle = Reviews.find({}).observeChanges({
added: function(id, fields) {
//when a document is added it gets sent to the client.
//Note: the initial payload of data happens here, lets say you had 5 reviews
//this would get called 5 times as soon as a user subscribes.
self.added("reviews", id, fields);
},
removed: function(id) {
//when a document is removed the client is told which one
self.removed("reviews", id);
},
changed: function(id, fields) {
//when a document has a change made to its fields the changes get sent
self.changed("reviews", id, fields);
}
});
//letting the client know that the initial payload of data has been sent.
//Stuff like iron-routers waitOn would rely on this message before loading a template
//that relies on this publication
self.ready();
//stops the publication running forever. This will fire when a client no longer needs a
//subscription or when they close the page.
self.onStop(function() {
handle.stop();
});
});
As for what it going on in the docs example with the initializing flag. The initializing flag is used as a way of simply counting all the initial payload of existing reviews in your case then after the observeChanges call telling the client how many there are. This is an optimisation on the other way of doing it which would be to send the client several self.changed messages during the initial payload.
Maybe it will make more sense if I show how it would be done without the flag:
Meteor.publish("counts-by-entry", function (entryId) {
var self = this;
check(entryId, String);
var count = 0;
//we need to initially add the document we are going to increment
self.added("counts", entryId, {count: 0});
var handle = Reviews.find({entry: entryId}).observeChanges({
added: function (id) {
count++;
//So for example if there were 100 reviews for this entry when the user
//subscribes this self.changed would get called 100 times:
//self.changed("counts", entryId, {count: 1})
//self.changed("counts", entryId, {count: 2})
//self.changed("counts", entryId, {count: 3})
//self.changed("counts", entryId, {count: 4}) etc...
//which is a waste when we can just initially tell the client how
//many reviews there are at the point of them subscribing
self.changed("counts", entryId, {count: count});
},
removed: function (id) {
count--;
self.changed("counts", entryId, {count: count});
}
});
//remove the self.added(...) which handles the optimiation as explained above
self.ready();
self.onStop(function () {
handle.stop();
});
});
Regardless it doesn't look like that particular publish is the problem. I would expect the console to make it clear what the issue is
waitOn should return an array with the subscription handles.