Detecting when a subscription is cancelled in Meteor - meteor

How can I detect when a subscription is cancelled?
There are many ways it can be cancelled and I would like to analyze cancellation/subscription behavior.

Set a handle to your collection subscription, e.g.
var subHandle = Meteor.subscribe('subscription-name')
Subscription handles have a method called ready() that is a reactive data source. If the subscription has been cancelled, it will return false. Because it is a reactive data source, you could place an if statement inside a reactive computation to detect when the the subscription is cancelled.
Example:
Deps.autorun(function() {
if (subHandle && (! subHandle.ready())) {
// subscription has been cancelled
}
});
However, make sure you run this function after subHandle has been defined. This computation will not register with a reactive dependency until it calls subHandle.ready().

Related

Meteor Blaze Subscription.ready() seems to trigger autorun.

I have now for umpteen months used this pattern for template level subscriptions in my Meteor/Blaze applications.
However, after upgrading to Meteor 1.4.3.2, I seem to have an odd bug on one of my templates.
I have this publication:
Meteor.publish('reports.byId', function(reportId){
console.log("subscribe reports.byId", reportId);
const reports = Reports.find({_id: reportId});
console.log(reports.count());
return reports;
});
I've removed any validation of user rights and have added the writes to check that I actually get data etc.
Now I'm calling this using this onCreated method:
Template.manageReport.onCreated(function(){
const instance = this;
const reportId = FlowRouter.getParam("reportId");
instance.autorun(function(){
const reportSub = instance.subscribe('reports.byId', reportId);
if (reportSub.ready()){
console.log("ready");
}
});
});
The odd thing is this: If I remove the check if the subscription is ready, everything works as expected. As soon as I check for the subscription readiness, the subscription is never ready and I can see via the log messages on the server that the subscriptions are requested to the tune of a few dozen times per second.
Yes, reportSub.ready() is reactive so it does trigger the autorun. Actually it's the only thing triggering your autorun. When subscription is ready it triggers autorun and then it's not ready anymore, because you've just called it again. It's a loop.
I think you should not call instance.subscribe and check for its readiness all inside the same autorun.
Since reportId is a route parameter and it's not going to change, you don't need autorun for the subscription. You would only need it if any parameter were a reactive variable that could change.
This should work just fine. Let me know how it goes.
Template.manageReport.onCreated(function() {
const instance = this;
const reportId = FlowRouter.getParam("reportId");
// Create subscription
const reportSub = instance.subscribe('reports.byId', reportId);
// Check when subscription is ready
instance.autorun(function() {
if (reportSub.ready()) {
console.log("ready");
}
});
});

How to get all subscription data before rendering and then just update the dom when new data is received?

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.

Are stop-returning functions still stopped when inside nested functions?

In addition, the following functions which return an object with a
stop method, if called from a reactive computation, are stopped when
the computation is rerun or stopped:
Tracker.autorun (nested) Meteor.subscribe observe() and
observeChanges() on cursors
This means that the observe stops here:
Tracker.autorun ->
cursor.observe()
But what about here?
Tracker.autorun ->
Tracker.nonReactive ->
cursor.observe()
When a MiniMongo reactive handle (find(), observe(), etc.) is created in a reactive computation, MiniMongo will detect it and attach a listener to the computation's onInvalidate that will stop the handle when the computation is invalidated (or stopped).
It does not matter whether this is done directly in the autorun callback
or in a function that is called from the callback it, as long as it is done synchronously (i.e, while in the context of the same computation).
There is one notable exception: a non-reactive callback.
A Tracker.nonreactive callback is fired within a non-reactive context, so the current computation is set to null and Tracker.active is set to false. Under those conditions, MiniMongo will not attach the aforementioned listener and the change observer will not be stopped automatically.
You can stop it manually, though:
const MyCollection = new Mongo.Collection(null);
const cursor1 = MyCollection.find({foo: 'bar'});
const cursor2 = MyCollection.find({foo: 'baz'});
let observeCallback = {
added(doc) {
console.log('added', doc);
}
};
let handle = Tracker.autorun(function(c) { // c is the computation object
cursor1.observe(observeCallback); // will be automatically stopped
Tracker.nonreactive(function() {
let observer = cursor2.observe(observeCallback);
c.onStop(function() {
observer.stop(); // explicitly stops the observer
})
});
});
MyCollection.insert({foo: 'bar'});
MyCollection.insert({foo: 'baz'});
handle.stop();

