Skip to content
Permalink
Browse files
Merge pull request #1406 from tchak/deferred-then
Add Ember.Deferred mixin which implements Promises/A spec
  • Loading branch information
ebryn committed Oct 11, 2012
2 parents 0184c2c + 2f8d5e6 commit f7ac080db3a2a15f5814dc26fc86712cf7d252c8
Show file tree
Hide file tree
Showing 3 changed files with 382 additions and 0 deletions.
@@ -8,3 +8,4 @@ require('ember-runtime/mixins/mutable_enumerable');
require('ember-runtime/mixins/observable');
require('ember-runtime/mixins/target_action_support');
require('ember-runtime/mixins/evented');
require('ember-runtime/mixins/deferred');
@@ -0,0 +1,114 @@
var get = Ember.get, set = Ember.set,
slice = Array.prototype.slice,
forEach = Ember.ArrayPolyfills.forEach;

var Callbacks = function(target, once) {
this.target = target;
this.once = once || false;
this.list = [];
this.fired = false;
this.off = false;
};

Callbacks.prototype = {
add: function(callback) {
if (this.off) { return; }

this.list.push(callback);

if (this.fired) { this.flush(); }
},

fire: function() {
if (this.off || this.once && this.fired) { return; }
if (!this.fired) { this.fired = true; }

this.args = slice.call(arguments);

if (this.list.length > 0) { this.flush(); }
},

flush: function() {
Ember.run.once(this, 'flushCallbacks');
},

flushCallbacks: function() {
forEach.call(this.list, function(callback) {
callback.apply(this.target, this.args);
}, this);
if (this.once) { this.list = []; }
}
};


/**
@class
@extends Ember.Mixin
*/
Ember.Deferred = Ember.Mixin.create(
/** @scope Ember.Deferred.prototype */ {

/**
Add handlers to be called when the Deferred object is resolved or rejected.
*/
then: function(doneCallback, failCallback, progressCallback) {
if (doneCallback) {
get(this, 'deferredDone').add(doneCallback);
}
if (failCallback) {
get(this, 'deferredFail').add(failCallback);
}
if (progressCallback) {
get(this, 'deferredProgress').add(progressCallback);
}

return this;
},

/**
Call the progressCallbacks on a Deferred object with the given args.
*/
notify: function() {
var callbacks = get(this, 'deferredProgress');
callbacks.fire.apply(callbacks, slice.call(arguments));

return this;
},

/**
Resolve a Deferred object and call any doneCallbacks with the given args.
*/
resolve: function() {
var callbacks = get(this, 'deferredDone');
callbacks.fire.apply(callbacks, slice.call(arguments));
set(this, 'deferredProgress.off', true);
set(this, 'deferredFail.off', true);

return this;
},

/**
Reject a Deferred object and call any failCallbacks with the given args.
*/
reject: function() {
var callbacks = get(this, 'deferredFail');
callbacks.fire.apply(callbacks, slice.call(arguments));
set(this, 'deferredProgress.off', true);
set(this, 'deferredDone.off', true);

return this;
},

deferredDone: Ember.computed(function() {
return new Callbacks(this, true);
}).cacheable(),

deferredFail: Ember.computed(function() {
return new Callbacks(this, true);
}).cacheable(),

deferredProgress: Ember.computed(function() {
return new Callbacks(this);
}).cacheable()
});
@@ -0,0 +1,267 @@
module("Ember.Deferred");

test("can resolve deferred", function() {

var deferred, count = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

deferred.then(function() {
count++;
});

stop();
Ember.run(function() {
deferred.resolve();
});

setTimeout(function() {
start();
equal(count, 1, "done callback was called");
}, 20);
});

test("can reject deferred", function() {

var deferred, count = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

deferred.then(function() {}, function() {
count++;
});

stop();
Ember.run(function() {
deferred.reject();
});

setTimeout(function() {
start();
equal(count, 1, "fail callback was called");
}, 20);
});

test("can resolve with then", function() {

var deferred, count1 = 0 ,count2 = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

deferred.then(function() {
count1++;
}, function() {
count2++;
});

stop();
Ember.run(function() {
deferred.resolve();
});

setTimeout(function() {
start();
equal(count1, 1, "then were resolved");
equal(count2, 0, "then was not rejected");
}, 20);
});

test("can reject with then", function() {

var deferred, count1 = 0 ,count2 = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

deferred.then(function() {
count1++;
}, function() {
count2++;
});

stop();
Ember.run(function() {
deferred.reject();
});

setTimeout(function() {
start();
equal(count1, 0, "then was not resolved");
equal(count2, 1, "then were rejected");
}, 20);
});

test("can call resolve multiple times", function() {

var deferred, count = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

deferred.then(function() {
count++;
});

stop();
Ember.run(function() {
deferred.resolve();
deferred.resolve();
deferred.resolve();
});

setTimeout(function() {
start();
equal(count, 1, "calling resolve multiple times has no effect");
}, 20);
});

test("deferred has progress", function() {

var deferred, count = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

deferred.then(function() {}, function() {}, function() {
count++;
});

stop();
Ember.run(function() {
deferred.notify();
deferred.notify();
deferred.notify();
});
Ember.run(function() {
deferred.notify();
});
Ember.run(function() {
deferred.notify();
deferred.resolve();
deferred.notify();
});
Ember.run(function() {
deferred.notify();
});

setTimeout(function() {
start();
equal(count, 3, "progress called three times");
}, 20);
});

test("resolve prevent reject and stop progress", function() {
var deferred, resolved = false, rejected = false, progress = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

deferred.then(function() {
resolved = true;
}, function() {
rejected = true;
}, function() {
progress++;
});

stop();
Ember.run(function() {
deferred.notify();
});
Ember.run(function() {
deferred.resolve();
});
Ember.run(function() {
deferred.reject();
});
Ember.run(function() {
deferred.notify();
});

setTimeout(function() {
start();
equal(resolved, true, "is resolved");
equal(rejected, false, "is not rejected");
equal(progress, 1, "progress called once");
}, 20);
});

test("reject prevent resolve and stop progress", function() {
var deferred, resolved = false, rejected = false, progress = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

deferred.then(function() {
resolved = true;
}, function() {
rejected = true;
}, function() {
progress++;
});

stop();
Ember.run(function() {
deferred.notify();
});
Ember.run(function() {
deferred.reject();
});
Ember.run(function() {
deferred.resolve();
});
Ember.run(function() {
deferred.notify();
});

setTimeout(function() {
start();
equal(resolved, false, "is not resolved");
equal(rejected, true, "is rejected");
equal(progress, 1, "progress called once");
}, 20);
});

test("will call callbacks if they are added after resolution", function() {

var deferred, count1 = 0;

Ember.run(function() {
deferred = Ember.Object.create(Ember.Deferred);
});

stop();
Ember.run(function() {
deferred.resolve('toto');
});

Ember.run(function() {
deferred.then(function(context) {
if (context === 'toto') {
count1++;
}
});

deferred.then(function(context) {
if (context === 'toto') {
count1++;
}
});
});

setTimeout(function() {
start();
equal(count1, 2, "callbacks called after resolution");
}, 20);
});

7 comments on commit f7ac080

@domenic
Copy link

@domenic domenic commented on f7ac080 Oct 14, 2012

Choose a reason for hiding this comment

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

Oh no, another "thenable" that isn't Promises/A compatible (e.g. no chaining or error-trapping/transformation). How should I detect this guy? Maybe my new test is

if (putativePromise.then === "function" &&
    !putativePromise.pipe && // exclude jQuery's so-called "promises"
    !putativePromise.deferredDone) // exclude Ember's "deferreds"

?

@domenic
Copy link

@domenic domenic commented on f7ac080 Oct 14, 2012

Choose a reason for hiding this comment

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

From Promises/A:

This function should return a new promise that is fulfilled when the given fulfilledHandler or errorHandler callback is finished. This allows promise operations to be chained together. The value returned from the callback handler is the fulfillment value for the returned promise. If the callback throws an error, the returned promise will be moved to failed state.

Not one of these sentences is implemented.

@lukemelia
Copy link
Member

@lukemelia lukemelia commented on f7ac080 Oct 14, 2012

Choose a reason for hiding this comment

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

@domenic Thanks for noting this while there is still time to consider changes. Is there a Promises/A test suite?

@domenic
Copy link

@domenic domenic commented on f7ac080 Oct 14, 2012

Choose a reason for hiding this comment

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

No but I really need to write one. I'll see what I can come up with.

@domenic
Copy link

@domenic domenic commented on f7ac080 Oct 14, 2012

Choose a reason for hiding this comment

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

Here's somewhat of a start, unfortunately in CoffeeScript; I was using it for my own purposes. More coming.

@tlrobinson
Copy link

@tlrobinson tlrobinson commented on f7ac080 Oct 14, 2012

Choose a reason for hiding this comment

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

Do the tests for @kriskowal's Q (https://github.com/kriskowal/q) match Promises/A?

@domenic
Copy link

@domenic domenic commented on f7ac080 Oct 14, 2012

Choose a reason for hiding this comment

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

They do, but most of them are testing more advanced features beyond the scope of Promises/A. This subset might work, although it's probably not exhaustive.

Please sign in to comment.