Skip to content
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

Deferred: Backwards-compatible standards interoperability #1996

Closed
wants to merge 18 commits into from

Conversation

gibson042
Copy link
Member

Ref #1821 (comment)

Maintained backwards compatibility (outside of specifications):

  • Don't process resolution values: dfd.resolve( thenable ).done( expectsThenable )
  • Multi-valued resolution: dfd.resolve( value1, value2, ... ).then( expectsMultipleArguments )
  • Progress: dfd.notify( progress ).then( null, null, expectsInvocation )
  • Contextual resolution: dfd.resolveWith( context, values ).then( expectsContext )

Necessarily-breaking changes

  • Any return value (i.e., not thrown) from onRejected handlers can fulfill a .then Deferred: dfd.reject().then( null, jQuery.noop ).done( expectsInvocation )

Potentially-breaking behavior decisions (current state of this PR in bold):

  • Thenables returned from .then onProgress handlers
    • progress: ignore vs. forward to outer onProgress vs. forward to outer onFulfilled vs. ???
    • rejection: throw (asynchronous) vs. ignore vs. forward to outer onRejected vs. ???
    • fulfillment: forward to outer onProgress vs. ignore vs. ???
  • .pipe behavior: preserve as-is vs. support non-promiseable thenables
  • .pipe support: deprecate vs. keep vs. drop
  • Testing .pipe/.then incompatibilities: prefix tests with [DIVERGENT] vs. ???
  • jQuery.when: privilege .promise (e.g., provide context) vs. privilege .then

To Do:

  • update jQuery.when
  • reduce size
  • rebase (avoiding for now to keep a stable size impact reference)
  • prototype unhandledRejections plugin
   raw     gz Sizes
252424  74450 dist/jquery.js
 85114  29662 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +5499  +1231 dist/jquery.js
  +921   +267 dist/jquery.min.js

gibson042 and others added 11 commits January 6, 2015 02:38
Specific spec and old jQuery compatibility assertions

Ref jquery#1821 (comment)
To run tests, invoke `grunt default promises-aplus-tests`.

Conflicts:
	Gruntfile.js
	package.json
	src/ajax.js
   raw     gz Sizes
249270  73674 dist/jquery.js
 84800  29540 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +2345   +455 dist/jquery.js
  +607   +145 dist/jquery.min.js
Non-thenable return from a rejection handler fulfills instead of rejects.

Deferreds cannot resolve from progress handlers.
Passes all jQuery QUnit tests.

   raw     gz Sizes
249550  73734 dist/jquery.js
 84891  29568 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +2625   +515 dist/jquery.js
  +698   +173 dist/jquery.min.js

   raw     gz Compared to last run
  +280    +59 dist/jquery.js
   +91    +28 dist/jquery.min.js
Reject promise self-resolution loops.

   raw     gz Sizes
249741  73808 dist/jquery.js
 84930  29583 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +2816   +589 dist/jquery.js
  +737   +188 dist/jquery.min.js

   raw     gz Compared to last run
  +191    +74 dist/jquery.js
   +39    +15 dist/jquery.min.js
Retrieve `then` only once per fulfillment value.

   raw     gz Sizes
249976  73882 dist/jquery.js
 84938  29589 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +3051   +663 dist/jquery.js
  +745   +194 dist/jquery.min.js

   raw     gz Compared to last run
  +235    +74 dist/jquery.js
    +8     +6 dist/jquery.min.js
Skip setTimeout for re-resolution so synchronous errors don't interrupt it.

   raw     gz Sizes
250371  73993 dist/jquery.js
 84968  29606 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +3446   +774 dist/jquery.js
  +775   +211 dist/jquery.min.js

   raw     gz Compared to last run
  +395   +111 dist/jquery.js
   +30    +17 dist/jquery.min.js
Ignore double-resolution attempts.

   raw     gz Sizes
250582  74046 dist/jquery.js
 84978  29614 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +3657   +827 dist/jquery.js
  +785   +219 dist/jquery.min.js

   raw     gz Compared to last run
  +211    +53 dist/jquery.js
   +10     +8 dist/jquery.min.js
Ignore post-resolution exceptions.

   raw     gz Sizes
