Skip to content
This repository

Y.Deferred, yo. Let's make this happen! #241

Closed
wants to merge 10 commits into from

10 participants

Luke Smith Juan Ignacio Dopazo Eric Ferraiuolo Brian Cavalier solmsted Ryan Grove Dav Glass Lee Shepherd Jenny Donnelly Matt Sweeney
Luke Smith
Collaborator

Adds Y.Deferred and Y.Promise in 'deferred' module and features and Y.when (like Y.Parallel) in 'deferred-extras'.

Aimed to serve as the standard API for a transaction object/layer for big IO refactor. API feedback still welcome, though there was much discussion lsmith#36.

added some commits August 30, 2012
Luke Smith Initial drop Y.Deferred and Y.Promise
With some real tests, some stubbed tests. Basic functionality
seems to be working, but I haven't been very rigorous about
it yet.
8c80d47
Luke Smith Added hacker-friendly version + extras
New version uses prototypes and gentleman's privates, and
is broken into two pieces:
1. `deferred` which offers Y.Deferred and Y.Promise with only
   deferred.then(), resolve(), reject(), promise(), and getStatus(),
   and promise.then() and promise().
2. `deferred-extras` which adds deferred/promise.onProgress() and
   deferred.notify() for progress updates, an promise.wait() to
   insert a delay in the chained sequence.

Also added `deferred-when` which creates a `Y.when()` method to wrap
multiple operations and returns a promise that will be resolved when
all individual operations complete.
79037da
Juan Ignacio Dopazo

I think we want something like:

    promise: function (promise) {
        // check for instanceof?
        if (promise) {
             promise._deferred = this;
             this._promise = promise;
        }
        return this._promise;
    }

And maybe let it replace the promise only once.

added some commits September 04, 2012
Luke Smith Bug fixes, incorporating @juandopazo's feedback
Split up the code into two modules:
deferred - core implementation
deferred-extras - +onProgress/notify, wait, and Y.when
17ad153
Luke Smith All callbacks async now. when() fixed+fails fast
Plus when() now supports passing simple values as well as
functions and promises.  Previously, it would never resolve
the allDone deferred if a simple value was passed.
3c13c95
Luke Smith Bind fallback then() callbacks to the deferred 7df81fb
Luke Smith Add missing oop requirement to meta a02d502
Luke Smith Add tests. deferred has 100% coverage, need extras 712d3e6
Luke Smith when() bug fix & disallow onProgress for resolved
Tests yay!
203c7f0
Luke Smith 100% coverage for extras, +more tests for deferred 01a9f33
Juan Ignacio Dopazo
Collaborator

Fantastic! The only thing missing is what I mention in that comment I left: a mechanism to return objects extending Y.Promise so that we can have a Y.Transaction class for IO that extends Y.Promise, a NodeDeferred plugin or any other creative use we come up with.

Eric Ferraiuolo ericf commented on the diff September 10, 2012
src/deferred/js/deferred.js
((31 lines not shown))
  31
+Represents an operation that may be synchronous or asynchronous.  Provides a
  32
+standard API for subscribing to the moment that the operation completes either
  33
+successfully (`resolve()`) or unsuccessfully (`reject()`).
  34
+
  35
+@class Deferred
  36
+@constructor
  37
+**/
  38
+function Deferred() {
  39
+    this._subs = {
  40
+        resolve: [],
  41
+        reject : []
  42
+    };
  43
+
  44
+    this._promise = new Y.Promise(this);
  45
+
  46
+    this._status = 'in progress';
9
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

Would a single word status be better? "progress".

Luke Smith Collaborator
lsmith added a note September 10, 2012

Maybe. I'm open to it, if you think it's important. To me, "progress" implies something has happened, which isn't necessarily the case. "unfulfilled" would be more appropriate, but is an odd word and would be typoed.

Eric Ferraiuolo Owner
ericf added a note September 11, 2012

I think a single word is important, and I agree that English sucks.

Lee Shepherd
bwg added a note September 11, 2012

I agree that a single word would be best. pending?

Jenny Donnelly Owner
jenny added a note September 11, 2012

I can't resist a good naming discussion. "inprogress"?

Matt Sweeney Collaborator

¿progresando?

Dav Glass Owner

"bobbingforapples"

Ryan Grove Collaborator
rgrove added a note September 11, 2012

Sold! You had me at "¿".

I tend to like pending or unfulfilled.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Ferraiuolo ericf commented on the diff September 10, 2012
src/deferred/js/deferred.js
((126 lines not shown))
  126
+        var then    = new Y.Deferred(),
  127
+            promise = this.promise(),
  128
+            resolveSubs = this._subs.resolve || [],
  129
+            rejectSubs  = this._subs.reject  || [];
  130
+
  131
+        function wrap(fn, method) {
  132
+            return function () {
  133
+                var args = slice.call(arguments);
  134
+
  135
+                // Wrapping all callbacks in setTimeout to guarantee
  136
+                // asynchronicity. Because setTimeout can cause unnecessary
  137
+                // delays that *can* become noticeable in some situations
  138
+                // (especially in Node.js), I'm using Y.soon if available.
  139
+                // As of today, Y.soon is only available in the gallery as
  140
+                // gallery-soon, but maybe it could get promoted to core?
  141
+                (Y.soon || setTimeout)(function () {
9
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

This would use process.nextTick in Node.js?

Luke Smith Collaborator
lsmith added a note September 10, 2012

If the implementation of Y.soon does, yes. And gallery-soon does use it. Speaking of which, maybe it should be pulled into core :)

Eric Ferraiuolo Owner
ericf added a note September 11, 2012

Oh Y.soon does a hole bunch of stuff. I'd like to look into why it would matter and what it would give you over setTimeout(function () {}, 0).

Juan Ignacio Dopazo Collaborator

It tries to do the shortest delay possible by using postMessage and other crazy hacks. I think it may be overkill. The only thing I can think about right now that may need faster timeouts in the browser are animations and those can use requestAnimationFrame. However Y.soon could be a nice abstraction over process.nextTick || setImmediate || setTimeout.

Eric Ferraiuolo Owner
ericf added a note September 11, 2012

Y.soon is probably a whole different topic for discussion but I'll try to be brief with comments here.

I feel the speed benefits are worth it.
http://jsperf.com/y-soon-vs-settimeout

I've read the timers used by setTimeout are expensive in other ways. I haven't tested this myself so this might be inaccurate. Depending on implementation, they can consume a bunch of memory, drain mobile device and laptop batteries, and add overhead to every turn of the JavaScript event loop. I feel like any of the implementations of Y.soon from gallery-soon (other than the fallback) would be less expensive, but I haven't done any actual performance comparisons to validate this claim.

As promises are intended to be a low level utility, performance should be a consideration. I can imagine a reasonably complex app chaining a dozen small promises for one unit of io. Then if the app does tons of near real time io, the cost of all the throw-away timers is going to add up.

I'd like to refactor Y.soon into a bunch of submodules and use conditional loading to decide one implementation to load. Then developers could choose to bypass conditional loading and use a specific implementation like MessageChannel, instead of it being completely wasted bytes. It's annoying that gallery-soon depends on node-base, especially in Node.js.

Y.soon could be brought into core by defining it as the fallback implementation using setTimeout 0. The gallery will continue to provide alternative implementations to replace that one.

Ryan Grove Collaborator
rgrove added a note September 14, 2012

There may be very specific use cases in which the extreme hackery in gallery-soon is worthwhile, but as a general-purpose utility in YUI itself, it would give me the willies. It's the most micro of micro-optimizations, and I don't think we should encourage its use by anyone who doesn't completely understand its black magicks.

It's clever as hell, but it's also relatively large for such a simple purpose, and it's edge case city. Maintaining browser compat and ensuring proper test coverage seems like it would be a nightmare.

I think I may have failed to explain myself clearly. I'd like to move the discussion of Y.soon here: http://yuilibrary.com/forum/viewtopic.php?f=18&t=10636

Eric Ferraiuolo Owner
ericf added a note September 19, 2012

Just to make sure this is captured here, I added some thoughts to the Y.soon() forum thread about us possibly wanting to use one of the hacks so that callbacks will be called async, but before the next DOM repaint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Ferraiuolo ericf commented on the diff September 10, 2012
src/deferred/js/deferred.js
((42 lines not shown))
  42
+    };
  43
+
  44
+    this._promise = new Y.Promise(this);
  45
+
  46
+    this._status = 'in progress';
  47
+
  48
+}
  49
+
  50
+Y.mix(Deferred.prototype, {
  51
+    /**
  52
+    Returns the promise for this Deferred.
  53
+
  54
+    @method promise
  55
+    @return {Promise}
  56
+    **/
  57
+    promise: function () {
6
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

Why a method? Why not just reference deferred.promise?

If this remains a method, should getPromise() be used?

Luke Smith Collaborator
lsmith added a note September 10, 2012

It seemed like a convention when reviewing some of the other deferred/promises libs out there, but the field is much broader than I'd initially found. I'm taking jQuery's implementation as strong convention for the sake of interoperability (or near-interop). jq has a promise() method to return the promise, though I suspect it is implemented as a method because it accepts an object parameter to decorate that object with the promise API. @juandopazo has asked for this same support, but I'm on the fence.

Juan Ignacio Dopazo Collaborator

I want promise objects with more methods, extended through prototypes. Otherwise promises are just a bloated version of Parallel

Eric Ferraiuolo Owner
ericf added a note September 11, 2012

Isn't jQuery just using their standard accessor/mutator pattern for promise(), effectively the method does what getPromise() and setPromise() would be doing?

Juan Ignacio Dopazo Collaborator

In jQuery promise() does parasitic inheritance on any object, adding lots of methods to it.

// Get a promise for this deferred
// If obj is provided, the promise aspect is added to the object
promise: function( obj ) {
    return typeof obj === "object" ? jQuery.extend( obj, promise ) : promise;
}

I'm pretty sure we will want the ability to customize the interfaces available on promises. The addMethod method may be good enough but I kind of like this prototype inheritance idea.

Also note that the Promises/D proposal defines the promiseSend method. It achieves something similar to addMethod, but uses a concept of message passing rather than method invocation. I personally don't really like the functionality of promiseSend, but I do like this interoperability side-effect: In the context of a promise library, the existence of a "promiseSend" method must be sufficient to distinguish a promise from any other value.

Although it only promotes interoperability if other libraries also implement it which I doubt many have.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/deferred/js/deferred.js
((136 lines not shown))
  136
+                // asynchronicity. Because setTimeout can cause unnecessary
  137
+                // delays that *can* become noticeable in some situations
  138
+                // (especially in Node.js), I'm using Y.soon if available.
  139
+                // As of today, Y.soon is only available in the gallery as
  140
+                // gallery-soon, but maybe it could get promoted to core?
  141
+                (Y.soon || setTimeout)(function () {
  142
+                    var result = fn.apply(promise, args),
  143
+                        resultPromise;
  144
+
  145
+                    if (result && typeof result.promise === 'function') {
  146
+                        resultPromise = result.promise();
  147
+
  148
+                        if (resultPromise.getStatus() !== 'in progress') {
  149
+                            then[method].apply(then, resultPromise.getResult());
  150
+                        } else {
  151
+                            result.promise().then(
3
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

You already have resultPromise cached above.

Luke Smith Collaborator
lsmith added a note September 10, 2012

It's declared, not assigned. Am I missing the line you're referring to?

Eric Ferraiuolo Owner
ericf added a note September 11, 2012

Yes, L147.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/deferred/js/deferred.js
((117 lines not shown))
  117
+    @method then
  118
+    @param {Function} [callback] function to execute if the Deferred
  119
+                resolves successfully
  120
+    @param {Function} [errback] function to execute if the Deferred
  121
+                resolves unsuccessfully
  122
+    @return {Promise} The promise of a new Deferred wrapping the resolution
  123
+                of either "resolve" or "reject" callback
  124
+    **/
  125
+    then: function (callback, errback) {
  126
+        var then    = new Y.Deferred(),
  127
+            promise = this.promise(),
  128
+            resolveSubs = this._subs.resolve || [],
  129
+            rejectSubs  = this._subs.reject  || [];
  130
+
  131
+        function wrap(fn, method) {
  132
+            return function () {
3
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

This method is begging for code comments, it's a mind warp, and hard to follow what's happening without them.

Luke Smith Collaborator
lsmith added a note September 10, 2012

It is a mind warp.

Eric Ferraiuolo Owner
ericf added a note September 11, 2012

I'd like to understand it more, can you add some comments to it next time you're in this code, if you can even remember what it's doing :P

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/deferred/js/deferred.js
((114 lines not shown))
  114
+    `functionA().then(functionB).then(functionC)` where `functionA` returns
  115
+    a promise, and `functionB` and `functionC` _may_ return promises.
  116
+
  117
+    @method then
  118
+    @param {Function} [callback] function to execute if the Deferred
  119
+                resolves successfully
  120
+    @param {Function} [errback] function to execute if the Deferred
  121
+                resolves unsuccessfully
  122
+    @return {Promise} The promise of a new Deferred wrapping the resolution
  123
+                of either "resolve" or "reject" callback
  124
+    **/
  125
+    then: function (callback, errback) {
  126
+        var then    = new Y.Deferred(),
  127
+            promise = this.promise(),
  128
+            resolveSubs = this._subs.resolve || [],
  129
+            rejectSubs  = this._subs.reject  || [];
2
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

Were these suppose to be a conditional assignment? i.e:

subs        = this._subs,
resolveSubs = subs.resolve || (subs.resolve = []),
rejectSubs  = subs.reject || (subs.reject = []);
Luke Smith Collaborator
lsmith added a note September 10, 2012

Nope, byte shaving. When resolved, the only subscriptions supported are those corresponding to whether it was resolve()d or reject()ed. See line 78, for example. So if you call then() after it was resolved, it will immediately resolve the new deferred, or do nothing. The empty array allows the rest of the function body to run without error, but is never referenced, so it's thrown away at the end of the function, making it a no-op.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Ferraiuolo ericf commented on the diff September 10, 2012
src/deferred/js/promise.js
((11 lines not shown))
  11
+}
  12
+
  13
+/**
  14
+Adds a method or array of methods to the Promise prototype to relay to the
  15
+so named method on the associated Deferred.
  16
+
  17
+DO NOT use this expose the Deferred's `resolve` or `reject` methods on the
  18
+Promise.
  19
+
  20
+@method addMethod
  21
+@param {String|String[]} methods String or array of string names of functions
  22
+                                 already defined on the Deferred to expose on
  23
+                                 the Promise prototype.
  24
+@static
  25
+**/
  26
+Promise.addMethod = function(methods) {
2
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

Wha what? Should this be public?

Luke Smith Collaborator
lsmith added a note September 10, 2012

Public to allow features to be added to Promise.prototype rather than needing to subclass both Y.Deferred and Y.Promise to add features. Made for less, and less complex code. shrug

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/deferred/js/when.js
((7 lines not shown))
  7
+@module deferred
  8
+@submodule deferred-when
  9
+**/
  10
+
  11
+/**
  12
+Wraps any number of callbacks in a Y.Deferred, and returns the associated
  13
+promise that will resolve when all callbacks have completed.  Each callback is
  14
+passed a Y.Deferred that it must `resolve()` when that callback completes.
  15
+
  16
+@for YUI
  17
+@method when
  18
+@param {Function|Promise} operation* Any number of functions or Y.Promise
  19
+            objects
  20
+@return {Promise}
  21
+**/
  22
+Y.when = function () {
3
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

Would this replace Y.Parallel?

Luke Smith Collaborator
lsmith added a note September 10, 2012

It's more code than Y.Parallel and does more. Better that IO, et al, are refactored to output transactions based on promises. In which case, Y.when would be preferable for that/those specific use case(s). If the seed has a specific use case, then it's fine for Y.Parallel to also exist. Things like grouping event subscriptions could go either way (not "transaction" related, per se, but could be considered to be, based on the events as independent/parallel steps in an operation).

Juan Ignacio Dopazo Collaborator

It's Parallel with success, failure and progress :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/deferred/js/when.js
((28 lines not shown))
  28
+
  29
+    function oneDone(i) {
  30
+        return function () {
  31
+            var args = slice.call(arguments);
  32
+
  33
+            results[i] = args.length > 1 ? args : args[0];
  34
+
  35
+            remaining--;
  36
+
  37
+            if (!remaining && allDone.getStatus() !== 'rejected') {
  38
+                allDone.resolve.apply(allDone, results);
  39
+            }
  40
+        };
  41
+    }
  42
+
  43
+    Y.Array.each(funcs, function (fn, i) {
2
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

Seems like using a for-loop here would be better for performance.

Luke Smith Collaborator
lsmith added a note September 10, 2012

Used to avoid incrementer leaking in the closure, necessitating a wrapping function anyway, so it seemed moot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/deferred/js/when.js
((40 lines not shown))
  40
+        };
  41
+    }
  42
+
  43
+    Y.Array.each(funcs, function (fn, i) {
  44
+        var finished = oneDone(i),
  45
+            deferred;
  46
+
  47
+        // accept promises as well as functions
  48
+        if (typeof fn === 'function') {
  49
+            deferred = new Y.Deferred();
  50
+        
  51
+            deferred.then(finished, failed);
  52
+            
  53
+            // It's up to each passed function to resolve/reject the deferred
  54
+            // that is assigned to it.
  55
+            fn.call(Y, deferred);
5
Eric Ferraiuolo Owner
ericf added a note September 10, 2012

Why Y? Do we need to apply a specific context?

Luke Smith Collaborator
lsmith added a note September 10, 2012

Arbitrary. I chose Y for lack of a better option, since I prefer not to execute from the global context. But maybe that's fine?

Ryan Grove Collaborator
rgrove added a note September 10, 2012

Stick with the global context, imho. There are a few other random places in the lib that use Y as a default context, but I find this confusing and would prefer to stamp them out.

Dav Glass Owner
Luke Smith Collaborator
lsmith added a note September 10, 2012

I'm fine changing it to global.

The current signature, again based on jq, accepts n args, all treated as callbacks (or promises or result values), so there's no room for a config object unless the signature is changed.

Promises/B defines a signature for when() as when(value, callback, errback), which handles only one promise, so more closely resembles Promises/A promise.then(callback, errback).

If I were to alter the signature for when(), I'd be more inclined to change it to Y.when( <array of callbacks, etc> [, callback [, errback]]). The array of callbacks/promises would be wrapped in a tracking deferred as the current implementation is. If no callback or errback is passed, the wrapping deferred's promise would be returned. If either were included, it would internally call wrapperPromise.then(callback, errback) and return the resulting promise from that then() chain.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Ferraiuolo
Owner

@lsmith This stuff looks like it will be a great API to build data-access stuff on!

I'm wondering about a couple of things…

  1. Do you look at Y.Deferred as a lower-level tool that wouldn't end up being exposed to the main end uses of YUI? I worry that it detracts from the YUI-way of doing things by not using events and attributes. That said I think it's important for a low-level API to remain lightweight.

  2. Double vs. single callback API. I'd like the discuss further the differences between using a Node.js style callback(err, data) API vs. the callback and errorback functions.

Also, I should probably do some deeper reading on the deferred/promise stuff, do you have any good links to share?

Luke Smith
Collaborator
  1. The primary interface is the then(callback, errback) function of the promise and the resolve() and reject() methods on the deferred. Anything else is sugar. That said, I am still holding on to my initial implementation that used on('progress', callback) instead or onProgress(callback), which then bled over to promise.on(<"resolve" or "success" or "done">, callback) (and so for "reject", "failure", etc), which could be expanded per class specialization. But as an abstract API, then(), resolve(), and reject() would be the baseline, and be public. But yes, I really didn't want to require custom events for this layer because of the module dep weight and code complexity. Deferreds really should be a simple, (fairly?) inflexible API.

  2. I thought we'd talked about using Node.js style callback(err, data) for the transport layers, promises for transactions, and classes for transaction factories (aka DataSource or Resource). That's still my thought. Transport callbacks would dovetail into transactions by forking err => reject().

Luke Smith Add docs, API feedback from review, tests
* More docs in then()
* errback that doesn't return a failed promise will continue to next callback,
  not next errback.  Signals recovery from the failure.
* Y.when renamed Y.batch
* New Y.when(promiseOrValue, callback, errback) added
* New Y.defer(callback(deferred)) added
* Tests added for new APIs and changed APIs
04834ed
Eric Ferraiuolo
Owner

Here's a good post about the true point of Promises:
https://gist.github.com/3889970

Luke Smith
Collaborator

Yep, and it supports my want for keeping the Deferred and Promise separate. I am technically exposing the promise's underlying deferred via promise._deferred, which I had issue with, but seemed pragmatic instead of building a new object with unique methods closing over the deferred each time. The success and exception flow is supported in my implementation, though I don't think I'm handling literal exceptions via try/catch. He made no comment on the relevance or importance of progress handlers, as seems to be the usual case.

Juan Ignacio Dopazo
Collaborator

Nice article! I found a possible bug based on that article:

// foo.json contains {items:[1,2,3]}
Y.io.get('foo.json')
    .then(function (id, xhr) {
        return JSON.parse(xhr.responseText);
    })
    .then(function (data) {
        return data.items.map(Math.sqrt);
    })
    .then(function (roots) {
        console.log(roots);
    }, function (id, xhr) {
        console.error(xhr);
    });

This works ok until returning an array from the second then(). Then it turns the array into parameters, so roots ends up being 1 instead of [1, 1.4142135623730951, 1.7320508075688772].

Maybe assuming an array means an argument list is not a good idea.

Juan Ignacio Dopazo juandopazo commented on the diff October 15, 2012
src/deferred/js/extras.js
((21 lines not shown))
  21
+    of operations after the inserted pause.
  22
+
  23
+    @method wait
  24
+    @param {Number} ms Number of milliseconds to wait before resolving
  25
+    @return {Promise}
  26
+    **/
  27
+    wait: function (ms) {
  28
+        var deferred = new Y.Deferred(),
  29
+            promise  = deferred.promise(),
  30
+            timeout;
  31
+
  32
+        // || 0 catches 0 and NaN
  33
+        ms = Math.max(0, +ms || 0);
  34
+
  35
+        this.then(function () {
  36
+            var args = slice.call(arguments);
5
Juan Ignacio Dopazo Collaborator

No need to call slice here.

Luke Smith Collaborator
lsmith added a note October 15, 2012

It is necessary to pass the then() args into the setTimeout function.

Juan Ignacio Dopazo Collaborator

I mean you can just do var args = arguments.

Luke Smith Collaborator
lsmith added a note October 15, 2012

Oh duh! Right you are :)

Juan Ignacio Dopazo Collaborator

There are a couple of those around. Must.save.CPU.cycles.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Ferraiuolo
Owner

Another data point, RSVP lib by Yehuda Katz and Tom Dale:
https://github.com/tildeio/rsvp.js

Luke Smith
Collaborator

Another implementation that ignores progress events. There's a discussion going on in the cujojs google group about propagating progress events down the promise chain. I seem to be on the "grumpy old man" side. shrug
https://groups.google.com/forum/?fromgroups=#!topic/cujojs/QurUiAeTa5E

Luke Smith
Collaborator

Update: I've been chatting with @briancavalier, who is working on better defining the Promises/A spec (yay!), and doing more research. This implementation will change, but I don't anticipate much change in the API.

Look forward to more commits in the PR.

Juan Ignacio Dopazo
Collaborator

Hey @lsmith do you think we should rewrite Domenic's test suite for YUI Test or can we run it as a CLI test?

Juan Ignacio Dopazo
Collaborator

Now that the promises A+ spec is stable.I drafted an implementation that implements that spec following the YUI style. I'm currently writing unit tests.

You can find it in a pull request in my own fork of YUI: juandopazo#4

There are some open issues:

  • Promises require Y.soon which means requiring “timers” every time we want promises
  • For tests, then() has a problem because it catches errors and so Y.Assert functions don’t work. @ericf mentioned the possibility of a workaround with YUI Test. Otherwise we may need end() a method that causes promises to throw errors
  • I still need to figure out how to return different types of promises without creating too many of them. See this gist to get an idea why this is an issue: https://gist.github.com/4506753
  • I need to translate the spec tests to YUI tests. This duplicates efforts but provides coverage. Should we run both versions of the spec tests?
  • Having compatibility with WinJS promises with a syntax like new Promise(function (fulfill, reject) {}) means creating two more functions per promise by using bind, and now that we're completely hiding the resolver from the promise by closing over it with instance properties we may want to avoid creating more functions and just pass the resolver to that function. Basically:
var promise = new Y.Promise(function (fulfill, reject) {
  fulfill('yay!');
});

// vs

var promise = new Y.Promise(function (resolver) {
  resolver.fulfill('yay!');
});
Luke Smith
Collaborator

Issue responses:

  • What specifically are you concerned about? Timers is small, so not size. It's another function hop between promises and setImmediate, but that cost is unavoidable unless we want to standardize on setTimeout, which we don't.
  • I'm curious about the specific cases where this causes trouble. I don't think we need end(), though it would make a reasonable extras module/feature.
  • commented on the gist. Not sure this is a problem.
  • I'd say keep the non-YUI Test version of the tests in tests/manual
  • I'm partial to the function (fulfill, reject) syntax, and not concerned about the private functions. I don't think you need to use bind. You could do away with the Resolver class and set this._resolver = { fulfill: function ()..., reject: function ()... }; if necessary. This point warrants more discussion, though, for sure.
Juan Ignacio Dopazo
Collaborator

Timers: you brought it up last time we chatted about this. I'm not that concerned. I'd like to have Y.Get return promises and that would mean bundling timers and promises in yui.js, but I can live with it being optional.

end: I only ran into this when writing tests. For instance:

'some asynchronous test for promises': function () {
  var test = this;

  Y.when(5).then(function (value) {
    // pausing and resuming work ok
    test.resume();

    // Assert.areEqual will throw an error which will be caught
    // by then() and reject this promise instead, so Y.Test
    // doesn't receive the assertion
    Y.Test.Assert.areEqual(4, value, 'value should be 4');
  });
  test.wait();
};

I absolutely prefer not to add end. I'll look at the internals of Y.Test to see if there's a workaround. Eric said something about there being an option that could help.

Syntax: I also like function (fulfill, reject). I don't usually worry about memory performance but since it's really easy to create a lot of Promise objects it's something to consider. Plus, there are cases in which you may want to use other methods from the resolver like getStatus. How would you expose them? At the A+ group, there's the proposal of having fulfill contain extra methods. But that means even more closures instead of prototype functions.

Juan Ignacio Dopazo
Collaborator

Another issue: I'm not sure Y.batch should treat functions differently. There isn't much difference between Y.batch(fn) and Y.batch(Y.Promise(fn)). Maybe it made sense when we had to create a Deferred object first. And some people might want to return functions for some reason. It's JavaScript and it has functions as values after all. I think we should minimize magic here.

Brian Cavalier

Hey guys, you've probably been following the resolver API convo over at Promises/A+, but just wanted to give a heads up that "fulfill" as a verb is being called into question. I'm currently against it, as are others, at least as it's been described to this point. I am strongly in favor the behavior of when.js and Q's resolve() (even if we change the name).

There's a new thread on the name here

Juan Ignacio Dopazo
Collaborator

Nevermind about tests and end. I totally forgot how to use pause and resume in YUI Test. I was doing test.resume(); Assert.foo() instead of test.resume(function () { Assert.foo() });. * facepalm *

@briancavalier thanks for the heads-up! I agree that not being upfront with what fulfill does could lead to unexpected errors. For the others to follow what Brian says, here's an example:

// the identity function should work in any `then` callback
function identity(x) {
  return x;
}
// for example:
Y.when(5).then(identity).then(function (value) {
  console.log(value); // 5!
});

// however, if the value is a promise, it can give you an unexpected result
var promise = Y.Promise(function (fulfill) {
  // promise holds a promise for 5 as a value (a promise inside a promise)
  fulfill(Y.when(5));
});
// now we create a new promise going through identity
promise.then(identity).then(function (value) {
  // but we get 5, not a promise, because identity returned a promise to 
  // then() and then() does special stuff to promises
  console.log(value); // 5!
});
Luke Smith
Collaborator

@juandopazo less magic for functions passed to Y.batch is fine. I honestly don't follow what the confusion is that you're trying to illustrate in your last code snippet. It all looks like expected behavior to me. Maybe I'm too close to it.

@briancavalier Thanks for the heads up. I've been remiss in keeping up with the org discussions, but will catch up tomorrow (Monday).

solmsted

@juandopazo I don't quite understand what you are saying about Y.batch. Currently it accepts functions or promises as arguments, are you saying that you want it to only accept promises? If so, I'd like to argue for you to keep it the way it is.

Y.batch is very similar to the API I wrote for Y.Async.runAll which I have really enjoyed using. It's true that

Y.batch(fn);

is not much different to

Y.batch(Y.Promise(fn));

but when your code starts to include dozens of promises, something like

Y.batch(function (resolve) {
    Y.batch(someFn, someFn, someFn, someFn, someFn).then(resolve);
}, function (resolve) {
    Y.batch(someFn, someFn, someFn, someFn, someFn).then(resolve);
}, function (resolve, reject) {
    Y.batch(someFn, someFn, someFn, someFn, someFn).then(resolve, reject);
}, function (resolve, reject) {
    Y.batch(someFn, someFn, someFn, someFn, someFn).then(resolve, reject);
}, anotherFn, anotherFn, anotherFn);

and if you have many such bits of code within a project, you really start to appreciate not having to repeatedly type new Y.Promise(...). Every repetition of new Y.Promise(fn) is replaced by the one line typeof fn === 'function' ? new Y.Promise(fn) : fn and I think that's a win.

I would even go so far as to say, you should allow new Y.Promise(otherPromise). If otherPromise is an instance of Y.Promise, just return otherPromise. If otherPromise is some other Promises A+ compatible promise object it should return a new promise that would resolve when the other promise resolves, reject when the other promise rejects, etc. This way, all new API's could accept either functions or any promises and they could be normalized to a Y.Promise by simply calling new Y.Promise(argument)

Juan Ignacio Dopazo
Collaborator

would even go so far as to say, you should allow new Y.Promise(otherPromise).

That's what Y.when is for. Y.when(5) will return a new promise fulfilled with the value 5. Y.when(promise) will just return the promise.

Currently Y.batch takes 3 different types of arguments: promises, functions and values. Promises and values (that are not functions) are passed through Y.when. So Y.batch(5) is equivalent to Y.batch(Y.when(5)). I didn't expect users to create that many promises in place. I expected them to do stuff like Y.batch(Y.io.js('foo.js'), Y.io.css('foo.css')). I know functions as values in this case is a rarity, but I want to simplify as much as possible around promises because they already have a substantial mental cost.

solmsted

I completely looked over the call to Y.when.

Okay, I see how that makes more sense, assuming you're working with other API's that return promises. Since YUI and Node.js don't currently have APIs that return promises, I'm very accustomed to using anonymous functions to wrap the async functionality everywhere. As YUI's use of promises grows, I'll be okay with Y.batch accepting promises or values. I'll always have gallery-async where I can hack in my own sugar.

Luke Smith
Collaborator

@solmsted another two points worth mentioning:

  1. Passing a function to Y.batch to get its promise wrapping magic is a convenience for what should be the uncommon case, but if Y.batch is used with a rash of anonymous functions, there's something wrong with the code. Most or all of those functions should be extracted out to methods, in which case, the function bodies can accept values and return promises.
  2. a function is a value, so treating functions differently makes it difficult to promise wrap functions as values rather than promise executors. The inconvenience is roughly equivalent, between Y.batch({wrapped:functionAsValue}, functionAsExecutor) and Y.batch(functionAsValue, Y.Promise(functionAsExecutor)), so it's more a matter of style and api-of-least-surprise.

I like the convenience, but it sacrifices a reasonable use case and promotes anon-fn hackery over more maintainable styles.

solmsted

@lsmith Yes I completely agree with your 2nd point. I failed to notice the call to Y.when and never considered that simple values would be passed to Y.batch. So I'm fine with Y.batch treating functions as values.

It doesn't seem necessary to add noise here but sometime in the future I'd like to resume a conversation about methods returning a promise vs anonymous callback functions.

Luke Smith
Collaborator

Closing this in favor of #445

Luke Smith lsmith closed this February 12, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 10 unique commits by 1 author.

Aug 30, 2012
Luke Smith Initial drop Y.Deferred and Y.Promise
With some real tests, some stubbed tests. Basic functionality
seems to be working, but I haven't been very rigorous about
it yet.
8c80d47
Luke Smith Added hacker-friendly version + extras
New version uses prototypes and gentleman's privates, and
is broken into two pieces:
1. `deferred` which offers Y.Deferred and Y.Promise with only
   deferred.then(), resolve(), reject(), promise(), and getStatus(),
   and promise.then() and promise().
2. `deferred-extras` which adds deferred/promise.onProgress() and
   deferred.notify() for progress updates, an promise.wait() to
   insert a delay in the chained sequence.

Also added `deferred-when` which creates a `Y.when()` method to wrap
multiple operations and returns a promise that will be resolved when
all individual operations complete.
79037da
Sep 04, 2012
Luke Smith Bug fixes, incorporating @juandopazo's feedback
Split up the code into two modules:
deferred - core implementation
deferred-extras - +onProgress/notify, wait, and Y.when
17ad153
Sep 05, 2012
Luke Smith All callbacks async now. when() fixed+fails fast
Plus when() now supports passing simple values as well as
functions and promises.  Previously, it would never resolve
the allDone deferred if a simple value was passed.
3c13c95
Luke Smith Bind fallback then() callbacks to the deferred 7df81fb
Luke Smith Add missing oop requirement to meta a02d502
Luke Smith Add tests. deferred has 100% coverage, need extras 712d3e6
Sep 06, 2012
Luke Smith when() bug fix & disallow onProgress for resolved
Tests yay!
203c7f0
Luke Smith 100% coverage for extras, +more tests for deferred 01a9f33
Sep 21, 2012
Luke Smith Add docs, API feedback from review, tests
* More docs in then()
* errback that doesn't return a failed promise will continue to next callback,
  not next errback.  Signals recovery from the failure.
* Y.when renamed Y.batch
* New Y.when(promiseOrValue, callback, errback) added
* New Y.defer(callback(deferred)) added
* Tests added for new APIs and changed APIs
04834ed
Something went wrong with that request. Please try again.