Skip to content

Instantly share code, notes, and snippets.

@m0rjc
Last active Aug 29, 2015
Embed
What would you like to do?
My second attempt at Javascript Promises (aka Futures), in native Javascript
/**
* Implementation of Javascript Promises.
* A simplified version of the U4.Promise framework I wrote for Unit4 in 2013.
* Building it up as I need it.
*/
var Promise = (function(){
/**
* @private
* Copy properties from one object to another.
* @param target
* @param source
*/
function applyProperties(target, source) {
for(var key in source) {
if(source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
}
/**
* Call handler for every own property in obj. Works on arrays and objects
* @param obj
* @param handler
* @param thisArg
*/
function each (obj, handler, thisArg) {
var key;
for (key in obj){
if(obj.hasOwnProperty(key)){
handler.call(thisArg || this, obj[key]);
}
}
}
/**
* Call handler for every own property in obj, until handler returns truthy.
* @param obj
* @param handler
* @param thisArg
*/
function some (obj, handler, thisArg) {
var key;
for (key in obj){
if(obj.hasOwnProperty(key)){
if(handler.call(thisArg || this, obj[key])){
break;
}
}
}
}
/**
* Create a copy of obj, applying a transform to each own property.
* @param obj
* @param transform
* @param thisArg
* @returns {Array}
*/
function map (obj, transform, thisArg) {
var result = Array.isArray(obj) ? [] : {},
key;
for (key in obj){
if(obj.hasOwnProperty(key)){
result[key] = transform.call(thisArg || this, obj[key]);
}
}
return result;
}
/**
* @param x
* @returns {*|boolean|Boolean} true if x is a Promise.
*/
function isPromise(x) {
return x && x.isPromise;
}
/**
* Do nothing.
*/
function noOperation() {}
/**
* Base Promise class.
* @constructor
*/
function Promise() {
this._promiseConstructor();
return this;
}
Promise.prototype = {
/** @property {Boolean} isPromise marker to say it's a promise. */
isPromise: true,
/** @property {Boolean} resolved true if the promise is resolved. */
resolved: false,
/** @property {Boolean} rejected true if the promise is rejected. */
rejected: false,
/** @property {*} result result if we have one. */
result: undefined,
/** @property {*} error error if we have one. */
error: undefined,
_promiseConstructor : function () {
/** @property {Promise[]} _next ongoing promises in success case. */
this._next = [];
/** @property {Promise[]} _otherwise ongoing promises in fail case. */
this._otherwise = [];
},
/**
* Register something to do when the promise resolves.
* @param {Function} handler handler function to call.
* @param {*} handler.result the upstream result
* @param {Promise|*} handler.returns handler returns its result or a Promise of its result.
* @param {Object} [scope] scope to call handler in.
* @returns {Promise} promise of the handler's completion.
*/
then: function (handler, scope) {
return this.thenResolve(new OngoingPromise(handler, scope));
},
/**
* Register another Promise as something to do when this promise resolves.
* The Promise will be rejected if this promise rejects.
* @param downstreamPromise
* @returns {*}
*/
thenResolve: function (downstreamPromise) {
var me = this;
if(me.resolved) {
downstreamPromise.onUpstreamResolved(me.result);
} else if (me.rejected) {
downstreamPromise.onUpstreamRejected(me.error);
} else {
me._next.push(downstreamPromise);
}
return downstreamPromise;
},
/**
* Register something to do when the promise Rejects.
* @param handler
* @param [scope]
* @returns {*}
*/
otherwise: function (handler, scope) {
return this.otherwiseResolve(new OngoingPromise(handler, scope));
},
/**
* Register another Promise as something to do when this promise rejects.
* The Promise will only be resolved if this promise or an upstream rejects.
* @param {Promise} downstreamPromise
*/
otherwiseResolve: function (downstreamPromise) {
var me = this;
if (me.rejected) {
downstreamPromise.onUpstreamResolved(me.error);
} else if (!me.resolved) {
me._otherwise.push(downstreamPromise);
}
return downstreamPromise;
},
/**
* Register something to do regardless of the result of the promise.
* @param {Function} handler function to call
* @param {Object} handler.arg composite containing the result
* @param {Object} handler.arg.result success result.
* @param {Object} handler.arg.error error result.
* @param {Boolean} handler.arg.isSuccess true if success
* @param {Object} [scope] scope for the handler
* @returns {Promise} promise of the handler completing.
*/
thenAlways: function (handler, scope) {
return this.alwaysResolve(new OngoingPromise(handler, scope));
},
/**
* Register a promise to call regardless of the outcome of this promise.
* The result will be a composite as described in {@link #thenAlways}.
* @param {Promise} downstreamPromise
*/
alwaysResolve: function (downstreamPromise) {
this.then(function(result){
downstreamPromise.onUpstreamResolved({
isSuccess: true,
result: result
});
});
this.otherwise(function(error){
downstreamPromise.onUpstreamResolved({
isSuccess: false,
error: error
});
});
return downstreamPromise;
},
/**
* If an error transforms through this point then it can be transformed.
* Allows something like "The warp drive is broken." to be transformed to
* "Problem jumping into hyperspace. The warp drive is broken."
*
* @param {Function} transform method to transform the error
* @param {*} transform.error the error being passed.
* @param {!Promise} transform.return the transformed error. This must be synchronous.
* @param {Object} [scope] scope for the transform function.
*/
transformError: function (transform, scope) {
return this.thenResolve(new TransformErrorPromise(transform, scope));
},
/**
* @protected
* Handle an upstream promise resolving.
* The base class immediately rejects itself.
* @param {*} error the error from the upstream promise.
*/
onUpstreamResolved: function (result) {
this.resolve(result);
},
/**
* @protected
* Handle an upstream promise rejecting.
* The base class immediately resolves itself.
* @param {*} result the result from the upstream promise.
*/
onUpstreamRejected: function (error) {
this.reject(error);
},
/**
* Resolve the Promise. Can only be called once.
* @param {Promise|*} [result] the result or the promise of the result.
*/
resolve: function (result) {
if (result && result.isPromise) {
result.thenResolve(this);
} else {
this.resolved = true;
this.result = result;
this._next.forEach(function (promise) {
promise.onUpstreamResolved(result);
});
this.inhibitFutureInput();
}
},
inhibitFutureInput: function() {
delete this._next;
delete this._otherwise;
this.resolve = noOperation;
this.reject = noOperation;
},
/**
* Reject the Promise. Can only be called once.
* @param {*|!Promise} [error] error result. Cannot be a promise.
*/
reject: function (error) {
this.rejected = true;
this.error = error;
this._next.forEach(function (promise) {
promise.onUpstreamRejected(error);
});
this._otherwise.forEach(function (promise) {
promise.onUpstreamResolved(error);
});
this.inhibitFutureInput();
},
/**
* @return {Boolean} true if resolved or rejected
*/
isComplete: function() {
return this.resolved || this.rejected;
}
};
/**
* @static
* Convenience function to create a rejected promise. When returned in a Promise handler, causes a
* reject.
* @param error the error to reject with.
*/
Promise.reject = function(error) {
var promise = new Promise();
promise.reject(error);
return promise;
};
/**
* @static
* Convert an unknown into a Promise.
* @param {Promise|*} result if a Promise then wait on that promise, otherwise return a promise resolved with the result.
* @returns {Promise}
*/
Promise.resolve = function(result) {
var promise;
if(isPromise(result)){
return result;
}
promise = new Promise();
promise.resolve(result);
return promise;
};
/**
* @private a promise with a handler, downstream of an original promise.
* @param handler
* @param scope
* @constructor
*/
function OngoingPromise(handler, scope){
this._promiseConstructor();
this.handler = handler;
this.scope = scope;
}
OngoingPromise.prototype = new Promise();
applyProperties(OngoingPromise.prototype, {
onUpstreamResolved: function(result) {
var nextResult;
try {
nextResult = this.handler.call(this.scope || this, result);
// If we re-enter onUpstreamResolved it is because nextResult
// was a Promise, so we want to resolve immediately.
this.onUpstreamResolved = this.resolve;
this.resolve(nextResult);
} catch (e) {
this.reject(e);
}
}
});
/**
* @private
* A promise that transforms errors that pass through it.
* @param {Function} transform
* @param {Object} [scope]
* @constructor
*/
function TransformErrorPromise(transform, scope){
this._promiseConstructor();
this.transform = transform;
this.scope = scope;
}
TransformErrorPromise.prototype = new Promise();
applyProperties(TransformErrorPromise.prototype, {
onUpstreamRejected: function (error) {
var newError;
try {
newError = this.transform.call(this.scope || this, error);
} catch (e) {
newError = 'Error transforming error. Original error was: ' + error;
}
this.reject(newError);
}
});
/**
* @static
* Create a Promise which resolves when all requirements have resolved. **Fast Reject Version**
*
* The Result will be a Map or Array matching Requirements with the Promise results in each place.
* If Rejected then the Error that from the first found rejection. The Promise will reject as soon as
* an upstream Promise rejects.
*
* @param {Object|Array} requirements promises or results to wait for.
* then we will wait and collect all results.
*
* If fastReject is true then the rejection passed down will be the first one encountered with no information about
* where it came from. If it is false then the rejection will be an Object/Array of promises based on the original
* requirement to allow interrogation.
*/
Promise.when = function(requirements) {
return new JoinPromise(requirements, true);
};
/**
* @static
* Create a Promise which resolves when all requirements have **Slow Reject Version**
*
* The Result will be a Map or Array matching Requirements with the Promise results in each place.
* If Rejected then the Error will be the same Map or Array of each Promise. Promises will be created
* to wrap non-Promise inputs.
*
* @param {Object|Array} requirements promises or results to wait for.
* then we will wait and collect all results.
*
* If fastReject is true then the rejection passed down will be the first one encountered with no information about
* where it came from. If it is false then the rejection will be an Object/Array of promises based on the original
* requirement to allow interrogation.
*/
Promise.whenAllWithSlowReject = function(requirements) {
return new JoinPromise(requirements, false);
};
/**
* @private
* @param requirements
* @param fastReject
* @constructor
*/
function JoinPromise(requirements, fastReject) {
this.requirements = requirements;
this.fastReject = !!fastReject;
this.onUpstreamStateChange();
each(requirements, function(requirement){
if (isPromise(requirement)) {
// This comes into our upstreamResolve method which is overridden
// to calculate the real state.
requirement.alwaysResolve(this);
}
}, this)
}
JoinPromise.prototype = new Promise();
applyProperties(JoinPromise.prototype, {
onUpstreamStateChange: function () {
var allOk = true,
allComplete = true,
someError = false,
requirements = this.requirements;
if(!this.isComplete()){
each(requirements, function(requirement){
if(isPromise(requirement)) {
allOk &= requirement.resolved;
allComplete &= (requirement.rejected || requirement.resolved);
someError |= requirement.rejected;
}
});
if(allOk){
this.doResolve();
} else if(allComplete || (this.fastReject && someError)){
this.doReject();
}
}
},
doResolve: function(){
this.resolve(map(this.requirements, function(requirement){
return isPromise(requirement) ? requirement.result : requirement;
}));
},
doReject: function(){
if(this.fastReject){
// Reject with the first error found.
var error = undefined;
some(this.requirements, function(requirement){
if(requirement && isPromise(requirement) && requirement.rejected){
error = requirement.error;
return true;
}
});
this.reject(error);
} else {
// Reject with full information.
this.reject(map(this.requirements, function(requirement){
return Promise.resolve(requirement);
}));
}
},
onUpstreamResolved: function(){
this.onUpstreamStateChange();
},
onUpstreamRejected: function(){
this.onUpstreamStateChange();
}
});
return Promise;
})();
describe('Promise', function() {
/** Promise handler to add one to the input. */
function addOne(x){return x+1;}
/** Promise handler to multiply the input by two. */
function timesTwo(x){return x*2; }
/** Promise handler that throws 'An Exception' */
function throwException(x){throw 'An Exception';}
/** Promise handler that returns a Rejected promise with message 'Reject'. */
function returnReject(x){return Promise.reject('Reject');}
/** Create a promise handler to store its input on its scope with the given key, and pass that input on. */
function storeResult(key){
return function(x){this[key] = x; return x;}
}
/** Create a prmise handler to increment a counter in its scope, then pass its input on. */
function increment(key){
return function(){this[key]++; return x;}
}
function assertPromiseReady(promise){
expect(promise.resolved).toBe(false, 'assertPromiseReady: expected resolved to be false');
expect(promise.rejected).toBe(false, 'assertPromiseReady: rejected resolved to be false');
expect(promise.isComplete()).toBe(false, 'assertPromiseReady: expected isComplete() to be false');
expect(promise.result).toBeUndefined('assertPromiseReady: expected result to be undefined');
expect(promise.error).toBeUndefined('assertPromiseReady: expected error to be undefined');
}
function assertPromiseResolved(promise, expectedResult) {
expect(promise.resolved).toBe(true, 'assertPromiseResolved: expected resolved to be true');
expect(promise.rejected).toBe(false, 'assertPromiseResolved: rejected resolved to be false');
expect(promise.isComplete()).toBe(true, 'assertPromiseResolved: expected isComplete() to be true');
expect(promise.result).toEqual(expectedResult, 'assertPromiseResolved: expected result');
expect(promise.error).toBeUndefined('assertPromiseResolved: expected error to be undefined');
}
function assertPromiseRejected(promise, expectedError) {
expect(promise.resolved).toBe(false, 'assertPromiseRejected: expected resolved to be false');
expect(promise.rejected).toBe(true, 'assertPromiseRejected: expected rejected to be true');
expect(promise.isComplete()).toBe(true, 'assertPromiseRejected: expected isComplete() to be true');
expect(promise.result).toBeUndefined('assertPromiseRejected: expected result to be undefined');
expect(promise.error).toEqual(expectedError, 'assertPromiseRejected: expected error');
}
describe('new Promise()', function() {
it('is initially unresolved and unrejected', function () {
var p = new Promise();
assertPromiseReady(p);
});
});
describe('resolve()', function(){
it('becomes resolved and complete once resolved', function(){
var p = new Promise();
p.resolve('Fred');
assertPromiseResolved(p, 'Fred');
});
it('can accept no argument as an answer', function(){
var p = new Promise();
p.resolve();
assertPromiseResolved(p, undefined);
});
it('can accept a complex argument as an answer', function(){
var p = new Promise(),
expectedResult = {foo:'bar'};
p.resolve(expectedResult);
assertPromiseResolved(p, expectedResult);
expect(p.result).toBe(expectedResult); // No clones
});
it('can be resolved with a Promise, so allowing casting to Promise', function(){
var promise1 = new Promise(),
promise2 = new Promise();
promise1.then(timesTwo).then(storeResult('result'), this);
promise1.resolve(promise2);
assertPromiseReady(promise1);
promise2.resolve(5);
assertPromiseResolved(promise1, 5); // Initial value resolved with
expect(this.result).toBe(10); // End result of the chain.
});
it('can be resolved with a Promise, fires immediately if the Promise is already resolved', function(){
var promise1 = new Promise(),
promise2 = new Promise();
promise2.resolve(5);
promise1.then(timesTwo).then(storeResult('result'), this);
promise1.resolve(promise2);
assertPromiseResolved(promise1, 5); // Initial value resolved with
expect(this.result).toBe(10); // End result of the chain.
});
it('if already resolved resists further attempts to resolve it', function(){
var p = new Promise(),
result = {
count: 0
};
p.then(storeResult('result'), result).then(increment('count'), result);
p.resolve('x');
expect(result).toEqual({count: 1, result: 'x'});
p.resolve('y');
expect(result).toEqual({count: 1, result: 'x'});
assertPromiseResolved(p, 'x');
});
it('if already rejected resists further attempts to resolve it', function(){
var p = new Promise(),
result = {
count: 0
};
p.then(storeResult('result'), result).then(increment('count'), result);
p.reject('x');
p.resolve('y');
expect(result).toEqual({count: 0});
assertPromiseRejected(p, 'x');
});
});
describe('reject()', function(){
it('becomes rejected and complete once rejected', function(){
var p = new Promise();
p.reject('An Error');
assertPromiseRejected(p, 'An Error');
});
it('can accept no argument as an error', function(){
var p = new Promise();
p.reject();
assertPromiseRejected(p, undefined);
});
it('can accept a complex argument as an answer', function(){
var p = new Promise(),
expectedResult = {error: true, msg:'bar'};
p.reject(expectedResult);
assertPromiseRejected(p, expectedResult);
expect(p.error).toBe(expectedResult); // No clones
});
it('if already resolved resists further attempts to reject it', function(){
var p = new Promise(),
result = {
count: 0
};
p.otherwise(storeResult('error'), result).then(increment('count'), result);
p.reject('x');
expect(result).toEqual({count: 1, error: 'x'});
p.reject('y');
expect(result).toEqual({count: 1, error: 'x'});
assertPromiseRejected(p, 'x');
});
it('if already rejected resists further attempts to resolve it', function(){
var p = new Promise(),
result = {
count: 0
};
p.then(storeResult('result'), result).then(increment('count'), result)
.otherwise(storeResult('error'), result).then(increment('count'), result);
p.reject('x');
expect(result).toEqual({count: 1, error: 'x'});
p.resolve('y');
expect(result).toEqual({count: 1, error: 'x'});
assertPromiseRejected(p, 'x');
});
});
describe('then()', function() {
it('can call multiple handlers once resolved', function () {
var p = new Promise(),
result1 = '--UNSET--',
result2 = '--UNSET--';
p.then(function (x) {
result1 = x;
});
p.then(function (x) {
result2 = x;
});
expect(result1).toBe('--UNSET--');
p.resolve(12);
expect(result1).toBe(12);
expect(result2).toBe(12);
});
it('accepts a scope parameter for handlers', function () {
var p = new Promise(),
capturedScope = '--UNSET--',
requiredScope = {foo: 'Bar'};
p.then(function () {
capturedScope = this
}, requiredScope);
p.resolve(12);
expect(capturedScope).toBe(requiredScope);
});
it('calls an added handler immediately if already resolved', function () {
var p = new Promise(),
result = '--UNSET--';
p.resolve(34);
p.then(function (x) {
result = x;
});
expect(result).toBe(34);
});
it('does not call its handlers once rejected', function () {
var p = new Promise(),
result = '--UNSET--';
p.then(function (x) {
result = x;
});
p.reject('An Error');
expect(result).toBe('--UNSET--');
});
});
describe('otherwise()', function() {
it('can call multiple error handlers once rejected', function () {
var p = new Promise(),
result1 = '--UNSET--',
result2 = '--UNSET--';
p.otherwise(function (x) {
result1 = x;
});
p.otherwise(function (x) {
result2 = x;
});
expect(result1).toBe('--UNSET--');
p.reject('An Error');
expect(result1).toBe('An Error');
expect(result2).toBe('An Error');
});
it('accepts a scope parameter for handlers', function () {
var p = new Promise(),
capturedScope = '--UNSET--',
requiredScope = {foo: 'Bar'};
p.otherwise(function () {
capturedScope = this
}, requiredScope);
p.reject('An Error');
expect(capturedScope).toBe(requiredScope);
});
it('calls an added error handler immediately once rejected', function () {
var p = new Promise(),
result = '--UNSET--';
p.reject('An Error');
p.otherwise(function(x){
result = x;
});
expect(result).toBe('An Error');
});
it('does not call its error handlers once resolved', function () {
var p = new Promise(),
result = '--UNSET--';
p.otherwise(function (x) {
result = x;
});
p.resolve('The Result');
expect(result).toBe('--UNSET--');
});
});
describe('thenAlways()', function() {
it('can call a handler for resolve and reject, resolve case', function(){
var p = new Promise(),
result = '--UNSET--';
p.thenAlways(function(x){
result = x;
});
p.resolve('Result');
expect(result).toEqual({
result: 'Result',
isSuccess: true
});
});
it('can call a handler for resolve and reject, resolve case - previously resolved', function(){
var p = new Promise(),
result = '--UNSET--';
p.resolve('Result');
p.thenAlways(function(x){
result = x;
});
expect(result).toEqual({
result: 'Result',
isSuccess: true
});
});
it('can call a handler for resolve and reject, reject case', function(){
var p = new Promise(),
result = '--UNSET--';
p.thenAlways(function(x){
result = x;
});
p.reject('An Error');
expect(result).toEqual({
error: 'An Error',
isSuccess: false
});
});
it('can call a handler for resolve and reject, reject case - previously rejected', function(){
var p = new Promise(),
result = '--UNSET--';
p.reject('An Error');
p.thenAlways(function(x){
result = x;
});
expect(result).toEqual({
error: 'An Error',
isSuccess: false
});
});
// These were not expected to work seeing the code. They were found to work
// because of work done to prevent re-entry of Promises. As long as all tests
// in this section pass then we can be confident. TDD means we don't need to
// solve it as it already works.
it('does not call the handler twice on reject', function(){
var p = new Promise(),
result = {count: 0};
p.thenAlways(increment('count'), result);
p.reject('Foo')
expect(result.count).toBe(1);
});
it('does not call the handler twice on prior reject', function(){
var p = new Promise(),
result = {count: 0};
p.reject('Foo')
p.thenAlways(increment('count'), result);
expect(result.count).toBe(1);
});
it('does not call the handler twice on prior resolve', function(){
var p = new Promise(),
result = {count: 0};
p.resolve('Foo')
p.thenAlways(increment('count'), result);
expect(result.count).toBe(1);
});
});
describe('Promise Chaining', function(){
it('Promises can be chained to produce a new Promise of the result of its handler', function(){
var firstPromise = new Promise(),
lastPromise = firstPromise.then(addOne).then(timesTwo).then(storeResult('Result'), this);
firstPromise.resolve(10);
expect(this.Result).toBe(22);
assertPromiseResolved(lastPromise, 22);
});
it('Promises can be chained to produce a new Promise of the result of its handler - Previously Resolve Case', function(){
var firstPromise = new Promise(),
lastPromise;
firstPromise.resolve(10);
lastPromise = firstPromise.then(addOne).then(timesTwo).then(storeResult('Result'), this);
expect(this.Result).toBe(22);
assertPromiseResolved(lastPromise, 22);
});
it('propagates rejects down the chain', function(){
var firstPromise = new Promise(),
lastPromise = firstPromise.then(addOne).then(timesTwo),
error = { error: true, message: 'An Error' };
lastPromise.otherwise(storeResult('capturedError'), this);
firstPromise.reject(error);
expect(this.capturedError).toBe(error); // Not a clone.
assertPromiseRejected(lastPromise, error);
});
it('A Promise can be rejected by throwing an exception in the handler', function(){
var firstPromise = new Promise(),
lastPromise = firstPromise.then(throwException).then(timesTwo);
lastPromise.otherwise(storeResult('capturedError'), this);
firstPromise.resolve(10);
expect(this.capturedError).toBe('An Exception');
assertPromiseRejected(lastPromise, 'An Exception');
});
it('A Promise can be rejected by returning Promise.reject() in the handler', function(){
var firstPromise = new Promise(),
lastPromise = firstPromise.then(returnReject).then(timesTwo);
lastPromise.otherwise(storeResult('capturedError'), this);
firstPromise.resolve(10);
expect(this.capturedError).toBe('Reject');
assertPromiseRejected(lastPromise, 'Reject');
});
it('will wait for a Promise returned by a handler to be resolved, then resolve', function(){
var initialPromise = new Promise(),
innerPromise = new Promise(),
finalPromise;
finalPromise = initialPromise.then(function(){return innerPromise;}).then(storeResult('result'), this);
assertPromiseReady(finalPromise);
initialPromise.resolve('Outer Result');
assertPromiseReady(finalPromise);
innerPromise.resolve('Inner Result');
assertPromiseResolved(finalPromise, 'Inner Result');
expect(this.result).toBe('Inner Result');
});
it('will all a hander to return a pre-resolved promise, then resolve', function(){
var initialPromise = new Promise(),
innerPromise = new Promise(),
finalPromise;
innerPromise.resolve('Inner Result');
finalPromise = initialPromise.then(function(){return innerPromise;}).then(storeResult('result'), this);
assertPromiseReady(finalPromise);
initialPromise.resolve('Outer Result');
assertPromiseResolved(finalPromise, 'Inner Result');
expect(this.result).toBe('Inner Result');
});
it('will wait for a Promise returned by a handler to be rejected, then reject', function(){
var initialPromise = new Promise(),
innerPromise = new Promise(),
finalPromise;
finalPromise = initialPromise.then(function(){return innerPromise;});
finalPromise.otherwise(storeResult('capturedError'), this);
initialPromise.resolve();
innerPromise.reject('Inner Error');
assertPromiseRejected(finalPromise, 'Inner Error');
expect(this.capturedError).toBe('Inner Error');
});
});
describe('thenResolve()', function() {
it('can be asked to resolve another promise when resolved. Returns that Promise', function () {
var promise1 = new Promise(),
promise2 = new Promise(),
results = {};
promise2.then(storeResult('p2'), results);
promise1.thenResolve(promise2).then(timesTwo).then(storeResult('endResult'), results);
assertPromiseReady(promise1);
assertPromiseReady(promise2);
promise1.resolve(2);
assertPromiseResolved(promise1, 2); // Initial value resolved with
assertPromiseResolved(promise2, 2); // Initial value resolved with
expect(results.p2).toBe(2); // Promise2 is resolved with the result of Promise1
expect(results.endResult).toBe(4);
});
it('resolves immediately if the promise is already resolved', function () {
var promise1 = new Promise(),
promise2 = new Promise(),
results = {};
promise2.then(storeResult('p2'), results);
promise1.resolve(2);
promise1.thenResolve(promise2).then(timesTwo).then(storeResult('endResult'), results);
assertPromiseResolved(promise1, 2); // Initial value resolved with
assertPromiseResolved(promise2, 2); // Initial value resolved with
expect(results.p2).toBe(2); // Promise2 is resolved with the result of Promise1
expect(results.endResult).toBe(4);
});
it('rejects the other promise if this promise is rejected', function(){
var promise1 = new Promise(),
promise2 = new Promise(),
results = {};
promise2.then(storeResult('p2'), results).otherwise(storeResult('p2Error'), results);
promise1.thenResolve(promise2);
promise1.reject('An Error');
assertPromiseRejected(promise1, 'An Error');
assertPromiseRejected(promise2, 'An Error');
expect(results.p2).toBeUndefined();
expect(results.p2Error).toBe('An Error');
});
it('rejects the other promise immediately if this promise is already rejected', function(){
var promise1 = new Promise(),
promise2 = new Promise(),
results = {};
promise2.then(storeResult('p2'), results).otherwise(storeResult('p2Error'), results);
promise1.reject('An Error');
promise1.thenResolve(promise2);
assertPromiseRejected(promise1, 'An Error');
assertPromiseRejected(promise2, 'An Error');
expect(results.p2).toBeUndefined();
expect(results.p2Error).toBe('An Error');
});
});
describe('otherwiseResolve()', function(){
it('can be asked to resolve another promise when rejected. Returns that Promise', function(){
var promise1 = new Promise(),
promise2 = new Promise(),
results = {};
promise2.then(storeResult('p2'), results);
promise1.otherwiseResolve(promise2).then(timesTwo).then(storeResult('endResult'), results);
assertPromiseReady(promise1);
assertPromiseReady(promise2);
promise1.reject(2);
assertPromiseRejected(promise1, 2); // Initial value resolved with
assertPromiseResolved(promise2, 2); // Initial value resolved with
expect(results.p2).toBe(2); // Promise2 is resolved with the result of Promise1
expect(results.endResult).toBe(4);
});
it('rejects immediately if the promise is already rejected', function(){
var promise1 = new Promise(),
promise2 = new Promise(),
results = {};
promise2.then(storeResult('p2'), results);
promise1.reject(2);
promise1.otherwiseResolve(promise2).then(timesTwo).then(storeResult('endResult'), results);
assertPromiseRejected(promise1, 2); // Initial value resolved with
assertPromiseResolved(promise2, 2); // Initial value resolved with
expect(results.p2).toBe(2); // Promise2 is resolved with the result of Promise1
expect(results.endResult).toBe(4);
});
/**
* Rejection is assumed for error handling not flow control, so we don't start off rejecting
* an error chain in the inital promise succeeded.
*/
it('does not reject the "otherwise Promise" when resolved', function(){
var promise1 = new Promise(),
promise2 = new Promise(),
results = {};
promise2.then(storeResult('p2'), results);
promise1.otherwiseResolve(promise2).then(timesTwo).then(storeResult('endResult'), results);
assertPromiseReady(promise1);
assertPromiseReady(promise2);
promise1.resolve(2);
assertPromiseResolved(promise1, 2); // Initial value resolved with
assertPromiseReady(promise2); // Never completes.
expect(results.endResult).toBeUndefined();
});
});
describe('transformError()', function(){
it('can transform an error passing through a chain of Promises', function(){
var externalSystemCall = new Promise();
function someServiceCall(){
return externalSystemCall
.then(addOne)
.transformError(function(e){
return 'Problem adding one to an external result: ' + e;
});
}
someServiceCall().otherwise(storeResult('capturedFinalError'), this);
externalSystemCall.reject('External Service Error');
expect(this.capturedFinalError).toBe('Problem adding one to an external result: External Service Error');
});
});
describe('Promise.resolve() - can be used to turn an unknown into a Promise', function(){
it('accepts a value and returns a resolved promise of that value', function(){
var p = Promise.resolve(2);
assertPromiseResolved(p, 2);
});
it('accepts no argument and returns a resolved promise of undefined value', function(){
var p = Promise.resolve();
assertPromiseResolved(p, undefined);
});
it('accepts a Promise and returns that same Promise', function(){
var p1 = new Promise(),
p2 = Promise.resolve(p1);
expect(p2).toBe(p1);
});
});
describe('Promise.when() - Joining promises', function(){
it('can wait on multiple Promises, success case, Object requirements', function(){
var p1 = new Promise(),
p2 = new Promise(),
p3 = new Promise(),
joined = Promise.when({
p1: p1,
p2: p2,
p3: p3
});
assertPromiseReady(joined);
p1.resolve(1);
assertPromiseReady(joined);
p3.resolve(3);
assertPromiseReady(joined);
p2.resolve(2);
assertPromiseResolved(joined, {
p1: 1,
p2: 2,
p3: 3
});
});
it('can wait on a mix of Promises and literals, success case, Object requirements', function(){
var p1 = new Promise(),
p3 = new Promise(),
joined = Promise.when({
p1: p1,
literal: 'Hello',
p3: p3
});
assertPromiseReady(joined);
p1.resolve(1);
assertPromiseReady(joined);
p3.resolve(3);
assertPromiseResolved(joined, {
p1: 1,
literal: 'Hello',
p3: 3
});
});
it('can wait on a mix of Promises and literals when a literal is undefined', function(){
var p1 = new Promise(),
p3 = new Promise(),
joined = Promise.when({
p1: p1,
literal: undefined,
p3: p3
});
assertPromiseReady(joined);
p1.resolve(1);
assertPromiseReady(joined);
p3.resolve(3);
assertPromiseResolved(joined, {
p1: 1,
literal: undefined,
p3: 3
})
});
it('can wait on multiple Promises, success case, Array requirements', function(){
var p1 = new Promise(),
p2 = new Promise(),
p3 = new Promise(),
joined = Promise.when([p1, p2, p3]);
assertPromiseReady(joined);
p1.resolve(1);
assertPromiseReady(joined);
p3.resolve(3);
assertPromiseReady(joined);
p2.resolve(2);
assertPromiseResolved(joined, [1, 2, 3]);
});
it('can wait on multiple Promises, success case, Array requirements with literals', function(){
var p1 = new Promise(),
p3 = new Promise(),
joined = Promise.when([p1, 2, p3]);
assertPromiseReady(joined);
p1.resolve(1);
assertPromiseReady(joined);
p3.resolve(3);
assertPromiseResolved(joined, [1, 2, 3]);
});
it('can wait on multiple Promises, success case, Object requirements, all previously resolved', function(){
var p = Promise.when({
p1: Promise.resolve(1),
p2: Promise.resolve(2),
pu: Promise.resolve(),
l1: 'Literal'
});
assertPromiseResolved(p, {
p1: 1,
p2: 2,
pu: undefined,
l1: 'Literal'
});
});
it('can wait on multiple Promises, error case, slow reject, Object requirements - rejects with Object containing Promises', function(){
var p1 = new Promise(),
p2 = new Promise(),
p3 = new Promise(),
joined = Promise.whenAllWithSlowReject({
p1: p1,
p2: p2,
p3: p3,
l1: 'Literal'
});
assertPromiseReady(joined);
p1.reject('E1');
assertPromiseReady(joined);
p2.resolve(2);
assertPromiseReady(joined);
p3.resolve(3);
assertPromiseRejected(joined, {
p1: p1,
p2: p2,
p3: p3,
l1: Promise.resolve('Literal')
});
});
it('can wait on multiple Promises, error case, slow reject, Array Requirements - rejects with Promise array', function(){
var p1 = new Promise(),
p2 = new Promise(),
p3 = new Promise(),
joined = Promise.whenAllWithSlowReject([p1, p2, p3, 'Literal']);
assertPromiseReady(joined);
p1.reject('E1');
assertPromiseReady(joined);
p2.resolve(2);
assertPromiseReady(joined);
p3.resolve(3);
assertPromiseRejected(joined, [p1, p2, p3, Promise.resolve('Literal')]);
});
it('can wait on multiple Promises, error case, fast reject, Object requirements - rejects with single error', function(){
var p1 = new Promise(),
p2 = new Promise(),
p3 = new Promise(),
joined = Promise.when({
p1: p1,
p2: p2,
p3: p3,
l1: 'Literal'
});
assertPromiseReady(joined);
p1.resolve(1);
assertPromiseReady(joined);
p2.reject('E2');
assertPromiseRejected(joined, 'E2');
});
it('can wait on multiple Promises, error case, fast reject, Array requirements - rejects with single error', function(){
var p1 = new Promise(),
p2 = new Promise(),
p3 = new Promise(),
joined = Promise.when([p1, p2, p3, 'Literal']);
assertPromiseReady(joined);
p1.resolve(1);
assertPromiseReady(joined);
p2.reject('E2');
assertPromiseRejected(joined, 'E2');
});
it('can wait on multiple Promises - once fast reject has fired future results are ignored', function(){
var p1 = new Promise(),
p2 = new Promise(),
p3 = new Promise(),
p4 = new Promise(),
joined = Promise.when([p1, p2, p3, 'Literal', p4]),
captureCount = 0;
function capture(){
captureCount++;
}
joined.thenAlways(capture);
assertPromiseReady(joined);
p1.resolve(1);
assertPromiseReady(joined);
p2.reject('E2');
assertPromiseRejected(joined, 'E2');
p3.reject('E3');
assertPromiseRejected(joined, 'E2');
p4.resolve(4);
assertPromiseRejected(joined, 'E2');
expect(captureCount).toBe(1);
});
it('can wait on multiple Promises, error case, Object requirements, all previously rejected - rejects immediately', function(){
var p1 = Promise.reject('E1'),
p2 = Promise.reject('E2');
Promise.whenAllWithSlowReject({
p1: p1,
p2: p2
}).otherwise(storeResult('result'), this);
expect(this.result).toEqual({
p1: p1,
p2: p2
});
});
it('can wait on multiple Promises, error case, Object requirements, fast reject, some previously rejected - rejects immediately', function(){
var result = {};
Promise.when({
p1: Promise.reject('E1'),
p2: new Promise(),
l1: 'Literal'
}).otherwise(storeResult('error'), result);
expect(result.error).toBe('E1');
});
});
});
@m0rjc
Copy link
Author

m0rjc commented Mar 21, 2015

This is not complete. I've not needed JoinPromise yet in my gallery app, so have not written it.

@m0rjc
Copy link
Author

m0rjc commented Mar 24, 2015

Implemented joining and unit tests.

Failing Tests: Error handling when joining promises using Promise.when is incomplete.

@m0rjc
Copy link
Author

m0rjc commented Mar 30, 2015

I've spent some time at work porting this to Ext-JS and in doing some found some ways it can be better. It all passes the unit tests (assuming I've uploaded latest which I can do) but there are things that can be Better. Given time I can make the tweaks - notably:

  • The subclass prototypes get the Promise initialiser, so carry the lists. Either lazy instantiate the lists or delete them from the prototype. Does not cause a problem.
  • A condition rather than overwriting method pointers makes sense to inhibit future firing. It was an interesting idea, but the condition more understandable.
  • The then method can better follow Promises-A and take a fail handler as second argument allowing then(handler, [failHandler], [scope])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment