Skip to content

Commit

Permalink
Merge pull request #1406 from tchak/deferred-then
Browse files Browse the repository at this point in the history
Add Ember.Deferred mixin which implements Promises/A spec
  • Loading branch information
ebryn committed Oct 11, 2012
2 parents 0184c2c + 2f8d5e6 commit f7ac080
Show file tree
Hide file tree
Showing 3 changed files with 382 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/ember-runtime/lib/mixins.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
114 changes: 114 additions & 0 deletions packages/ember-runtime/lib/mixins/deferred.js
Original file line number Diff line number Diff line change
@@ -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()
});
267 changes: 267 additions & 0 deletions packages/ember-runtime/tests/mixins/deferred_test.js
Original file line number Diff line number Diff line change
@@ -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

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

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

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

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

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

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

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.