Permalink
Browse files

Merge pull request #1406 from tchak/deferred-then

Add Ember.Deferred mixin which implements Promises/A spec
  • Loading branch information...
2 parents 0184c2c + 2f8d5e6 commit f7ac080db3a2a15f5814dc26fc86712cf7d252c8 @ebryn ebryn committed Oct 11, 2012
@@ -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

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

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
Member

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

@domenic

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

@domenic

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

@tlrobinson

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

@domenic

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.