Meteor How to block a method call before the first one is finished?

I have the following scenario:
Client side has a button clicking it will execute Meteor.call method on the server-side which will call API and fetch products, During this time I wan't to disable this button + block this method from executing again basically nothing stops you from clicking the button 100x times and server will keep on executing same method again and again.
Few ideas I had in my mind: Use sessions to disable button (Problem: can still using the console Meteor.call and abuse it)
I also looked at Meteor.apply in the docs with wait:true didn't seems to stop from method execution. I honestly not sure how this kind of thing is handled with no hacks.
Client-side:
'click .button-products': function(e){
Meteor.call('getActiveProducts', function(error, results){
if (error)
return Alerts.add(error.reason, 'danger', {autoHide: 5000});
if (results.success)
return Alerts.add('Finished Importing Products Successfully', 'success', {autoHide: 5000});
})
}
Server-side
Meteor.methods({
getActiveProducts: function(){
var user = Meteor.user();
var api = api.forUser(user);
importProducts = function(items){
nextPage = items.pagination.next_page;
items.results.forEach(function(product){
var sameproduct = apiProducts.findOne({listing_id: product.listing_id});
if (sameproduct) {
return;
}
var productExtend = _.extend(product, {userId: Meteor.userId()});
apiProducts.insert(productExtend);
});
};
var products = api.ProductsActive('GET', {includes: 'Images', limit: 1});
importProducts(products);
while (nextPage !== null) {
products = api.ProductsActive('GET', {includes: 'Images', page: nextPage, limit: 1});
importProducts(products);
}
return {success: true};
}
});
From the Meteor docs:
On the server, methods from a given client run one at a time. The N+1th invocation from a client won't start until the Nth invocation returns. However, you can change this by calling this.unblock. This will allow the N+1th invocation to start running in a new fiber.
What this means is that subsequent calls to the method won't actually know that they were made while the first call was still running, because the first call will have already finished running. But you could do something like this:
Meteor.methods({
getActiveProducts: function() {
var currentUser = Meteor.users.findOne(this.userId);
if (currentUser && !currentUser.gettingProducts) {
Meteor.users.update(this.userId, {$set: {gettingProducts: true}});
// let the other calls run, but now they won't get past the if block
this.unblock();
// do your actual method stuff here
Meteor.users.update(this.userId, {$set: {gettingProducts: false}});
}
}
});
Now subsequent calls may run while the first is still running, but they won't run anything inside the if block. Theoretically, if the user sends enough calls, the first call could finish before all of the others have started. But this should at least significantly limit the number of etsy calls that can be initiated by a user. You could adapt this technique to be more robust, such as storing the last time a successful call was initiated and making sure X seconds have passed, or storing the number of times the method has been called in the last hour and limiting that number, etc.
A package I wrote a while back might come in handy for you. Essentially it exposes the Session api on the server side (hence the name), meaning you can do something like ServerSession.set('doingSomethingImportant', true) within the call, and then check this session's value in subsequent calls. The session can only be set on the server, and expires upon connection close (so they could spam calls, but only as fast as they can refresh the page).
In the event of error, you can just reset the session. There shouldn't be any issues related to unexpected errors either because the session will just expire upon connection close. Let me know what you think :)

How to prevent a client race condition between Meteor.userId() and subscription updates that depend on userId?

