How to stop and restart collection observers in Meteor? - javascript

I want to be able to stop and restart observers on my collections in Meteor.
Imagine I have the following observer:
// Imagine some collection of Blog posts "Posts"
Posts.find().observe({
changed: notifySubscribedUsers
});
// function notifySubscribedUsers() { ... }
// is some function that will email everyone saying some post was updated
Now imagine I want to update lots of Posts, but I dont want the observers to be called. How can I get access to the observers, stop/pause them and then later restart them (after the db job is finished) ?
TIA

The observer returns a handle:
var handle = Posts.find().observe({
changed: notifySubscribedUsers
});
Then you can stop it with:
handle.stop()
It's not possible to 'pause' it in the conventional sense, if you want to pause it you could just ignore the data it gives you.
To do this in a neat wrapped up method you could do something like:
var handle;
var start = function() {
if(handle) handle.stop();
var handle = Posts.find().observe({
changed: notifySubscribedUsers
});
}
var stop = function() { if(handle) handle.stop }
Or to put it on a collection:
// posts.js collection file
Posts.startObservers = function startObservers() {
Posts.observer = Posts.find().observe({
change: notifySubscribedUsers // or some other function
});
};
Posts.stopObservers = function stopObservers() {
if(Posts.observer) {
Posts.observer.stop(); // Call the stop
}
};
// Trigger Somewhere else in the code
Posts.stopObservers();
MyTool.doWorkOnPosts(); // Some contrived work on the Posts collection
Posts.startObservers();

Related

How to make this run only once?

