Javascript Promise Xtended - Wrapper to add new methods w/o changing the native promise prototype
- built-in
defer
if needed - assignable context for all functions
- public string constants for better readability
- exposed
value
andstatus
of each promise - nice methods like
finally
,timeout
,any
ornodeify
- cancellable promise chain (partly or as long as you want)
- create cancellable promises by adding public methods to a created Promise
- change underlying promise at runtime - that way you don't need to redefine global promise
PromiseX
is NO plain Promise plugin, it is a wrapper to extend basic functionality.
Per default, PromiseX
uses the native Promise. If no promise is present, an error is thrown.
Since some implementations have better performance than native promise,
you can choose the base promise for PromiseX
. And better, you can have multiple instances with different base promises!!!
PromiseX.config('getPromise') === window.Promise; // true
var PromiseZousan = PromiseX.config('createPromise', Zousan); // https://github.com/bluejava/zousan
PromiseX.resolve('native').then(function() {
return PromiseZousan.resolve('zousan');
}).then(…);
PromiseX
with native Promise is passing all Tests of Compliance Test Suite.
It follows Promises/A+ Specification, but some points are extended.
For example Point 2.2.5 is true in general, but with PromiseX
you are free to add your desired context :D
- Install with yarn:
yarn add opusonline-promisex.js
- Install with npm:
npm install opusonline-promisex.js
You can support me and Donate Bitcoin: 1ERZVwAmd4bM3DWjtxionJE8m24wpCa5nV - Thanks!
executor
should be the execution function called with optional context,
if executor
is empty the promise is a deferred
with resolve and reject functions,
if executor
is a native promise the new object will be a wrapper for this promise
every other executor is treated as PromiseX.resolve(value)
// default use
var promise = new PromiseX(function(resolve, reject) {
var async = doSomeAsyncStuff();
async.onready = resolve;
async.onerror = reject;
});
// extended
var promise = new PromiseX(function(resolve, reject, self) {
var async = doSomeAsyncStuff();
async.onready = resolve;
async.onerror = reject;
self.cancel = function() {
reject(new Error('canceled'));
};
});
promise.cancel();
// context
var collection = {};
collection.promise = new PromiseX(function(resolve) {
this.resolve = function(value) {
resolve(value);
return this.promise;
};
}, collection);
collection.resolve('resolved').then(…);
// PromiseX.resolve(value)
var promise = new PromiseX('resolved');
// PromiseX.defer() including resolve/reject
var promise = new PromiseX();
promise.resolve('resolved');
// PromiseX.cast() or PromiseX.resolve() for underlying promises
var nativePromise = Promise.resolve('native');
var promiseX = new PromiseX(nativePromise);
promiseX.then(…);
// status and value
var promise = PromiseX.resolve('foo'); // promise.status == 'resolved'; promise.value == 'foo'
- PromiseX.PENDING
- PromiseX.RESOLVED
- PromiseX.REJECTED
var promise = Promise.resolve('foo');
if (promise.status === PromiseX.RESOLVED) {
doStuff();
}
just like standard Promise.then, always returns a new Promise
resolve function is executed if previous Promise is resolved
reject function is executed if previous Promise is rejected
resolve/reject functions are called with optional context
skipped if previous promise is cancelled
// default use
doAsync().then(function(value) {
console.log(value);
});
// resolve and reject
doAsync().then(function(value) {
console.log(value);
}, function(reason) {
console.error(reason);
});
// catch
doAsync().then(null, function(reason) {
console.error(reason);
});
// context
var collection = {};
doAsync().then(function(value) {
this.result = value;
}, null, collection); // null is for reject function
just like standard Promise.catch, always returns a new Promise
reject function is executed if previous Promise is rejected
shorthand for Promise.then(null, reject)
reject function is called with optional context
skipped if previous promise is cancelled
// default use
doAsync().catch(function(reason) {
console.error(reason);
});
// context
var collection = {};
doAsync().catch(function(reason) {
this.result = reason;
}, collection);
non-standard, always returns a new Promise
defined here: https://www.promisejs.org/api/#Promise_prototype_finally
callback is executed with optional context when Promise is fulfilled
previous resolved/rejected values are propagated to next Promise
attention: this behaves exactly like try-catch-finally
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Control_flow_and_error_handling#The_finally_block
and is a bit different to others regarding the return value of finally callback
skipped if previous promise is cancelled
// default use
showLoadingSpinner();
doAsync().finally(function() { // or just .finally(hideLoadingSpinner) but be sure that hideLoadingSpinner returns no value
hideLoadingSpinner();
// return;
}).then(function(value) {
// result of doAsync is shown here
console.log(value);
}).catch(function(reason) {
// errors in doAsync will be catched here
console.error(reason);
});
PromiseX.reject('error').finally(function() {
throw 'finally error';
}).catch(function(reason) {
// reason == 'finally error';
});
PromiseX.resolve('foo').finally(function() {
return 'bar';
}).then(function(reason) {
// reason == 'bar';
});
non-standard
does not return a promise, throws outside promises on next tick
defined here: https://www.promisejs.org/api/#Promise_prototype_done
if resolve/reject is/are provided, a last Promise.then is executed with optional context
skipped if previous promise is cancelled
// default use
doAsync().then(doStuff).done();
// then().done() in one
doAsync().done(doStuff);
non-standard
transforms Promise to node-like callback - meaning: callback(error, value)
defined here: https://www.promisejs.org/api/#Promise_prototype_nodify
skipped if previous promise is cancelled
doAsync().nodeify(function (error, result) {
if (error) {
console.error(error);
return;
}
doStuff(result);
});
non-standard
used in many Promise libraries like BluebirdJS
delays execution of next Promise in chain
previous value or error is propagated
skipped if previous promise is cancelled
doAsync().delay(5000).then(function(value) {
// 5s after doAsync resolves
// value is result of doAsync
}, function(reason) {
// 5s after doAsync rejects
// reason is error of doAsync
});
non-standard
cancelled promise chain can be catched here
resolve method gets reason as parameter
influence continuing by returning a value or throw
return nothing (undefined) means continue cancelling
getJSON('data').catch(function(reason) {
return PromiseX.cancel({message: 'Load Error', error: reason});
}).then(function(data) {
// success loading
// process data
// never called if cancelled before
return processData(data);
}).then(function(foo) {
// only called if not cancelled
duStuff(foo);
}).catch(function(reason) {
// even catch is never called if cancelled before!!!
}).cancelled(function(error) {
console.warn(error.message, error.error);
return 'from cancelled with <3'; // optional
}).then(function(message) {
// continues here
// message from cancelled handler (optional)
});
standard, returns a resolved Promise with given value
heads-up: if value is a PromiseX
, value is returned
PromiseX.resolve('done');
standard, returns a rejected Promise with given reason
PromiseX.reject('fail');
non standard
used in many Promise libraries like BluebirdJS
example here https://www.promisejs.org/patterns/#race
timeout for given Promise fulfillment
if reason is given, timeout Promise rejects with reason
heads-up: I refused doAsync().timeout() because I want to avoid using timeout later in promise chain
since setTimeout starts immediately when calling and not when promise starts
PromiseX.timeout(doAsync(), 5000).then(function (value) {
// result of doAsync in under 5000ms
}, function(reason) {
// error from doAsync in under 5000ms
// or reason.message == Timeout otherwise since Error('Timeout') is thrown
});
PromiseX.timeout(doAsync(), 5000, 'doAsyncTimeout').catch(function(reason) {
// error from doAsync in under 5000ms
// or reason.message == 'doAsyncTimeout'
});
// good practice
doAsync().then(function() {
return PromiseX.timeout(somePromise(), 5000); // timeout starts counting when then is executed
}).catch(function(reason) {
// reason.message == 'Timeout'
});
non-standard
returns a resolved Promise with given value after certain amount of time
PromiseX.delay(5000, 'foo').then(function(value) {
// 5 seconds later
// value == 'foo'
});
// fancy Promise timeout
PromiseX.race([doAsync(), PromiseX.delay(100).then(function(){
throw new Error('timeout');
})]).catch(function(reason) {
// reason == doAsync error or timeout
});
doAsync().then(function(value) {
return PromiseX.delay(5000, value);
}).then(function(value) {
// value == doAsync value; 5 seconds after doAsync resolved
});
non-standard
returns a deferred object including promise and
resolve and reject methods to fulfill the promise
https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred
attention: don't use deferred.promise; there is a promise
property but only for internal use!
function doAsync() {
var deferred = PromiseX.defer(); // could also be: deferred = new Promise();
setTimeout(function() {
if (test) {
deferred.resolve();
} else {
deferred.reject();
}
}, 5000);
return deferred;
}
doAsync().then(…)
ensures to return a PromiseX
promise
if value is a promise, return that promise
different to PromiseX.resolve since cast
transforms and resolve
gives a resolved PromiseX
http://www.wintellect.com/devcenter/nstieglitz/5-great-features-in-es6-harmony
// any value
PromiseX.cast('foo').then(…);
// promise
var promise = Promise.resolve();
PromiseX.cast(promise).then(…);
// PromiseX
var promise = new PromiseX();
PromiseX.cast(promise).then(…); // PromiseX.cast(promise) === promise
standard
returns a Promise that is resolved only if all promises are resolved
or rejected if any promise of list is rejected
resolve function gets array of promise values
following promises can be cancelled if any promise returns a cancel promise
PromiseX.all([doAsync1(), doAsync2()]).then(function(resolveArray) {
var value1 = resolveArray[0];
var value2 = resolveArray[1];
}).catch(function(reason) {
// could be any error from doAsync1 or doAsync2
});
// fancy usecase
var requests = PromiseX.map(['content1.json', 'content2.json'], getJSON); // same as [getJSON('content1.json'), getJSON('content2.json')]
PromiseX.all(requests).then(…).catch(…);
standard
returns a Promise that is resolved as soon as one promise is resolved
or rejected as soon as one promise of list is rejected
following promises can be cancelled if any promise returns a cancel promise
PromiseX.race([doAsync1(), doAsync2()]).then(function(value) {
// value could be resolved value from doAsync1 or doAsync2 deciding on faster one
}).catch(function() {
// could be any error from doAsync1 or doAsync2
});
// nice for timeout
PromiseX.race([doAsync(), PromiseX.delay(100).then(function(){
throw new Error('timeout');
})]).catch(function(reason) {
// reason == doAsync error or timeout
});
non-standard
is fulfilled only if all promises are fulfilled either resolved or rejected.
each promise's fulfillment state and value is provided in the propagated value array
as promise.value and promise.status
following promises can be cancelled if any promise returns a cancel promise
var requests = PromiseX.map(['c1.json', c2.json], getJSON); // could also be [getJSON('c1.json'), getJSON('c1.json')]
PromiseX.every(requests).then(function(resultArray) {
resultArray.forEach(function(result) {
if (result.status === PromiseX.RESOLVED) {
console.log(result.value);
} else {
console.error(result.value);
}
});
}).catch(function(reason) {
// reason could be any error during performing PromiseX.every, NOT a rejected request
});
non-standard
is fulfilled as soon as any promise is resolved or all promises are rejected
following promises can be cancelled if any promise returns a cancel promise
var requests = ['endpoint1', 'endpoint2'].map(function(endpoint) { // could also be [getJSON('endpoint1/a.json'), getJSON('endpoint2/a.json')];
return getJSON(endpoint + '/a.json');
});
PromiseX.any(requests).then(function(response) {
// response is from fastest endpoint, yeah :D
}).catch(function(reason) {
// none of endpoints resolved
// reason.message == 'No promises resolved successfully.'
});
non-standard
returns an array of PromiseX
created from each value by the map function executed with optional context
mapFunction is called as mapper(current, index, length, values)
heads-up: take care of errors; invalid input throws
var requestConfig = {
credentials: 'include'
};
var requests = PromiseX.map(['c1.json', c2.json], function(file) {
var request = new Request('endpoint/' + file, requestConfig);
return fetch(request).then(function(response) { // see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
if (response.ok === false) {
throw new TypeError('Invalid response');
}
return response.json();
});
});
// use of PromiseX promises
requests.forEach(function(promise) {
// promise is automatically casted to PromiseX within PromiseX.map
promise.nodeify(callback);
});
// usecase
PromiseX.every(requests).then(…).catch(…);
non-standard
reduces an array of promises or values to one promise chain
reduceFunction(previous, current, index, length) is called after previous promise is resolved
with the result of previous promise and current promise; first result is initialValue or undefined
used in used in many Promise libraries like BluebirdJS
or slightly different here promise-reduce
// simple usecase
PromiseX.reduce(['entries-10.txt', 'entries-20.txt'], function(result, file) {
return readEntriesAsync(file).then(function(entries) {
// entries of entries-10 is 10 and so on
return entries + result; // first result = 0 (initValue)
});
}, 0).then(function(total) {
// total == 0 + 10 + 20 = 30
});
// even simpler
PromiseX.reduce([1, 2, 3], function(total, current) {
return total + current;
}, 0).then(function(total) {
// total == 6
});
// example adapted from http://www.html5rocks.com/en/tutorials/es6/promises/#toc-creating-sequences
var urls = ['chapter-1.json', 'chapter-2.json', …];
var requests = PromiseX.map(urls, getJSON); // start loading each immediately
PromiseX.reduce(requests, function(_, chapterPromise) {
return chapterPromise.then(function(chapter) {
addHTMLToPage(chapter.htmlContent); // add in correct order as soon as loaded
});
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).finally(function() {
document.querySelector('.spinner').style.display = 'none';
});
non-standard
returning PromiseX.cancel(reason) will cancel the whole following promise chain
cancel can be caught on PromiseX#cancelled(reason) method within a chain
getJSON('/data').catch(function(error) {
return PromiseX.cancel({message: 'AJAX error', error: error});
}).then(function() {
// this function is never invoked on cancel
}).then(function() {
// this function is never invoked on cancel
}).catch(function() {
// this function is never invoked on cancel
}).cancelled(function(reason) {
console.warn('cancelled', reason.message, reason.error);
});
non-standard
influence behaviour of PromiseX plugin
'getPromise' and 'setPromise' G/Setter for the underlying promise - that way you don't need to redefine global promise
'createPromise' creates a new separate PromiseX instance with a given underlying promise
var Bluebird = Promise.noConflict(); // if Bluebird is included
PromiseX.config('setPromise', Bluebird);
var PromiseZ = PromiseX.config('createPromise', Zousan);
assert(PromiseZ.config('getPromise') instanceof Zousan);
static error on PromiseX
can be used to throw special error and check with 'instanceof'
provides message and even error like stack
getJSON('/data.json').then(function(data) {
if (data.length === 0) {
throw PromiseX.CancellationError('data.json is empty');
}
return doStuff(data);
}).then(…).catch(function(reason) {
if (reason instanceof PromiseX.CancellationError) {
console.error(reason);
} else {
// here for example getJSON errors are caught
}
});
static error on PromiseX
can be used to throw special error and check with 'instanceof'
provides message and even error like stack
function loadWithTimeout(url, delay) {
return new PromiseX(function(resolve, reject) {
setTimeout(function() {
throw new PromiseX.TimeoutError('loadWithTimeout from ' + url);
}, delay);
getJSON(url).then(resolve, reject);
});
}
loadWithTimeout('/data', 5000).catch(function(reason) {
if (reason instanceof PromiseX.TimeoutError) {
console.warn('timeout', reason.message);
}
});