-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1406 from tchak/deferred-then
Add Ember.Deferred mixin which implements Promises/A spec
- Loading branch information
Showing
3 changed files
with
382 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); |
f7ac080
There was a problem hiding this comment.
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
?
f7ac080
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From Promises/A:
Not one of these sentences is implemented.
f7ac080
There was a problem hiding this comment.
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?
f7ac080
There was a problem hiding this comment.
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.
f7ac080
There was a problem hiding this comment.
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.
f7ac080
There was a problem hiding this comment.
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?
f7ac080
There was a problem hiding this comment.
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.