I am seeing a repeatable issue where a user authenticates ("logs in") with a Meteor server, and then a client subscription that depends on userId is updated (and dependent UI templates reactively update) before Meteor.userId() registers the successful login.
For example, in this code snippet, the assert will throw:
var coll = new Meteor.Collection("test");
if (Meteor.isServer) {
Meteor.publish('mineOrPublic', function () {
// Publish public records and those owned by subscribing user
return coll.find({owner: { $in: [ this.userId, null ]}});
});
}
if (Meteor.isClient) {
var sub = Meteor.subscribe('mineOrPublic');
var cursor = coll.find({});
cursor.observe({
added: function (doc) {
if (doc.owner) {
// This should always be true?!
assert(doc.owner === Meteor.userId());
}
}
});
}
Analogous to the added function above, if I write a template helper that checks Meteor.userId(), it will see a value of null, even when it is invoked with a data context of a document with an owner.
There is apparently a race condition between Meteor collection Pub/Sub and the Account userId update mechanisms. It seems to me that Meteor.userId() should always be updated before any subscriptions update based on a change in this.userId in a server publish function, but for some reason the opposite usually seems to be true (that is, the assert in the code above will usually throw).
The reason I care is because I have packages that depend on obtaining a valid Meteor Authentication token (using Accounts._storedLoginToken()) on the client for use in securing HTTP requests for files stored on the Meteor server. And the authentication token isn't correct until Meteor.userId() is. So the flow of events usually goes something like this:
User logs in
Publish function on server reruns based on the change in this.userId.
Client begins receiving new documents corresponding to the change in userId.
UI Template reactively updates to add DOM elements driven by new documents
Some of the DOM elements are <img> tags with src= values that depend on the data context.
HTTP requests are triggered and ultimately fail with 403 (forbidden) errors because the required authentication cookie hasn't been set yet.
Meteor.userId() finally updates on the client, and code reactively runs to set the authentication cookie
Helpers in the template that depend on a session variable set in the cookie update code are rerun, but the DOM doesn't change, because the URLs in the <img> tags don't change.
Because the DOM doesn't change, the tags don't retry their failed attempts to load the images.
Everything settles down, and the user has to manually reload the page to get their images to appear.
I've come up with two possible approaches to work around this issue:
In the template helper that generates the URL for the <img> tag, always append a dummy query string such as: "?time=" + new Date().getTime(). This causes the DOM to change every time the helper is called and fixes the problem, but it screws-up browser caching and if not coordinated will cause some assets to unnecessarily load multiple times, etc.
In every template helper that touches document data add a test of:
if (this.owner && this.owner !== Meteor.userId()) {
// Perhaps Meteor.loggingIn() could be used above?
// Invalid state, output placeholder
} else {
// Valid state, output proper value for template
}
I really hope someone knows of a less kludgy way to work around this. Alternatively, if consensus arises that this is a bug and Meteor's behavior is incorrect in this respect. I will happily file an issue on Github. I mostly really enjoy working with Meteor, but this is the kind of gritty annoyance that grinds in the gears of "it just works".
Thanks for any and all insights.
After trying lots of things, this variation on the example code in the OP seems to consistently solve the race condition, and I find this an acceptable resolution, unlike my initial attempted workarounds.
I still feel that this kind of logic should be unnecessary and welcome other approaches or opinions on whether Meteor's behavior in the OP sample code is correct or erroneous. If consensus emerges in the comments that Meteor's behavior is wrong, I will create an issue on Github for this.
Thanks for any additional feedback or alternative solutions.
var coll = new Meteor.Collection("test");
if (Meteor.isServer) {
Meteor.publish('mineOrPublic', function (clientUserId) {
if (this.userId === clientUserId) {
// Publish public records and those owned by subscribing user
return coll.find({owner: { $in: [ this.userId, null ]}});
} else {
// Don't return user owned docs unless client sub matches
return coll.find({owner: null});
}
});
}
if (Meteor.isClient) {
Deps.autorun(function () {
// Resubscribe anytime userId changes
var sub = Meteor.subscribe('mineOrPublic', Meteor.userId());
});
var cursor = coll.find({});
cursor.observe({
added: function (doc) {
if (doc.owner) {
// This should always be true?!
assert(doc.owner === Meteor.userId());
}
}
});
}
This code works by giving the server publish function the information it needs to recognize when it is running ahead of the client's own login state, thereby breaking the race condition.
I think this is something that Meteor should do automatically: clients should not see documents based on changes to this.userId in a publish function until after the client Meteor.userId() has been updated.
Do others agree?
I tried with this code that works on server too. In association with FileCollection package.
if (Meteor.isServer) {
CurrentUserId = null;
Meteor.publish(null, function() {
CurrentUserId = this.userId;
});
}
....
OrgFiles.allow({
read: function (userId, file) {
if (CurrentUserId !== file.metadata.owner) {
return false;
} else {
return true;
}
}
...

Resources