-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
[v5] Resumable promise logic #4190
base: main
Are you sure you want to change the base?
Conversation
|
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 637446e:
|
if (state.steps?.[name]) { | ||
return state.steps[name][1]; | ||
} | ||
|
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.
Could the step be visualized as "pending" when something like a $$xstate.step.pending
event would be sent?
I really like this idea, but I think it's going to be very easy to forget to use |
could we remove first parameter of step? how do you think about using reference check of promise as React's useThenable? |
You have the same problem with generators, since it's possible to make a The important thing to remember here is that this is an opt-in feature. The default behavior is that the promise (or parts of the promise) will re-run. So it will still work, but this may be undesirable. However, I'd argue that it's impractical to prevent every possible way that a developer may forget to do something or do things wrongly, outside of providing lint rules. IMO if strict guardrails are desired, using a state machine would be the way to go. |
If we remove the first parameter, then 1) we don't have explicit names for steps, which makes it harder to visualize, and 2) similar to React hooks, we're now dependent on the order in which these steps get called, which limits the ability to call these within e.g. loops or if-statements. Reference checking wouldn't work when restoring state (e.g. from localstorage or a DB). |
Maybe function step(promiseCreator: () => Promise<any>, nameOverride?: string) {
let name = promiseCreator.name;
if (nameOverride) name = nameOverride;
if(!name) {
if (isDevelopment) {
console.warn('...')
}
return promiseCreator();
}
if (state.steps?.[name]) {
return state.steps[name][1];
}
...
} Usage: const fetchUser = () => fetch(url);
step(fetchUser); // => name = fetchUser
step(fetchUser, 'user'); // => name = user
step(() => fetch(url), 'user'); // => name = user
step(() => fetch(url)); // => no name, just returns the promise and warns in dev mode |
I guess depending 'name' property of constructor or instance could make confusion for production as names are mingled formally? |
That might indeed be problematic for visualization. |
This PR introduces a
step(...)
wrapper to persist intermediate state in promises.The
step(name, promiseFn)
function executes the promise created by thepromiseFn
the first time, and persists its result to the actor's internal state.The promise actor's state can then be persisted (
actor.getPersistedState()
) and restored normally (createActor(logic, { state })
) and the logic will essentially "resume" at the latest incompletestep(...)
. The way this works is by using persisted values for previous steps; if there is a persisted value, no need to run the promise again.Motivation
This would be completely opt-in - you can create promise logic as normal. Calling async functions in a named
step(name, promiseFn)
gives you a couple benefits: