Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Merge pull request #1406 from tchak/deferred-then

Add Ember.Deferred mixin which implements Promises/A spec
  • Loading branch information...
commit f7ac080db3a2a15f5814dc26fc86712cf7d252c8 2 parents 0184c2c + 2f8d5e6
@ebryn ebryn authored
View
1  packages/ember-runtime/lib/mixins.js
@@ -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');
View
114 packages/ember-runtime/lib/mixins/deferred.js
@@ -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()
+});
View
267 packages/ember-runtime/tests/mixins/deferred_test.js
@@ -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
Collaborator

@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.
Something went wrong with that request. Please try again.