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

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");
}
});
});

Related

Firebase React Native fetch data

I am trying to do an app on react-native with a feed. On my main screen, I go fetch the data :
fetchData() {
firebase.database().ref(`/posts/${group}`).on('value', async snapshot => {...}
}
when I want for example to like a comment of the post, I first push the data into firebase with different queries, for example :
export const likeComment = (...) => {
firebase.database().ref(`/posts/${group}/${post}`).update
({
updatedAt: firebase.database.ServerValue.TIMESTAMP
});
firebase.database().ref(`/posts/${group}/${post}/lastComments`).set(...);
But I realized that my first function fetchData was called 3 times.
then I grouped my queries like :
let updates = {}
updates[`/posts/${group}/${post}/lastComments`] = {...};
updates[`/posts/${group}/${post}`] = { ... };
firebase.database().ref().update(updates);
Then, fetchData was called still 2 times.
I was wondering if that was the best way to do it, and why does my function fetchData was still called twice.
Thanks for your help
It's hard to tell from the limited code, but my guess is that fetchData is not actually being called more than once, but instead the snapshot is firing when you make updates to your Firebase database.
The part of your code where you say .on('value', async snapshot => you're setting up a listener to send you updates any time that value changes.
So my guess is that your three invocations are:
The first time you actually call fetchData and set up the
listener
Your .on( snapshot listener firing when you call
update
Your .on( snapshot listener firing again when you
call set
This push-based database workflow is one of the main benefits of Firebase. If you only want to get the data once and not get live updates, you can call .once( instead of .on(
https://firebase.google.com/docs/database/web/read-and-write

Fetch data for record edit page

I have a page that lets you edit user data. I'm using FlowRouter for the routing and it can be found on the route /employees/:id.
I need to update the detail form when data changes on the server and leave the route if it was deleted by other client.
I decided to use Tracker.autorun which informs me whenever the data changes. The previous user info is stored on the template so it's easy to tell if the record was deleted.
Template.UpdateEmployee.onCreated(function () {
const self = this;
self.subscribe('user', FlowRouter.getParam('id'));
self.autorun(function () {
const _id = FlowRouter.getParam('id');
const user = Meteor.users.findOne({_id});
if(!user && self.user)
FlowRouter.go('/employees');
self.user = user;
if(!user)
return;
user.email = user.emails[0].address;
$('.ui.form').form('set values',user);
});
});
And lastly in the onRendered callback I'm checking if the data was set on template as I believe not doing so could lead to data being available before the template is rendered and hence values wouldn't get set properly. Is this correct?
Template.UpdateEmployee.onRendered(function () {
if(this.user){
user.email = user.emails[0].address;
$('.ui.form').form('set values',user);
}
});
Are there any pitfalls to this solution?
I can see a couple drawbacks inherently. The first one is doing a find query on the client. Typically you would want to return data from the server using Meteor publish and subscribe.
The second is you are passing the key to find the data over the URL. This can be spoofed by other users for them to find that users data.
Lastly if you are doing a find on the user object, I assume you might be storing data there. This is generally bad practice. If you need to store user data with their profile, it's best to create a new collection and publish/subscribe what you need.

Meteor.connection._lastSessionId is undefined in onCreated

So I have this code guys
Template.mainLayout.onCreated(function () { //HERE
console.log("mainLayout created");
var context = FlowRouter.current();
// use context to access the URL state
console.log(context);
var visitedOne = context.path;
//getting the connID
var clientIp = headers.getClientIP(); // no need for this anymore
var clientConnId = Meteor.connection._lastSessionId; // HERE
console.log(clientIp);
console.log(clientConnId); //HERE
// console.log(Meteor.connection._lastSessionId);
Meteor.call("updateHistory", {clientIp,clientConnId,visitedOne}, function(error, result){
if(error){
console.log("error", error);
});
if(result){
}
});//Meteor.call
});
My problems are marked by the comments //HERE
Meteor.connection._lastSessionId returns undefined at onCreated event. However if I try to get on click event it works just fine. Why is this caused, what's a workaround for this?
You're attempting to log the session ID before the connection has received it. For example, wrap your call in a setTimeout:
...
setTimeout(() => {
console.log(Meteor.connection._lastSessionId);
}, 500);
...
You might have to tweak the timeout value a bit, but it will be logged. Using setTimeout in this fashion really isn't that reliable though, as the amount of time it takes for the session ID to get set can vary. You'll likely want to look into setting up some kind of simple polling to continuously check for the session ID, until it's set.
Basically _lastSessionId isn't yet available on the client when the template is originally created (it's probably the first template rendered in your app). However there is no need to get this on the client since you're calling a server method anyway, just use the variable directly there where it will already exist!
So simplify:
Meteor.call("updateHistory", {clientIp,clientConnId,visitedOne}, callback)
to:
Meteor.call("updateHistory", visitedOne, callback)
and get the clientIp (if necessary) and use this.connection.id on the server.

Meteor.subscribe is skipped, doesn’t work in Tracker.autorun

I’m using user’s profile to publish specific data to client.
However the subscribe doesn’t work and it’s killing me.
Although the Tracker.autorun() runs, the server doesn’t run any publish.
Tracker.autorun(function () {
console.log(‘autorun is running');
console.log(Meteor.user()) //to be reactive to user's update
Meteor.subscribe(“Lists”,{
onStop:function() {
console.log('subscribe call back onStop');
},
onReady: function(){
console.log('subscribe call back onReady');
});
console.log(Lists.find().count());
}
Meteor.publish('Lists', function(){
console.log('publish is running’);
var list = Meteor.users.findOne({_id: this.userId}).list;
return Lists.find({_id: {$in: list}});
}
p.s if I type Meteor.subscribe( “Lists”) in Chrome's console line, it works and server run publish normally.
p.s The Lists collection didn't change. Just publishing a different set. Is it the reason?
Thank for your reading.
If you have a close look at the docs for subscribe, you'll find this note in a section about reactive computations:
However, if the next iteration of your run function subscribes to the same record set (same name and parameters), Meteor is smart enough to skip a wasteful unsubscribe/resubscribe.
So because you are always calling subscribe with the same arguments, meteor isn't actually restarting it. The trick is just to pass extra parameters to defeat this "optimization". For example:
Tracker.autorun(function() {
var user = Meteor.user();
var list = user && user.list;
if (!_.isEmpty(list)) {
Meteor.subscribe('Lists', list, function() {
console.log(Lists.find().count());
});
}
});
Here we are extracting the list variable from the user (assuming it's published) and using it as an extra parameter to force the subscription to rerun. If it isn't published, you could just use a random id like this:
Tracker.autorun(function() {
var user = Meteor.user();
Meteor.subscribe('Lists', Random.id(), function() {
console.log(Lists.find().count());
});
});
This should also work but may be a little less efficient because it will fire whenever any property of the user changes.
I think you should run the "console log" after the subscribe and the publish methods return something, if you do somenthing inmediatly is should not work because the server is not returning anything yet, add a callback to the subscription.
Tracker.autorun( function() {
Meteor.subscribe( "List", function() {
console.log( "okok" );
// do your magic
}
};
hope it help, sorry about my english =)

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 :)

Resources