-
Notifications
You must be signed in to change notification settings - Fork 637
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
[css-view-transitions-1] Fix some promise timing, and avoid duplicate unhandled rejections #8454
Conversation
@@ -897,7 +897,7 @@ urlPrefix: https://html.spec.whatwg.org/multipage/rendering.html; type: dfn; | |||
{{UpdateCallback|updateCallback}} is called asynchronously, once the current state of the document is captured. | |||
Then, when the promise returned by {{UpdateCallback|updateCallback}} fulfills, the new state of the document is captured. | |||
|
|||
{{UpdateCallback|updateCallback}} is _always_ called, even if the transition cannot happen (e.g. due to duplicate `view-transition-name` values). | |||
{{UpdateCallback|updateCallback}} is *always* called, even if the transition cannot happen (e.g. due to duplicate `view-transition-name` values). |
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.
Seems like bikeshed's markdown parser doesn't support emphasis with _
. Just a formatting fix.
1. Let |callbackPromise| be null. | ||
|
||
* If |transition|'s [=ViewTransition/update callback=] is null, then resolve |callbackPromise|. | ||
1. If |transition|'s [=ViewTransition/update callback=] is null, | ||
then set |callbackPromise| to [=a promise resolved with=] undefined, | ||
in |transition|'s [=relevant Realm=]. | ||
|
||
* Otherwise, let |callbackPromise| be the result of [=/invoking=] |transition|'s [=ViewTransition/update callback=]. | ||
1. Otherwise, set |callbackPromise| to the result of [=/invoking=] |transition|'s [=ViewTransition/update callback=]. |
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.
No normative change here, just using more standard spec prose.
css-view-transitions-1/Overview.bs
Outdated
1. [=Resolve=] |transition|'s [=ViewTransition/update callback done promise=] | ||
with the result of [=reacting=] to |callbackPromise|: | ||
|
||
* If |callbackPromise| was rejected with reason |r|, then [=reject=] |transition|'s [=ViewTransition/update callback done promise=] with |r|. | ||
- If the promise was fulfilled, then return undefined. |
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.
This is technically a normative change, but it doesn't change any currently observable behaviour. It protects us from possible race conditions in future.
css-view-transitions-1/Overview.bs
Outdated
then [=resolve=] |transition|'s [=ViewTransition/finished promise=]. | ||
1. [=Resolve=] |transition|'s [=ViewTransition/finished promise=] with the result of [=reacting=] to |transition|'s [=ViewTransition/update callback done promise=]: | ||
|
||
* If |transition|'s [=ViewTransition/update callback done promise=] was rejected with reason |reason|, | ||
then [=reject=] |transition|'s [=ViewTransition/finished promise=] with |reason|. | ||
- If the promise was fulfilled, then return undefined. |
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.
Again, technically a normative change, but no observable behaviour change.
1. [=Skip the view transition=] |transition| with |reason|. | ||
|
||
* If the promise was resolved, then: | ||
* If the promise was fulfilled, then: |
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.
[=marked as handled=]. | ||
|
||
Note: This is [=marked as handled=] to prevent duplicate {{unhandledrejection}}s, | ||
as this promise only ever rejects along with the [=update callback done promise=]. |
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.
This would only be a duplicate if update callback done promise is not handled. If it is handled, but the finished promise is not handled, then should that still cause an unhandledrejection?
In other words, I feel like we shouldn't try too hard to prevent duplicate unhandledrejections, since these are different promises that would both be not handled if rejected
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.
Imagine code like this:
async function attemptTransition() {
const transition = document.startViewTransition(callback);
try {
await transition.updateCallbackDone;
} catch (err) {
reportBadError(err);
return;
}
try {
await transition.ready;
// Do some JS animations here
} catch (err) {
reportSkippedTransition(err);
return;
}
try {
await transition.finished;
// Do some cleanup
} catch (err) {
reportCleanupError(err);
return;
}
}
If the update callback throws, the above code will result in two identical unhandled rejections. Although the updateCallbackDone
promise is handled, the function exits, leaving ready
and finished
unhandled. That doesn't seem useful, and annoying to work around. So, my goal in this PR is to avoid unhandled rejections on ready
and finished
when the rejection would be the same as updateCallbackDone
.
Note that as soon as the developer does:
await transition.finished;
…they're creating a new unhandled promise. This PR only impacts cases where the developer doesn't do anything with the promises.
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.
I was imagining the case where updateCallbackDone is listened to by some app state monitor, to make sure that all of the proper dom modifications take place, whereas finished is listened to by some logging / metrics subsystem to just report failures. In that case, suppose that updateCallbackDone rejects and is handled appropriately by the app state monitor. However, the logging system doesn't handle it (maybe as an oversight) but it does not result in unhandled rejection because updateCallbackDone handled it.
I think my case is fairly contrived, but I'm just wary of handling rejections for promises that seem different enough.
WDYT?
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.
Yeah, I see what you're saying. But it doesn't feel that different to what would happen if both systems were listening to updateCallbackDone
, which I think it more likely in this case (finished
only ever rejects if updateCallbackDone
rejects).
- The app state handles
updateCallbackDone
. - The logging system intends to handle
updateCallbackDone
, but fails to for some code path.
There's no unhandled rejection from updateCallbackDone
, because the app state already handled it.
I'll ask Domenic for a second opinion on this though.
Tangent: I kinda think unhandled rejections in general are a bad design. They seem to be built around single consumer.
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.
Domenic likes the pattern in this PR (he did a similar thing in the navigation API) #8455 (comment).
But I'm not opposed to take it to the group internally, or CSSWG, if you want more eyes on it.
@@ -1135,9 +1143,14 @@ urlPrefix: https://html.spec.whatwg.org/multipage/rendering.html; type: dfn; | |||
|
|||
Note: This happens if |transition| was [=skip the view transition|skipped=] before this point. | |||
|
|||
1. [=Mark as handled=] |transition|'s [=ViewTransition/ready promise=]. |
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.
Likewise here, it seems that this is only a duplicate if update callback done promise is not explicitly handled? Or am I misunderstanding?
1. [=promise/React=] to |callbackPromise|: | ||
|
||
* If |callbackPromise| was resolved, then [=resolve=] |transition|'s [=ViewTransition/update callback done promise=]. | ||
1. [=Resolve=] |transition|'s [=ViewTransition/update callback done promise=] |
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.
I feel like this is an extremely subtle point which I wouldn't have known if you didn't link to the your 2014 article.
"resolve the promise with the result of reacting to callbackPromise". This does, in fact, mean reject the promise if callbackPromise was rejected, right?
I'm not sure what race conditions you're referencing below, but I think it would be better to be explicit here "reject if rejects, resolve if resolved" type of language
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.
"resolve the promise with the result of reacting to callbackPromise". This does, in fact, mean reject the promise if callbackPromise was rejected, right?
Yeah, the process of react is very similar to promise.then()
, as in, if you don't react to the reject case, then the rejection is passed through.
I'll add a note to make this clear.
The only reason the fulfilled case is handled is to ensure that updateCallbackDone
fulfills with undefined
rather than whatever the callback returns.
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.
I'm not sure what race conditions you're referencing below
We didn't have any in the spec right now, but since our intention was to have updateCallbackDone
follow callbackPromise
, except resolving with undefined
, it seems better to spec it with that intent.
Here's the difference between the two patterns: https://jsbin.com/butulov/edit?js,console
In the current spec, this will cause three unhandled rejections:
As it will cause
transition.updateCallbackDone
,transition.ready
, andtransition.finished
to reject with the same error.This PR always marks
transition.finished
as handled, as it only ever rejects for the same reason astransition.updateCallbackDone
. It markstransition.ready
as handled if it's rejected with the same reason astransition.updateCallbackDone
.