meteor / meteor

Meteor, the JavaScript App Platform
https://meteor.com
Other
44.4k stars 5.19k forks source link

Calling (sync) Meteor.call of a asnyc Meteor.methods Method on the Client still returns a Promise #13326

Closed Marcel-Tronco closed 1 month ago

Marcel-Tronco commented 2 months ago

This is happening in Meteor 3.0.2 in a Debian Bookworm Docker container.

As the documentation points out we can call a async method of Meteor.methods on the client synchronously with Meteor.call. The documentation points out to use callAsnyc to get a promise and call to block the results synchronously.

So if we have some method like this:

Meteor.methods({
  async function myFetcher() {
    const res = await fetch(<some url with a json response>)
    return await res.json()
  }
})

We'd expect to call it in the client like this:

const someClientFunction = () => {
  const myJsonResult = Meteor.call('myFetcher')
  // do something with the json result...
}

But instead the Json-Result still is a promise. I worked around it using it like this:

const asyncClientFunction = async () => {
  const myAwaitetJsonResult = await Meteor.call('myFetcher')
}

The documentation says to sync calls:

If you do not pass a callback on the server, the method invocation will block until the method is complete. It will eventually return the return value of the method, or it will throw an exception if the method threw an exception. (Possibly mapped to 500 Server Error if the exception happened remotely and it was not a Meteor.Error exception.)

That might either mean for a async method that the promise is returned or that the server waits until the promise is resolved.

So I can't determine whether this is a bug or not. But if it's not I'd appreciate some clarification on callAsync and call distinction, especially as you have an async method yourself in the docs for the methods.

denihs commented 1 month ago

Hi @Marcel-Tronco,

There is a difference here between what the async stub is and what the async invocation is.

Let's say you have this method:

Meteor.methods({
  syncMethod() {
    const result = 2;
    console.log(result)
    return result;
  }
})

Here are the two ways you can call it with Meteor.call():

// Case 1:

console.log(1)
Meteor.call("syncMethod")
console.log(3)

// Results:
// 1
// 2
// 3

// Case 2:

console.log(1)
Meteor.call("syncMethod", (err, res) => console.log("invocation result ", res))
console.log(3)

// Results
// 1
// 2
// 3
// invocation result 2

For the example above, the second invocation is "async", but your method ("stubs") is sync.

That's what the docs is saying in the paragraph you sent here.

The doc also specifies that Meteor.call must be used to call sync methods: "This is how to invoke a method with a sync stub."

And we also have this warning there:

image

However, we can use Meteor.call() to invoke an async method, as you're doing, but you'll get a promise because you're invoking an async method.

So it's not a bug. Meteor will not solve async methods and return the value. You need to await for it.

Marcel-Tronco commented 1 month ago

Thanks for the explanation. That's what I figured. In the example above I only defined those methods on the server by the way, so I didn't define a client stub. The concept of the stub is a little opaque atm. It's only used at several points, but there's not too much explanation.

This is just to give you an idea what appeared unclear to me as a user:

How are stubs defined? The docs say every client invocation of a server method defines one, but that i can define one myself too. But then again, is there a situation where there would be no client stub? On some explanatory StackOverflow question stub is used for both server methods and client simulations of them. What's the difference between calling .call (without a callback) and .callAsync other than .callAsync always returning a promise?

I suppose there's good reason for you to do this (probably enabling async handling in client/server comunications), but as a user I'd appreciate to know about the differences between .call .callAsync to not stumble into surprises.

denihs commented 1 month ago

In Meteor, stubs are client-side simulations of server methods that make the app feel more responsive. They're only created if the method is defined on both the client and server.

This is what makes optimist-ui possible on Meteor. At this point, Meteor uses Minimongo to store the data on the client side and do its "magic".

Meteor.call without a callback is synchronous on the server but always asynchronous on the client, while Meteor.call with a callback and Meteor.callAsync (which returns a Promise) are always asynchronous.

is there a situation where there would be no client stub?

Yes. Stubs are created when your method calls a MongoDB write function (IF this method is declared in the client). For example:

//client
Meteor.methods({
  "task.save"(task) {
    return TaskCollection.insertAsync(task)
  }
})

When you call task.save (it doesn't matter if it's with .call or .callAsync, Meteor will consider this invocation a stub because it needs to simulate an insert in the client.

Now, if your method does not call anything MongoDB related, you don't have a stub:

Meteor.methods({
  "getInfo"() {
    // fetch data in some external API
    return fetch(/**/)
  }
})

I hope this helps you.

For now, I'm closing this issue (as it isn't a bug), but if you have more questions, feel free to leave them below 🙌