I'm trying to get it where If someone clicks the button it will update the database but what happens if I enter 50 then it will keep running it and I have a tracking board that sums everything up so it overloads my server and makes the total in the 1000's when its normally just over 100.
I've tried a document ready function, I've had on and one. ('click') and it keeps running multiple times
$('#update_new_used-counter').one('click', function (event) {
event.preventDefault();
let updated_new_counter = $('#new_sold-update').val().trim();
let updated_used_counter = $('#used_sold-update').val().trim();
trackingBoardRef.on("value", function(snapshot) {
let current_new_counter = snapshot.val().new;
let current_used_counter = snapshot.val().used;
if (updated_new_counter == '') {
trackingBoardRef.update({
new: current_new_counter,
});
} else {
trackingBoardRef.update({
new: updated_new_counter,
})
};
if (updated_used_counter == '') {
trackingBoardRef.update({
used: current_used_counter,
});
} else {
trackingBoardRef.update({
used: updated_used_counter,
})
};
console.log(snapshot.val().new);
console.log(snapshot.val().used);
});
});
That's what I have now and it just keeps running multiple times until firebase says I had to many requests and stops it. I just want it to update once
When you call:
trackingBoardRef.on("value", function(snapshot) {
You attach a listener to the data in trackingBoardRef that will be triggered right away with the current value, and then subsequently whenever the data under trackingBoardRef changes. And since you're changing data under trackingBoardRef in your code, you're creating an infinite loop.
If you only want to read the data once, you can use the aptly named once method:
trackingBoardRef.once("value", function(snapshot) {
...
Note that if you're update the value under trackingBoardRef based on its current value, you really should use a transaction to prevent users overwriting each other's changes.

Collection empty when accessed from rendered event?

I have a collection that I am subscribing to, but when I attempt to access it from my onRendered event it always returns as an empty array. Below is the method I am using:
FlightCounts = new Mongo.Collection("flightCounts");
if (Meteor.isClient) {
Meteor.subscribe('flightCounts');
Template.hello.rendered = function(){
var counts = FlightCounts.find().fetch()
console.log(counts)
}
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
Meteor.publish('flightCounts', function(){
return flightCounts.find();
})
});
}
Can anyone see why my collection would always be empty here? Any suggestions on how to get it populated?
The underlying issue is that the publish function should reference Meteor's Mongo.Collection name, FlightCounts, and not the raw db.collection name flightCounts:
Meteor.publish('flightCounts', function(){
return FlightCounts.find();
});
I also agree with the prior answer that your template should check to ensure the subscription is ready before logging the data, otherwise it may not have arrived yet:
Template.hello.onRendered(function(){
this.autorun(() => {
let ready = Template.instance().subscriptionsReady();
if (!ready){ return; }
let counts = FlightCounts.find().fetch();
console.log(counts);
});
});
(Note that the newer syntax for Template.name.rendered is Template.name.onRendered().)
if (Meteor.isClient) {
Meteor.subscribe('flightCounts');
Template.hello.rendered = function() {
var template = this;
template.autorun(function () {
var counts = FlightCounts.find().fetch()
console.log(counts)
});
}
}
Your collection is not populated because, it is still getting data from server. So you should wrap your code inside an autorun, so that it will re-run whenever data is updated.
P.S: I am on mobile, so formatting may not be proper. Sorry about it.
Alternatively, you can use subscriptions of FlowRouter or waitOn of IronRouter to put the subscribe there
Update Surprisingly, IronRouter now has the subscriptions option as well

How to add observer for nsIBrowserSearchService

I'm having trouble adding an observer to watch for changes in firefox's search engines. I read the nsIBrowserSearchService page on the Mozilla Developer Site which suggests to use the init() method of the Services.search object.
void init([optional] in nsIBrowserSearchInitObserver observer);
I tried that and I managed to get it to execute the function once on start up but it never calls it again when I add or remove or reorder the search engines. So I'm doing something wrong.
I have experience with observers but only with using general preferences and I generally use add them and remove them using the code below. I'm not sure how to do it with the nsIBrowserSearchService . I would like to observe the nsIBrowserSearchService in the same or a similar way but I'm not sure what I would put for
branch = Services.prefs.getBranch("preferenceNameHere");
I'm not observing Services.prefs but Services.search I assume, and that has no getBranch method as far as I can tell.
This is how I normally add and remove the observer in Chrome.js
const {Ci, Cu} = require("chrome");
const {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
branch = Services.prefs.getBranch("preferenceNameHere");
branch.addObserver("", observe, false);
function observe(subject, topic, data) {
// instanceof actually also "casts" subject
if (!(subject instanceof Ci.nsIPrefBranch)) {
return;
}
//do stuff here
};
exports.onUnload = function(reason) {
// Need to remove our observer again! This isn't automatic and will leak
// otherwise.
branch.removeObserver("", observe);
if(reason == "disable" || reason == "uninstall"){
//restore changes made by addon
}
};
Can anyone advise me on how to do this so I can observe for changes and ensure that I remove the observer properly with the search object. Thanks
What you are trying above is trying to use pref observer on non-prefs, this is not possible. You have to use the regular observer service like this.
This notifies you when the user changes the engine.
Cu.import('resource://gre/modules/Services.jsm');
var observers = {
'browser-search-engine-modified': {
aTopic: 'browser-search-engine-modified',
observe: function (aSubject, aTopic, aData) {
if (aData == 'engine-current') {
console.log('current engine was changed!');
//console.log('aSubject on change:', aSubject.name, 'same as Services.search.currentEngine.name:', Services.search.currentEngine.name); //aSubject is the engine
//console.log('aTopic on change:', aTopic); //aTopic is obviously `browser-search-engine-modified`
}
},
reg: function () {
Services.obs.addObserver(observers[this.aTopic], this.aTopic, false);
},
unreg: function () {
Services.obs.removeObserver(observers[this.aTopic], this.aTopic);
}
}
};
To start listening do this:
for (var o in observers) {
observers[o].reg();
}
To stop listening do this:
for (var o in observers) {
observers[o].unreg();
}
I'm not sure what happens when user adds a new engine but doenst select it. Or if he removes a engine. Please let me know what those messages are when user does that.

Meteor, One to Many Relationship & add field only to client side collection in Publish?

Can anyone see what may be wrong in this code, basically I want to check if a post has been shared by the current logged in user AND add a temporary field to the client side collection: isCurrentUserShared.
This works the 1st time when loading a new page and populating from existing Shares, or when adding OR removing a record to the Shares collection ONLY the very 1st time once the page is loaded.
1) isSharedByMe only changes state 1 time, then the callbacks still get called as per console.log, but isSharedByMe doesn't get updated in Posts collection after the 1st time I add or remove a record. It works the 1st time.
2) Why do the callbacks get called twice in a row, i.e. adding 1 record to Sharescollection triggers 2 calls, as show by console.log.
Meteor.publish('posts', function() {
var self = this;
var mySharedHandle;
function checkSharedBy(IN_postId) {
mySharedHandle = Shares.find( { postId: IN_postId, userId: self.userId }).observeChanges({
added: function(id) {
console.log(" ...INSIDE checkSharedBy(); ADDED: IN_postId = " + IN_postId );
self.added('posts', IN_postId, { isSharedByMe: true });
},
removed: function(id) {
console.log(" ...INSIDE checkSharedBy(); REMOVED: IN_postId = " + IN_postId );
self.changed('posts', IN_postId, { isSharedByMe: false });
}
});
}
var handle = Posts.find().observeChanges({
added: function(id, fields) {
checkSharedBy(id);
self.added('posts', id, fields);
},
// This callback never gets run, even when checkSharedBy() changes field isSharedByMe.
changed: function(id, fields) {
self.changed('posts', id, fields);
},
removed: function(id) {
self.removed('posts', id);
}
});
// Stop observing cursor when client unsubscribes
self.onStop(function() {
handle.stop();
mySharedHandle.stop();
});
self.ready();
});
Personally, I'd go about this a very different way, by using the $in operator, and keeping an array of postIds or shareIds in the records.
http://docs.mongodb.org/manual/reference/operator/query/in/
I find publish functions work the best when they're kept simple, like the following.
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('sharedPosts', function(postId) {
var postRecord = Posts.findOne({_id: postId});
return Shares.find{{_id: $in: postRecord.shares_array });
});
I am not sure how far this gets you towards solving your actual problems but I will start with a few oddities in your code and the questions you ask.
1) You ask about a Phrases collection but the publish function would never publish anything to that collection as all added calls send to minimongo collection named 'posts'.
2) You ask about a 'Reposts' collection but none of the code uses that name either so it is not clear what you are referring to. Each element added to the 'Posts' collection though will create a new observer on the 'Shares' collection since it calls checkSharedId(). Each observer will try to add and change docs in the client's 'posts' collection.
3) Related to point 2, mySharedHandle.stop() will only stop the last observer created by checkSharedId() because the handle is overwritten every time checkSharedId() is run.
4) If your observer of 'Shares' finds a doc with IN_postId it tries to send a doc with that _id to the minimongo 'posts' collection. IN_postId is passed from your find on the 'Posts' collection with its observer also trying to send a different doc to the client's 'posts' collection. Which doc do you want on the client with that _id? Some of the errors you are seeing may be caused by Meteor's attempts to ignore duplicate added requests.
From all this I think you might be better breaking this into two publish functions, one for 'Posts' and one for 'Shares', to take advantage of meteors default behaviour publishing cursors. Any join could then be done on the client when necessary. For example:
//on server
Meteor.publish('posts', function(){
return Posts.find();
});
Meteor.publish('shares', function(){
return Shares.find( {userId: this.userId }, {fields: {postId: 1}} );
});
//on client - uses _.pluck from underscore package
Meteor.subscribe( 'posts' );
Meteor.subscribe( 'shares');
Template.post.isSharedByMe = function(){ //create the field isSharedByMe for a template to use
var share = Shares.findOne( {postId: this._id} );
return share && true;
};
Alternate method joining in publish with observeChanges. Untested code and it is not clear to me that it has much advantage over the simpler method above. So until the above breaks or becomes a performance bottleneck I would do it as above.
Meteor.publish("posts", function(){
var self = this;
var sharesHandle;
var publishedPosts = [];
var initialising = true; //avoid starting and stopping Shares observer during initial publish
//observer to watch published posts for changes in the Shares userId field
var startSharesObserver = function(){
var handle = Shares.find( {postId: {$in: publishedPosts}, userId === self.userId }).observeChanges({
//other observer should have correctly set the initial value of isSharedByMe just before this observer starts.
//removing this will send changes to all posts found every time a new posts is added or removed in the Posts collection
//underscore in the name means this is undocumented and likely to break or be removed at some point
_suppress_initial: true,
//other observer manages which posts are on client so this observer is only managing changes in the isSharedByMe field
added: function( id ){
self.changed( "posts", id, {isSharedByMe: true} );
},
removed: function( id ){
self.changed( "posts", id, {isSharedByMe: false} );
}
});
return handle;
};
//observer to send initial data and always initiate new published post with the correct isSharedByMe field.
//observer also maintains publishedPosts array so Shares observer is always watching the correct set of posts.
//Shares observer starts and stops each time the publishedPosts array changes
var postsHandle = Posts.find({}).observeChanges({
added: function(id, doc){
if ( sharesHandle )
sharesHandle.stop();
var shared = Shares.findOne( {postId: id});
doc.isSharedByMe = shared && shared.userId === self.userId;
self.added( "posts", id, doc);
publishedPosts.push( id );
if (! initialising)
sharesHandle = startSharesObserver();
},
removed: function(id){
if ( sharesHandle )
sharesHandle.stop();
publishedPosts.splice( publishedPosts.indexOf( id ), 1);
self.removed( "posts", id );
if (! initialising)
sharesHandle = startSharesObserver();
},
changed: function(id, doc){
self.changed( "posts", id, doc);
}
});
if ( initialising )
sharesHandle = startSharesObserver();
initialising = false;
self.ready();
self.onStop( function(){
postsHandle.stop();
sharesHandle.stop();
});
});
myPosts is a cursor, so when you invoke forEach on it, it cycles through the results, adding the field that you want but ending up at the end of the results list. Thus, when you return myPosts, there's nothing left to cycle through, so fetch() would yield an empty array.
You should be able to correct this by just adding myPosts.cursor_pos = 0; before you return, thereby returning the cursor to the beginning of the results.

