Data: Await resolver implementing isFulfilled#70806
Conversation
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
@johnhooks, do you mind rebasing the branch on top of the latest trunk? @jsnajdr, IIRC you worked recently on improving this area of the code. Side note: gutenberg/packages/data/src/test/registry.js Line 294 in e043709 |
…ced tests for isFulfilled and async promise hygieneAdd advanced tests for isFulfilled and async promise hygieneUpdate registry.js Add comprehensive test suite for isFulfilled edge cases and async promise hygiene to registry.js. Includes tests for: - Non-existent selector handling - Async promise cleanup and resolution - Concurrent async selectors with parameters - Promise rejection handling - Memory leak prevention for promise references References: PR WordPress#70806, addresses bug WordPress#70805 These tests ensure proper cleanup of async promise chains and prevent memory leaks in selector resolution.
29dd42b to
835585a
Compare
|
@Mamaduka I've updated the branch, colocated the tests, and added a few more after reviewing the other tests in |
7dce472 to
3c27e4c
Compare
| store.dispatch( | ||
| metadataActions.finishResolution( selectorName, args ) | ||
| ); | ||
| }, 0 ); |
There was a problem hiding this comment.
This could be neatly integrated into the main setTimeout callback several lines below. Just don't run the fulfill function when isFulfilled returns true:
const isFulfilled =
typeof resolver.isFulfilled === 'function' &&
resolver.isFulfilled( state, ...args );
if ( ! isFulfilled ) {
const action = resolver.fulfill( ...args );
if ( action ) {
await store.dispatch( action );
}
}It's the same thing, without code duplication, with just one difference: both startResolution and finishResolution are called, right after each other without any waiting. But that's fine, it's even welcome, because outside observers will see the same sequence of updates, no matter how the selector/resolver is internally implemented.
Another way to fix this is in the mapResolveSelector and mapSuspendSelector functions. These functions could perform the isFulfilled check and return a result right away. In that case, the startResolution/finishResolution actions would never be called when isFulfilled is true. But it's probably a worse solution than the first one, again for consistency reasons: sometimes, for the same selector, start/finishResolution would be called, sometimes not.
There was a problem hiding this comment.
I agree with calling startResolution/finishResolution and being consistent. I'll wrap the call to isFulfilled into the main setTimeout. Thank you for the review.
3c27e4c to
8958d1b
Compare
|
|
||
| const promise = registry.resolveSelect( 'demo' ).getData(); | ||
| jest.runAllTimers(); | ||
| await expect( promise ).rejects.toThrow( 'isFulfilled error' ); |
There was a problem hiding this comment.
@jsnajdr this is odd. The error is caught and the state of the resolver transitions to failed, but Jest fails the test without this line. I'm still researching the issue.
There was a problem hiding this comment.
I've walked through with the debugger, wow there is a lot going on for a single resolution. Though after running jest.runAllTimers, promise has a rejected status and Error: isFulfilled error, even though it was caught in the try/catch and included in the FAIL_RESOLUTION action.
There was a problem hiding this comment.
OH I got it, mapResolveSelector rejects it. So the test is correct, if this is the desired behavior.
Should the resolver resolution fail if isFulfilled throws an error?
There was a problem hiding this comment.
Yes, resolveSelect detects the error and returns it.
If isFulfilled throws, that is certainly suprising and unexpected and the error should be surfaced. So yes, the resolution should fail.
| return; | ||
| } | ||
|
|
||
| const state = store.getState(); |
There was a problem hiding this comment.
This could be inlined into the isFulfilled call, we don't use the state anywhere else do we?
resolver.isFulfilled( store.getState(), ...args )| } ); | ||
|
|
||
| const promise1 = registry.resolveSelect( 'demo' ).getPage( 1 ); | ||
| jest.runAllTimers(); |
There was a problem hiding this comment.
For the resolveSelect tests, we would be better off if we used jest.useRealTimers(). Then you don't need to run timers manually.
It will happen automatically if you move these tests to redux-store/test/index.js, where all other resolveSelect tests are. This registry.js suite has jest.useFakeTimers() at the start.
|
|
||
| const promise = registry.resolveSelect( 'demo' ).getData(); | ||
| jest.runAllTimers(); | ||
| await expect( promise ).rejects.toThrow( 'isFulfilled error' ); |
There was a problem hiding this comment.
Yes, resolveSelect detects the error and returns it.
If isFulfilled throws, that is certainly suprising and unexpected and the error should be surfaced. So yes, the resolution should fail.
Add a test to the `@wordpress/data` package to demostrate the deadlock of awaiting a resolver implementing `isFulfilled`. See WordPress#70805.
Reduces duplication by handling isFulfilled in the same code path as fulfill. Both paths now mark the resolver as running first, and isFulfilled is now wrapped in try/catch for error handling.
8958d1b to
15b78e3
Compare
jsnajdr
left a comment
There was a problem hiding this comment.
Very nice, thanks for working on this and addressing all feedback 👍
|
@jsnajdr absolutely, I appreciate you taking the time to help me through it. Also shout out to @TimothyBJacobs for suggesting I create the PR. |
What?
Add dispatch of
finishResolutionwhen a resolver'sisFulfilledcallback is true in the@wordpress/datapackage.Fixes #70805.
Why?
A deadlock issue was discovered in the
@wordpress/datapackage when usingresolveSelecton a selector/resolver pair implementingisFulfilled.This failed test run from before adding the fix demonstrates the issue.
How?
The concept is to dispatch
finishResolutionto the metadata store whenresolver.isFulfilledistrue. This adds the correct state to the store, allowing resolution of the promises inmapResolveSelectorandmapSuspendSelector.