fix: handle late resolving promises with promise cancelation#864
Merged
sarahdayan merged 41 commits intonextfrom Jan 26, 2022
Merged
fix: handle late resolving promises with promise cancelation#864sarahdayan merged 41 commits intonextfrom
sarahdayan merged 41 commits intonextfrom
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 4fc760a:
|
Haroenv
reviewed
Jan 13, 2022
9e903a7 to
eaa0ce3
Compare
eaa0ce3 to
6cc5451
Compare
3ff85e0 to
1d31b0a
Compare
1d31b0a to
9ad3000
Compare
7d753b4 to
6e633fd
Compare
sarahdayan
commented
Jan 21, 2022
Contributor
francoischalifour
left a comment
There was a problem hiding this comment.
Solid solution!
We can remove some indirections to make the cancelable promise better fit the library and save some bytes.
Haroenv
reviewed
Jan 24, 2022
Haroenv
reviewed
Jan 25, 2022
93f11e1 to
c14844c
Compare
francoischalifour
approved these changes
Jan 26, 2022
Haroenv
approved these changes
Jan 26, 2022
Contributor
Haroenv
left a comment
There was a problem hiding this comment.
lgtm once the "should add be returned" and "can the internals be simplified" comments are addressed :)
Co-authored-by: François Chalifour <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This fixes an internal design flaw causing debouncing to malfunction.
TL;DR
The fix uses a custom cancelable promises implementation so we can imperatively prevent pending promises' callback from triggering. The implementation is partial and minimal to keep impact on bundle size as little as possible.
packages/autocomplete-core/dist/umd/index.production.js: 5.78 KB → 5.97 KBpackages/autocomplete-js/dist/umd/index.production.js: 16.29 KB → 16.49 KBContext
Each keystroke triggers
onInput, which fetches sources and items, then updates the state:collectionsisOpenbased on other state valuesThere can be multiple pending requests at the same time, and each request can take an unknown amount of time to settle.
Sometimes, the user can manually close the panel (clicking outside, hitting
Escape, hittingEnter) while results are being fetched. In such cases, we don't want the panel to reopen once the requests settle, even if there are results. This was fixed in #829.current.mov
On
blurorEscape, the panel closes and stays closed even when "late" promises settle.The biggest problem with this implementation is that it doesn't play well with promises than never settle.
Problems
The current design tracks running requests with a counter, then imperatively prevents updates when closing while requests are still running, and only reenables them once they're all settled. Yet in some cases, promises can not settle:
debouncePromisefunction consists of returning a promise that resolves in a timeout, and clearing it when invoking the function before the time is up.resolveorreject), it could never settle.In such cases, the request is counted as pending, but will never settle. As soon as the user would close the panel, this situation would cause updates to remain blocked. Conversely, if a request took a really long time, no matter if it was stale and newer requests had already resolved, it would "hold" updates for no reason.
failing-debounce.mov
Another flaw of this design is that it keeps the
statusloading (see above demo, the loading indicator keeps on running even though we've closed the panel).Counting running requests the way we do is flawed, and imperatively skipping and reenabling updates is error-prone.
Solution
When the user closes the panel before results are returned, we can consider they're no longer interested. In that case, it's okay not to follow through with updating the state once results are ready: even though it makes sense it the program's flow, it doesn't match the user's behavior and expectations.
In that sense, we should conceptually "drop" any pending request when the user closes the panel. In this context, "dropping" means not executing any attached handler. It's okay for requests to finish, as they will populate the search client's cache. When the user clicks the search box, the state will update with fresh results either from the cache, or wait some more until the request finishes.
Conversely, it should be okay for a promise never to settle. If the user closes the panel, a hanging promise should be garbage collected.
Implementation
The new implementation relies on cancelable promises, a thin promise wrapper around promises that expose a
cancelfunction. When invoking it, anythenorcatchhandler attached to the promise won't be called.When a request is triggered, its promise is added to a private list of pending requests on the
store. This list updates itself: whenever a promise settles or is canceled, it's removed. It also exposes acancelAllfunction, which cancels all pending promises and clears the list.When a user manually closes the panel,
cancelAllis invoked, canceling all pending promises and clearing the list. No attachedthenorcatchhandler ever triggers, meaning those requests have no impact on the UI state.This flow is easier to reason with, because it replaces the mental model of arbitrarily skipping state updates with the concept of no longer caring about requests. We no longer need to mix logic in
thenhandlers for when updates are skipped and when they aren't.We can also immediately make state updates to reflect what's going on (e.g., set the
statusback toidle, clear thelastStalledId).cancelable.mov
Debouncing
The solution works well with debouncing, and doesn't affect later state updates as the current implementation does (see #844).
debounce.mov
fixes #844