Meteor method reactivity doesn't work

I'm not experienced in Javascript but I've read a ton of articles about Meteor reactivity but still can't figure out why it is not working in my case.
When a new product is added, I want to be recalculated total cost and use it in the totalCost helper so it's almost real time visible in the browser.
Can someone please take a look at my code and try to figure out some logic error? Everything except the reactivity is working on my computer.
I have got this method in /models/Product.js :
Meteor.methods({
totalProductCost: function() {
var pipeline = [
{$match: {owner: Meteor.userId()}},
{$group: {_id: null, cost: {$sum: "$cost"}}}
];
var data = Products.aggregate(pipeline)["0"].cost;
return (data === undefined) ? 0 : data;
}
});
Then I've got layout.js in client folder:
if (Meteor.isClient) {
var handle = Meteor.subscribe("Products", Meteor.userId());
ProductManager = {
_productItems: null,
_dep: new Tracker.Dependency(),
getProducts: function () {
this._dep.depend();
return this._productItems;
},
setProducts: function (value) {
if (value !== this._productItems) {
this._productItems = value;
this._dep.changed();
}
},
getTotalCost: function () {
return ReactiveMethod.call('totalProductCost');
}
}
// TRACKER
Tracker.autorun(function () {
if (handle.ready()) {
ProductManager.setProducts(Products.find().fetch());
}
});
// HELPERS
Template.boxOverview.helpers({
"totalCost" : function () {
return ProductManager.getTotalCost();
},
});
}
It seems that you used a collection.aggregate in a method. If you need reactivity, you need to use a publication rather than a method (or you need to call the method each time you want to refresh). However, if you use your aggregation inside your publication (I assume you use a package for it) you will loose reactivity as well.
What I would advise you is to use a publication without aggregate function. You calculate your product cost by creating a new field and adding it to your cursor. Once, you do that, if you want to keep reactivity, it is necessary to use to use cursor.observeChanges() or just cursor.observe().
Have a look at this example:
var self = this;
// Modify the document we are sending to the client.
function filter(doc) {
var length = doc.item.length;
// White list the fields you want to publish.
var docToPublish = _.pick(doc, [
'someOtherField'
]);
// Add your custom fields.
docToPublish.itemLength = length;
return docToPublish;
}
var handle = myCollection.find({}, {fields: {item:1, someOtherField:1}})
// Use observe since it gives us the the old and new document when something is changing.
// If this becomes a performance issue then consider using observeChanges,
// but its usually a lot simpler to use observe in cases like this.
.observe({
added: function(doc) {
self.added("myCollection", doc._id, filter(doc));
},
changed: function(newDocument, oldDocument)
// When the item count is changing, send update to client.
if (newDocument.item.length !== oldDocument.item.length)
self.changed("myCollection", newDocument._id, filter(newDocument));
},
removed: function(doc) {
self.removed("myCollection", doc._id);
});
self.ready();
self.onStop(function () {
handle.stop();
});
This is taken from here.

Categories

Resources