Difference between using Meteor.isClient/isServer and this.isSimulation?

Is there any difference or advantage to using this.isSimulation within a Meteor method over Meteor.isClient and Meteor.isServer?

1 Like

Funny, I was about to post this question and this old post came up in the auto topic search. :wink: Still wondering about this…

1 Like

this.isSimulation specifically identifies whether you are in a stub. In many cases this could be achieved with Meteor.isClient, but if you have server calls to server methods (think microservices, for example), then isSimulation is the one you need.

I’ve got server-only code I need to run (Stripe method calls, which will actually bomb on the client side). I’ve got this:

    if (!this.isSimulation) {
      const stripe = require('stripe')(Meteor.settings.stripeSecretKey);
      const stripeUpdateCustomer = Meteor.wrapAsync(stripe.customers.update,
        stripe.customers);

But I wasn’t sure if doing if (Meteor.isServer) was the same exact thing or not.

Same question here!

Was looking at the docs/guide that suggested putting code that needs to stay secure under !isSimulation rather than isServer.

Does that mean that code within isSimulation does not get sent to the client at all? If so, then great - that allows us to put code that needs to stay secure under there.

If it does get sent to the client, then where exactly, in a method’s code, should private code be kept?

Any code which is made available to the client - whether by an import or due to eager loading from a shared folder (like lib/) will be visible to the client. That includes code in if (Meteor.isClient), if (Meteor.isServer) and if (!Meteor.isSimulation).

In server-only code: code imported only to the server, or eagerly loaded from a file somewhere under a server/ folder.

1 Like

Cheers for the response!

Hmm… I’ve tried that in the past, but (obvious when typing it here) run into an error because the client is unable to load the module/function in question if it is within a server folder.

So for example, @ffxsam 's example: Difference between using Meteor.isClient/isServer and this.isSimulation?

Does that code mean that the stripeSecretKey is loaded to and visible on the client?

@ffxsam is using Meteor.settings. By default, settings are only available to the server, unless they’re under a top-level public key. So (without seeing the rest of his code, or his actual settings file) I would say it’s not available (undefined) on the client.

Ah okay - so if I understand correctly and extrapolate the same logic, then the below would be legit and secure:

//../server/secretCode.js

export const secretFunction = (x, y) => {
  return secretValue
}

//../lib/methods.js
Meteor.methods({
  methodName( x, y ){
    if (!Meteor.isSimulation) {
      import { secretFunction } from './secretCode.js';
      secretFunction(x,y); //Execute secret stuff on just the server, without client knowledge of what is in secretFunction
    }
  }
})

I.e. - If a function is defined within a server folder and is imported within an isServer or !isSimulation within a method (which is available on the client) - then the body of the function code and what is happening within it is hidden from the client and can’t be manipulated/seen?

Or have I misunderstood this?

That looks like it will work. On the client, the import won’t get executed, so your secret will be safe :slight_smile:.

1 Like

Legend! Thank’s for helping me get my head round that.

1 Like

Hi Rob!

I was running into this error:

(node:24824) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

I was calling a server method from the run() section of a validated-method, without wrapping it into a if (this.isSimulation).

The error disappeared after I’ve done this:

    if (this.isSimulation) {


      // Now check if sufficient credits
      Meteor.call('checkCredits', ( error, credit ) => {

        if ( !error && credit ) {

          // Reduce credit
          Meteor.call('removeCredit');

          const coupon = Coupons.findOne({_id: couponId});

          if ( coupon ) {

            Coupons.update(couponId, { $set: { published: true }});

          }

        }
        else {

          throw new Meteor.Error('Insufficient credit!');

        }

      });

    }

Am I correct that I can call a Server method from the client stub only?

Thanks!

I’m not quite sure what you’re asking. You can call a Meteor method from anywhere. Technically, a stub is run on the client for optimistic UI, while the client/server conversation occurs (which takes longer). You could call another method from the stub, while the “outer” method is running, although it makes it hard to reason about: do you have a stub for the stub?

I suspect your original error ((node:24824) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated...) was symptomatic of something else. Without seeing the original code, I don’t know.

@robfallows

the above code throwed no error anymore, but did not return any result back to the client. I then did this:
const checkCredit = Meteor.call('checkCredits');

if (checkCredit) {
 return 'Yes Credit'
}

And now everything works fine. I think the validated-method and the (stub client/server) technique caused this. The run() part of the method runs on the client and the server. And exactly this was causing this issue I think.

I attached Code block 'WORKING' and I attached Code block 'ERROR'

CODE BLOCK WORKING:

export const publishCoupon = new ValidatedMethod({
  name: 'publishCoupon',
  validate({ couponId }) {

    const handler = new ErrorHandler();
    handler.id = couponId;

    const errors = handler.validate();

    if (errors.length) {

      throw new ValidationError(errors);

    }

    const file = Files.findOne({reference: couponId, publication: 'image'});

    if ( !file ) {

      throw new Meteor.Error('error.file_upload_required','file_upload_required');

    }

  },
  run({ couponId }) {

    const userId = Meteor.userId();

    if ( !userId ) {

      throw new Meteor.Error('Authorized users only');

    }

    const credits = Meteor.call('checkCredits');

    console.log('credits: ' + credits);

    if (credits) {

      // Reduce credit
      Meteor.call('removeCredit');

      const coupon = Coupons.findOne({_id: couponId});

      if ( coupon ) {

        //Coupons.update(couponId, { $set: { published: true }});

      }

    }
    else {

      throw new Meteor.Error('error.insufficient_credits','insufficient_credits');

    }

  }
});

CODE BLOCK ERROR:

export const publishCoupon = new ValidatedMethod({
  name: 'publishCoupon',
  validate({ couponId }) {

    const handler = new ErrorHandler();
    handler.id = couponId;

    const errors = handler.validate();

    if (errors.length) {

      throw new ValidationError(errors);

    }

    const file = Files.findOne({reference: couponId, publication: 'image'});

    if ( !file ) {

      throw new Meteor.Error('error.file_upload_required','file_upload_required');

    }

  },
  run({ couponId }) {

    const userId = Meteor.userId();

    if ( !userId ) {

      throw new Meteor.Error('Authorized users only');

    }

    // Now check if sufficient credits
      Meteor.call('checkCredits', ( error, credit ) => {

        if ( !error && credit ) {

          // Reduce credit
          Meteor.call('removeCredit');

          const coupon = Coupons.findOne({_id: couponId});

          if ( coupon ) {

            Coupons.update(couponId, { $set: { published: true }});

          }

        }
        else {

          throw new Meteor.Error('Insufficient credit!');

        }

      });
  }
});

I think the difference here is the callback part ( error, credit )

Calling the method into a const like
const credits = Meteor.call(‘checkCredits’);

changed the behavior of the method.

Anyway now everthing is working as expected, and I can understand that It is quite hard to investigate into some weird code fragments.

You should understand that ValidatedMethod executes the run method on the client and on the server. In this case, you call Meteor methods in run, so these will be called from the client and from the server, which is unnecessary here:

If you are interested in providing optimistic UI, the client code should run much faster than a client call to a server method. It should be supplying an expected, or simulated response ahead of the actual reply from the server, which may be several hundred ms later. However, you are doubling up the method calls (for each call in the run you are running it from the client to the server and from the server to the server).

Correct. You have used const credits = Meteor.call(‘checkCredits’); on the client, which returns undefined (unless you have a checkCredits stub on the client). On the server, when that’s run, you’re allowed to use the no-callback form of Meteor.call, so it works.

1 Like
@robfallows

If you are interested in providing optimistic UI, the client code should run much faster than a client call to a server method. It should be supplying an expected, or simulated response ahead of the actual reply from the server, which may be several hundred ms later. However, you are doubling up the method calls (for each call in the run you are running it from the client to the server and from the server to the server).

Could you please provide an correct example of that code, that shows the correct use?

I don’t think you want optimistic UI. It would be misleading to simulate a successful credit transaction which is then rolled back.

It would be preferable to write the code in such a way that doesn’t cause method call duplication and just use a spinner while the transaction is in flight.

1 Like

Ok So I do not use the validated-method, instead I will provide a regular Meteor.call() in the client code itself.

How about this bit of code, there is no Method call to the server in the run() block. Would that be OK to use in order to use optimistic ui feature?

export const insertTeamMember = new ValidatedMethod({
  name: 'insertTeamMember',
  validate({ email, branchId }) {

    const doc = {
      email,
      branchId
    };

    const errors = TeamValidation(doc);


    if (errors.length) {      
      throw new ValidationError(errors);
    }

  },
  run({ email, branchId }) {

    const userId = Meteor.userId();

    if (!userId) {
      throw new Meteor.Error('not-authorized');
    }

    const company = Company.findOne({userId: userId});

    if (!company) {
      throw new Meteor.Error('only registered companies can add team members');
    }

    const member = {
      email,
      company,
      branchId,
      userId: userId,
      token: Random.hexString(32),
      joined: false,
      joinedAt: '',
      joinedUid: '',
      joinedUsername: '',
      invitedAt: new Date()
    };

    console.log(member);

    return Team.insert(member);

  }
});