250862  74103 dist/jquery.js
 84992  29621 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +3937   +884 dist/jquery.js
  +799   +226 dist/jquery.min.js

   raw     gz Compared to last run
  +280    +57 dist/jquery.js
   +14     +7 dist/jquery.min.js
Only check objects and functions for thenability.

   raw     gz Sizes
251129  74152 dist/jquery.js
 85036  29630 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +4204   +933 dist/jquery.js
  +843   +235 dist/jquery.min.js

   raw     gz Compared to last run
  +267    +49 dist/jquery.js
   +44     +9 dist/jquery.min.js
}),
done = jQuery.map( new Array( 3 ), function() { return assert.async(); } );

piped.fail(function( result ) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That piped deferreds inherit rejection is the reason why I couldn't justify dropping .pipe... the standard and our current behavior are just too divergent here.

@dmethvin
Copy link
Member

dmethvin commented Jan 6, 2015

Wow. I'll be reading through this today ... maybe tomorrow as well. :)

// Support: Promises/A+ section 2.3.3.3.4.1
// https://promisesaplus.com/#point-61
// Ignore post-resolution exceptions
if ( depth + 1 >= maxDepth ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the part in the story where it swallows errors. Some of the libs do complicated "next-turn" checking but I wonder if we could just do a console.log if the error that was thrown is one of the developer-screwup errors like SyntaxError. Something as simple as this?

if ( /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error/.test(e) ) {
    console.log("[Caught exception] "+e);
    console.trace();
}

It could be in a public method that users could override perhaps?

Also note we only have one test case for thrown exceptions and none for browser-initiated exceptions.

@dmethvin
Copy link
Member

dmethvin commented Jan 6, 2015

This is really awesome. 👏 I want to understand it a little more, the spec references are helping.

@markelog
Copy link
Member

markelog commented Jan 6, 2015

👏 indeed

/cc @domenic

@dcherman
Copy link
Contributor

dcherman commented Jan 7, 2015

It looks like the $.when implementation is still using .promise as a test to determine whether or not a given argument is a promise or not. Can that be changed for compatibility with other Promise libs, or is that out of scope for this PR?

@gibson042
Copy link
Member Author

That's definitely in scope here. I'll update it.

Unwrap thenables for progress handlers
Await fulfillment (ONLY) from thenables returned by progress handlers
* Ignore progress
* Throw rejections

   raw     gz Sizes
252116  74390 dist/jquery.js
 85040  29641 dist/jquery.min.js

   raw     gz Compared to master @ 66e1b6b
 +5191  +1171 dist/jquery.js
  +847   +246 dist/jquery.min.js

   raw     gz Compared to last run
  +987   +239 dist/jquery.js
    +4    +11 dist/jquery.min.js
@gibson042
Copy link
Member Author

After some thought, I ended up nearly reversing the last block above... thenables returned from .then progress handlers are used only for their eventual fulfillment values (which translate back into progress values for the subordinate Deferred), with progress ignored and rejection generating (asynchronous) exceptions so their non-effect can be observed. It's possible to take a different direction, however, and I'd like other opinions—progress events, especially multiple independent chains of progress events originating from various points on a resolution chain, can generate the kinds of headaches usually reserved for time travel fiction.

Still to do:

  • update jQuery.when
  • reduce size
  • rebase (avoiding for now to keep a stable size impact reference)
  • prototype unhandledRejections plugin

@dmethvin
Copy link
Member

I would be fine with corralling .then to only support standard Promise features, leaving back-compatible behavior to .pipe and the rest if that makes life easier.

@gibson042
Copy link
Member Author

So that would involve .then resolved/rejected handlers always receiving exactly one argument and undefined context, and progress handlers never being called at all? We can do so, and it will simplify this effort, but I think it will mean committing to keep .pipe around for longer (since our internal uses will lean on it more heavily).

It's also a bigger backcompat break, but I don't know if that's a good thing or a bad thing here.

I'd like to further point out that these cases are just as potentially problematic with .pipe, it's just unlikely that anything ever comes close to exercising the full power of what we expose with progress—including our own unit tests. For example, I just found #2010.

@dcherman
Copy link
Contributor

This may be more of a use case for jQuery.Migrate, but what about adding warnings in the unminified versions to warn the user if thenCallback.length or failCallback.length is > 1 when then is called since the standards compliant one will only accept 1 argument?

I'm just thinking about the nasty case of $.ajax where each callback accepts 3 arguments; the normal way to do that with Promises is to resolve it with a tuple and access the properties by index ( or with a spread helper that many libs provide ), however that's obviously a massive backcompat break.

@gibson042
Copy link
Member Author

@jaubourg and @jquery/core:
I know we're all getting kind of antsy about Deferred/Promise interop, and having two competing PRs doesn't help. This one is basically ready to land now (with reduction deferred for later), but as I mentioned in the meeting, I'd like to focus more on the behavior changes and their reflection in unit tests than on the implementation. So please look over the description and test/unit/deferred.js and provide any comments you may have on the relevant points. Once we agree on them, the code can follow.

@benjamingr
Copy link

In order to deal with multiple arguments in then you can do what most promise libraries do and expose a .spread method in addition to .then that converts an array to variable arguments. Note that ES6 already ships with destructuring which accomplishes this.

@dcherman
Copy link
Contributor

dcherman commented Feb 2, 2015

Just another point of concern with synchronous ajax calls:

I just took a look and unless I missed them, there are no unit tests that assert the ordering of callbacks. Since all handlers ( including the inline success, error, and complete options ) are attached using Deferreds, this change might have the effect of "breaking" synchronous ajax in that the callbacks will no longer be called synchronously.

@benjamingr
Copy link

@dcherman using deferreds with synchronous AJAX calls is not and has not been supported as of 1.8. Quoting the manual:

As of jQuery 1.8, the use of async: false with jqXHR ($.Deferred) is deprecated; you must use the success/error/complete callback options instead of the corresponding methods of the jqXHR object such as jqXHR.done() or the deprecated jqXHR.success().

@dcherman
Copy link
Contributor

dcherman commented Feb 2, 2015

@benjamingr I did forget about that, but my point still stands actually unless I'm misunderstanding something:

https://github.com/jquery/jquery/blob/master/src/ajax.js#L490-L492
https://github.com/jquery/jquery/blob/master/src/ajax.js#L622-L624

The callback options just add the callbacks to the Deferred which is now resolved asynchronously, no?

@gibson042
Copy link
Member Author

@dcherman: this change forces then callbacks to follow the on-object done/fail/always, by design. The actual sequence is that the latter fire first, synchronously in the order in which they were added, and then the former fire asynchronously in the order they were added. And the ajax module doesn't use then.

In short: only then is affected.

Do you foresee that causing problems? If so, I'm not sure anything can be done, since the asynchronicity is explicitly specified.

@dcherman
Copy link
Contributor

dcherman commented Feb 2, 2015

@gibson042 Ah I see how it works now, I misunderstood how this change worked. Since as @benjamingr pointed it the Deferred/Promise methods have been deprecated for sync XHR since 1.8, it shouldn't officially cause a problem. I'm sure there are people out there that were depending on .then for sync, but there's not much that you can do about that and it's unsupported.

@timmywil
Copy link
Member

@gibson042 How bout we land this?

@gibson042
Copy link
Member Author

I didn't get a chance to merge this, but did clean up some text. At this point, I feel like it's good to go and the remaining work can be dealt with later.

@benjamingr
Copy link

Awesome! Great news

On Wed, Feb 25, 2015 at 7:25 PM, Richard Gibson notifications@github.com
wrote:

I didn't get a chance to merge this, but did clean up some text
gibson042@694374c.
At this point, I feel like it's good to go and the remaining work can be
dealt with later.


Reply to this email directly or view it on GitHub
#1996 (comment).

@timmywil
Copy link
Member

Hey @gibson042, reminder to merge this when you get a chance. Thanks!

@gibson042 gibson042 added this to the 3.0.0 milestone Mar 20, 2015
@gibson042 gibson042 closed this in 555a50d Mar 20, 2015
gibson042 added a commit that referenced this pull request Mar 20, 2015
arschmitz added a commit to jquery-archive/jquery-mobile that referenced this pull request Mar 27, 2015
kakul pushed a commit to kakul/jquery-mobile that referenced this pull request Apr 14, 2015
gibson042 added a commit that referenced this pull request Nov 10, 2015
@markelog markelog mentioned this pull request Nov 16, 2015
@lock lock bot locked as resolved and limited conversation to collaborators Jan 19, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Development

Successfully merging this pull request may close these issues.

9 participants