-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
[Try] Async stan #27155
[Try] Async stan #27155
Conversation
@@ -182,10 +156,10 @@ export const createDerivedAtom = ( resolver, updater = noop, config = {} ) => ( | |||
); | |||
}, | |||
resolve, | |||
subscribe( listener ) { | |||
async subscribe( listener ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unsubscribe callback should be available right away so maybe this should return synchronously with an array of promise and unsubscribe callback?
expect( registry.get( count ) ).toEqual( 2 ); | ||
expect( await registry.get( count ) ).toEqual( 1 ); | ||
await registry.set( count, 2 ); | ||
expect( await registry.get( count ) ).toEqual( 2 ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need all the async/wait for the getters for sync atoms and subscriptions? Note that even if the behavior is sync, JS will make it async and only resolve on the next tick and this breaks the editor (and useSelect) in various ways. It is important that sync selectors stay sync and resolve right away.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, I’ll think some more about this on Monday. It seems like Recoil separates atoms and selectors, and only the latter may be async - maybe that would be a better way to go, our API is already pretty similar. I wonder how it handles errors thrown on the intersection of the two though, like propagation of the atom value to async selectors
Maybe we can have an "error" property per atom, like we have |
Alternatively, it can be just an internal value and if it's set we throw on |
I think that would break stack traces similarly to how rungen does |
Yes, making everything async should not be needed and it would introduce its own set of problems. There are use cases that are completely synchronous, like flipping a toggle state in memory, others are asynchronous. The library needs to support both seamlessly.
I don't understand why switching between sync and async should change anything about error handling. Sync functions return values or throw exceptions. Async functions return promises of values or rejected promises with errors (and never throw exceptions). There is a complete 1:1 symmetry between them. I think the system described below should work. It distinguishes clearly between sync and async operations, reports special states as exceptions, and should never swallow any error.
It's important here that
The
|
Agreed, it seems like making everything asynchronous is not the right way to go here.
What I meant is that a synchronous function doesn't have any way of throwing the error as soon as it happens – so the same problem you meant to address with the synchronous
I was trying to avoid that, but yes, there may be no other way here. I just don't like how it's unclear what's the right stack trace in this scenario. Should it stem from the first I also wonder how would the errors propagate in that model, e.g. what should happen in the following scenario: Should derived atoms 2, 3, 4 and 5 switch to an error state or just remain unresolved forever? Would we throw the same error each time |
I'm not sure I understand here: are you concerned mainly about the stack trace of the error? It's true that the "resolver threw an error" and " The |
@jsnajdr TIL about |
I think a good and consistent mental model is: the selectors are just like function calls, where the functions are called only once and the result is memoized (both returned value and thrown exception). In your example: yes, derived atom 1 is resolved with error and stays in that state forever. On the first call of It's the same with derived atoms 2, 3, and 4. Their resolvers also fail, this time because the resolvers call If you want to "unblock" the chain and retry the resolution, you do that basically by invalidating the memoization cache or setting some caching policy on it, other than "remember forever". We are getting very close to what a The derived atom can also have a policy to retry the resolution several times before finally reporting the error upstream. The Calypso data layer, implemented by @dmsnell , has that feature. There is a large universe of options on how to precisely configure the resolvers and their cache. |
The good news is that we probably don't need to wait for anyone to implement or start supporting the proposal. I don't see a reason why we couldn't start setting |
#26866 got reverted so I believe we should close this PR as it depends on the code that is not used. |
Description
For now this PR won't even work - it serves as a discussion prompt. I didn't bother to update types or consumers of this API as I'd like to talk it out first.
#26866 introduced a new state management system called
stan
. As discussed in #26866 (comment), there are some rough edges related to asynchronicity. For example, resolution rejection of dependent derived atoms are suppressed and never bubble up:I was pondering what's the right way of approaching this problem and concluded that as soon as there are promises in the mix, everything handling these promises should also be "promisified". This PR is an experiment in which everything in
stan
becomesasync
.Usage would change from this:
to this:
Ubiquitious
await
is an obvious downside. Also in this implementation it's even more important to cancel resolution when a "concurrent" update arrives - I thinkrx.js
could make this part very easy but I abstained from introducing a new dependency for now.Upsides are that all the errors are now catchable as they'll bubble up the promise chain. We also no longer need any heuristics to reason about exceptions (
if ( unresolved.length === 0 ) { throw e; }
). Also, should we decide to switch to any other way of handling the state internally, this API opens many doors that are not trivially opened with synchronousget
.cc @jsnajdr @youknowriad @gziolo