New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Async Methods with Callback #12677
Async Methods with Callback #12677
Conversation
- fix accounts- and passwords- tests
- bring back the callback for removeAsync
Have the callbacks back seems the best solution IMO. So we keep the same behavior as today. |
This will save us from a lot of work. This is one of the major blockers that we are facing with migrating to 3.0 |
Callbacks surely looks cleaner and more aligned with the current approach. |
I think for DX we should stick to the general standards in NODE with either await or callbacks. Coming from callbacks in Meteor feels natural to consider callbacks the "right" approach. I'd personally prefer to know that Meteor remains within the current and future standards of NodeJS and Javascript. |
Agree with @paulincai. Feels kinda weird to mix await and callbacks. What were the downsides to the approach of passing in a flag? Were there other approaches considered? It would also be nice to see an example where both client side errors and server side errors are caught. |
@jamauro I may not be the best person to answer this, but IMO the main downside of passing the flag is that you loose the option to await the client side return and listen do the backend return at the same time, so you be locked to one or another. |
I also have to agree with @paulincai and @jamauro - I personally would go for different methods. Something like (on the CLIENT): // default: call async function and await until there is a response from the server:
const actualId = await Collection.insertAsync(doc);
// optional: call async function on the client, but NOT await the actual response from the server, but work with "optimistic response":
const stubId = await Collection.insertAsyncOptimistic(doc); I know, not perfect, but in my (honest, but subjective) opinion the best solution. This
|
I think @rodrigok pretty much summarized here. @Twisterking idea isn't bad, we actually thought about having more methods. But the problem with that is that you have more methods hehe. And having more methods means more documentation and more things for newcomers to understand. Besides, another advantage of keeping callbacks is the migration from the old version. For a code like this: const stubId = Collection.insert(doc, function (err, result) {
// do something with the server result
}) All you have to do is this: const stubId = await Collection.insertAsync(doc, function (err, result) {
// do something with the server result
}) Without callbacks, you need to use I know it's not ideal. It looks like we're running from the way promises should be. But we need to remember that in Meteor, these callbacks are side effects. You use them to make sure everything worked out in the server, and get some data from there. It's different from In a lot of cases, we don't want to wait for the server, but if something wrong happens there we want to revert the changes in the client. That's where, as I see, callbacks shine. |
@denihs I am aware of the drawbacks. At the same time my thinking is the following: Meteor 3.0 IS, by definition, a very major release and will have quite a number of breaking changes. ... one of the reasons why this is version 3! ;) --> in my book, at some point, you need to be "brave enough" to make the proper change to Promises. so yes, you are right: You then need to work with I am okay with your solution - to be perfectly honest, we at Orderlion will never use the callbacks and But I stand by my point that this solution seems a bit "half assed" to me and that you need to stand behind your (very good!) decision of now using Promises / async / await everywhere. |
I may not be in position to answer this, but I also think "the migration from the old version is easier" is a kinda weak argument, in the end it is about bringing Meteor forward and having the best DX. But the option to get the client AND server response is definitely an advantage and nice to have. Maybe an alternative solution could be to still give priority to the server response (which is, if you do not heavily rely on the realtime aspect of meteor probably more often used than the client return) by returning it as a promise, but still also have callbacks for the server/client returns. const stubId = await Collection.insertAsync(doc, {
onServerData: (result) => console.log('server result: ', result),
onClientData: (result) => console.log('client result: ', result),
onError: (error) => console.error('error: ', error)
}); Another consideration is that there are a lot of popular and awesome async state management solutions out there such as react-query and SWR which could be used a lot easier with Meteor if it would properly support await when returning the server response. And in my opinion the reality of WebDev is that for most usecases realtime is just not needed. |
This is definitely the main issue. Breaking this will break all apps using optimistic UI. To those who want to use a Promise, or a Promise with the flag on what to return, Meteor can easily provide a very simple wrapper that converts the callback version to a promise. Everybody wins. Yes, that results to more methods / documentation. But those can be grouped and informing the newcomer that the default is the Promise version. But if you want to use optimistic ui, you can use the callback version |
I use Optimistic UI heavily and think it's a great feature of Meteor. Just wondering if mixing async/ await with callbacks is the best approach. I haven't really seen that approach elsewhere. For my own edification, what would be the downsides to an approach like this: const {stubPromise, serverPromise} = insertAsync(doc)
const stubId = await stubPromise
// take action with the stubId
const result = await serverPromise.catch(error => {
// rollback changes as needed
}) |
That looks like a very clean approach, @jamauro. Maybe we can further simplify it into something like:
|
For For import { createProject } from '/methods/projects';
// continues returning a promise for the server result
let serverResult = await createProject();
// The returned promise has two additional properties with promises
// that can be used instead
let { stubPromise, serverPromise } = createProject();
let stubResult = await stubPromise;
let serverResult = await serverPromise; There are also some discussions from 2015 around a promise api that might provide some inspiration: #5005 and #4939. |
Is the idea to get rid of automated optimistic UI? What did not work with the implicit "Simulate on the client, reconcile on server response" approach? Why does it need to be explicit now? |
Sorry for being late, but let me throw my two cents. I completely understand where the idea of using a callback comes from, but my concern about that is it doesn't have the "meaning" bound to it. I mean, if I saw a codebase that uses a callback, I'd most likely ignore the returned value of this call. That's why I'm far more in the direction of what @zodern proposed (also elsewhere). I don't see the need for const promise = Meteor.call(...);
const serverResult = await promise;
const stubResult = await promise.stub; The benefit is that it's backward compatible, i.e., |
@make-github-pseudonymous-again
No, Optimistic UI is not going anywhere. Meteor (almost) always gave you the option to react to the "method stubs", i.e., know what was the result of the local simulation before the server responded. This discussion is only about the API for doing that. |
@radekmie the problem I see with your approach is that apps that use Minimongo will always have to worry about waiting for the right promise. For example: // This:
const stubId = Collection.insert(doc, function (err, result) {
// do something with the server result
})
// Will have become this:
const promise = Collection.insertAsync(doc)
promise.catch(function (err) {
// do something if server fails
})
const stubId = await promise.stub; So basically every call for a Minimongo (Local) method you'll have to add this This is because So, as I see, if we were to follow this approach, we'll have to invert promises for the Minimongo (Local) methods. // For callAsync
const promise = Meteor.callAsync(...);
const serverResult = await promise;
const stubResult = await promise.stub;
// For Local Mongo methods:
const promise = Collection.insertAsync(doc)
promise.server.catch(function (err) {
// do something with the server result
})
const stubId = await promise; At least this is what makes sense to me because in most cases you don't even provide a callback to Based on that, @jamauro's approach here looks cleaner to me. I would just change const { result, serverPromise } = await Coll.insertAsync(doc)
// and
const { result, serverPromise } = await Meteor.callAsync(...) |
I honestly never cared about the stubs, mostly because they "lie". Like, if I'd like to I'm really interested in what others will say here, because maybe the tens of Meteor apps I've worked on and audited were special in this area 😛 EDIT: I know that currently, Minimongo returns stub results on the client. And I also know that it caused a lot of problems in the projects I worked on. |
I think we have different discussions in the same mix and it is going to be hard to reach one conclusion:
Breaking down into different things we can reach different conclusions for each case and maybe find a good trade-off. What IMO would be the best trade-off:
Hmmmm, and about beginners/newcomers? Maybe we could also have two options for Methods, one very simple (ignoring stub) and another with these two promises. I don't know if my answer and explanation were too vague so let me know if my suggestion is not clear enough and I can expand on that but I think these three proposals (four with the beginner one) would address all the concerns in this PR. The bad part of this trade-off: more code to be maintained. 🤷 |
IMO the priority with an API should be to be as simple as possible while making sure its clear what is happening with everything. I would be fine with this solution, but the problem I have is as a developer its not immediately clear whats going to happen with a callback and a promise. For instance if you do:
Is that going to be the server response the stub? What if you don't have a stub defined and you have a callback? Its leaving a lot open for interpretation, which means more documentation, and confusion. I like @radekmie's solution with the separate promise to listen to while not complicating the base API if you are doing something simple. To go forward with this proposed API I would suggest having to explicitly name the callback, so you are completely clear what you are getting:
I think stubs are things you are specially handling, and needing to be explicit is worth the added functionality. |
I like the approach @zodern outlined above. I put together a quick REPL to play around with it. Check it out Here's what I like about it:
const result = await Meteor.callAsync(...);
const { stubPromise, serverPromise } = Meteor.callAsync(...);
const stubResult = await stubPromise;
const serverResult = await serverPromise;
Regarding:
This is probably a separate discussion but this feels like an opportunity to change Curious to hear what y'all think. |
@jamauro looking at the REPL you created I think it looks really good! Also addresses most of what @filipenevola mentioned here. I'm curious to hear what the others think too. |
Not all stub exceptions should prevent server from running, I think. The easiest example is using
(Emphasis mine.) I think we're mixing two things here. Sure, My point is basically what @ToyboxZach wrote:
|
Let me attempt to clarify a couple things:
Let's assume everyone likes the approach and it's implemented. Then my thoughts were, what else can be simplified / improved? For example, it might be nice if Meteor.callAsync was changed to act more like const { stubPromise, serverPromise } = Meteor.callAsync(...);
const stubResult = await stubPromise;
const serverResult = await serverPromise; the stubResult isn't undefined, and the engineer doesn't need to wade through the docs to discover they need to use Meteor.applyAsync with the option |
@radekmie My question is why is a change in the API required at all? All we are doing is going from sync to async, why is there a need to change how to specify which of the real or stub value is of interest? Is it because they run concurrently and each of them could finish first and we do not want to wait for the other? Or is it a new feature to be able to act on both values explicitly, something that was not possible before? I would personally prefer |
|
@make-github-pseudonymous-again Sorry for answering so late, I missed your message 😐
I think it's more about how both of them are accessible, not which one is where. And the fact that the result should be awaitable now allows us to improve the ergonomics of this API.
This is not possible without Fibers, at least as long as
In short: it's all about the complexity. To make Minimongo "async-native", we have to implement async methods. To do that, we can either wrap the sync ones in |
Note that you can await non-promises, so you can await the existing API out-of-the-box. Having them typed as |
I meant both |
So my question is: What is the current accessibility? |
What is the current state of this discussion here? My opinion still stands, I would opt for something like this as suggested by @zodern and @radekmie : const promise = Meteor.call(...);
const serverResult = await promise;
const stubResult = await promise.stub; All other solutions seem "dirty" to me and not really following the standards out there. In 99% of cases you want to if you really want to opt for the other approach, I would personally at least go for "array like destructing" like e.g. React's const [ result, serverPromise ] = await Coll.insertAsync(doc)
// and
const [ result, serverPromise ] = await Meteor.callAsync(...) |
Hi, i've ran into this too right now... With another simple thing: Calling a method with a stub using callAsync:
then the await yields... if another piece of the code then runs and tries to set a timer somewhere: Meteor.setTimeout(() => {
sayHi();
}, 1000); You get an error & _an exception which breaks our code saying: "Can't set timers inside simulations" (thrown here: meteor/packages/meteor/timers.js Line 38 in de88454
Which is not good, but it's manageable if you just use window.setTimeout if you know what you're doing because you're not inside a stub / method, right? Ah, one funky thing is: We could maybe actually allow timers now using promises and/or async/await in method stubs for async methods, right? 😄 But another, more troublesome thing is: The timer thing looks like it could be mitigated easily, but that also means that If an async stub gets awaited, and we're not entirely synchronising all code across our entire codebase in the client, then another method could indeed get called in another part of the frontend without us having much control besides manually synchronizing using reactive vars or some other mechanism? 😯 Because... BLAZEJS in the template lifecycle events isn't async-aware - and even if you make your So you can't rely on anything being synchronized outside your templates lifecycle methods' boundary. Which is bad in my view. Very bad. So this PR: Add firstRunPromise to Tracker.computation objects might be back in the race... to be able to await the first run of a computation at least... This might be not so bad for react or other frameworks, but for Blaze this is bad. The Stub part is a cool feature for instant reactions in the client and a part of Meteors' original story. We rely on it for timely updates in our UI. Nothing that couldn't be undone / skipped over / mitigated, but it just makes life less nice. I think it'd be very good to find a solution besides "ah make sure yourself that no two One thing I don't get or what might be an idea (?): Why does the stub-or-not - check have to be done depending on some environment setting on another? For regular client -> server calls it's pretty obvious that if it's in the client, it's the stub, and if it's running on the server it's the real deal, no? :D Maybe we could choose a "default interface" for this regular use case, and if somebody really needs stubs to run for his server to server calls, the he could use an advanced interface somehow? |
We're solving this issue on this PR. |
What we're doing here is testing the idea of having callbacks for the new async methods:
callAsync
,insertAsync
,updateAsync
, etc...The reason for this is that right now we rely on flags to decide when to wait or not for the server result.
To give an example, if you were to insert something in the DB today you can do something like this:
What's happening in the example above is that you can get an id of something you inserted in the Minimongo, but you can also provide a callback to the insert function to make sure everything worked out in the server.
Now, with the async API, we're working with promises. So if you just do something like:
You will get the result generated in the server. But maybe you don't want to wait that much. Maybe you just want the result from the client.
In that case you can provide a flag:
With callback, it would look something like this:
Which looks more like what we have today.
Important to notice
I gave examples here with
Collection.insertAsync
, but this has nothing to do with Mongo or Minimongo.All this is about
client
andserver
on Meteor.The examples above are the same if you change
Collection.insert
byMeteor.call
andCollection.insertAsync
byMeteor.callAsync
.Sometimes you need to wait for the server, but if you're working with real-time and need to rely on the optimistic UI, you don't want to wait for the whole trip to the server.
With callbacks, it's easier to understand and separate what's client and server.
Also, keeping the callbacks would make it easier to migrate from the older version.
To run the test on this branch: