0

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};
  }
});
zumbamusic
  • 180
  • 1
  • 2
  • 14
  • 1
    I'm not familiar with Meteor specifically, but in general, you **can't** prevent the server method from being called (they can always do it in the console). What you can do, is upon entering the `getActiveProducts` method, first check if a certain flag is set that indicates the the user has already called the function. If it is set, skip the rest of the function. If it isn't set, then set it and unset it just before returning. – Gerrat Dec 19 '14 at 21:29
  • @Gerrat if that's true with meteor as well, I still not sure on how to initiate the check on the server side and skip function if it was executed and not finished already. Also if i have a lot of buttons/functions i guess possible there is a package for something like that or if not I will have to create one to make it easy one line global function – zumbamusic Dec 19 '14 at 21:34

2 Answers2

0

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.

sbking
  • 7,630
  • 24
  • 33
  • there is an issue with this approach, If an error is thrown such `Meteor.Error` gettingProducts will stay true, and even if it's not really running it will say it's! I can of course try and cover every error out there with `Meteor.users.update(this.userId, {$set: {gettingProducts: false}});` but it's kind of error prone, there must be a better way to detect that or that's the best can be? – zumbamusic Dec 20 '14 at 01:08
  • You could wrap any code that could throw an error in a throw/catch and do the update from there. Or just do the X seconds method which does not rely on resetting the value after the method finishes. – sbking Dec 20 '14 at 01:15
  • I can definitely wrap every error but if it's unexpected error in anyway something like my own server crash anything unexpected will cause X user to be stuck with this in his collection and limited usability of the website so I think it's not really safe to use. Option 2. by X seconds? I cant really predict how much time it will take and even if I set it at X seconds, I throw an error if server doesn't have users authorized service API, First call will show error correctly but if you click again it will not show anything or the wrong error, I think it's over complicated to cover everything – zumbamusic Dec 20 '14 at 01:51
  • I have started looking on meteor github files about loggingIn() method which is set to true/false I think that logic should work in this case it's damn too complicated to do such a simple thing. – zumbamusic Dec 20 '14 at 01:59
  • After alot of analysis, For my case it's best to use collection and cover errors manually by removing them – zumbamusic Dec 20 '14 at 23:11
0

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

mark
  • 1,725
  • 1
  • 15
  • 14
  • I actually have found your package after looking for persistent session I liked it but wanted to post an issue for few question/possible improvement and thought ah maybe what I need doesn't fit your needs! It's really interesting you found my question! :) I really liked that it's uses real collection and no hacks it will definitely be reliable, I did also like you can read the session from user/set from server. I didn't understand the connection thing what is it a connection? 2 different tabs = 2 connections? Page refresh = connection close? How to overcome page refresh if possible? – zumbamusic Dec 20 '14 at 14:31
  • The connection is basically the DDP connection that's established per client. So you're correct, 2 different tabs would mean 2 different connections. The reason the package is tied to connection (and not to something like userId) is that the session should work independent of user accounts (originally it was used for setting up a one-time access API). For the purposes of rate limiting I think page refresh is a pretty good deterrent (I can't speak too much about the matter). Feel free to open an issue though, and I can look into it further :) – mark Dec 20 '14 at 19:58