diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e88a2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/tmp \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..3a517ea --- /dev/null +++ b/.npmignore @@ -0,0 +1,3 @@ +/node_modules/ +/tmp +/test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..066a06b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.0 (june 19, 2014) + +* Init release diff --git a/README.md b/README.md new file mode 100644 index 0000000..3c2d191 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# ES6 Promise polyfill + +This is a polyfill of [ES6 Promise](https://github.com/domenic/promises-unwrapping). The implementation based on [Jake Archibald implementation](https://github.com/jakearchibald/es6-promise) a subset of [rsvp.js](https://github.com/tildeio/rsvp.js). If you're wanting extra features and more debugging options, check out the [full library](https://github.com/tildeio/rsvp.js). + +For API details and how to use promises, see the JavaScript Promises HTML5Rocks article. + +## Notes + +The main target: implementation should be conformance with browser's implementations and to be minimal as possible in size. So it's strictly polyfill of ES6 Promise specification and nothing more. + +It passes both [Promises/A+ test suite](https://github.com/promises-aplus/promises-tests) and [rsvp.js test suite](https://github.com/jakearchibald/es6-promise/tree/master/test). And as small as 2,6KB min (or 1KB min+gzip). + +The polyfill uses `setImmediate` if available, or fallback to use `setTimeout`. Use [setImmediate polyfill](https://github.com/YuzuJS/setImmediate) by @YuzuJS to rich better performance. + +## How to use + +### Browser + +To install: + +```sh +bower install es6-promise-polyfill +``` + +To use: + +```htmpl + + +``` + +### Node.js + +To install: + +```sh +npm install es6-promise-polyfill +``` + +To use: + +```js +var Promise = require('es6-promise-polyfill').Promise; +var promise = new Promise(...); +``` + +## Usage in IE<9 + +`catch` is a reserved word in IE<9, meaning `promise.catch(func)` throws a syntax error. To work around this, use a string to access the property: + +```js +promise['catch'](function(err) { + // ... +}); +``` + +Or use `.then` instead: + +```js +promise.then(undefined, function(err) { + // ... +}); +``` diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..8b28ad4 --- /dev/null +++ b/bower.json @@ -0,0 +1,11 @@ +{ + "name": "es6-promise-polyfill", + "version": "1.0.0", + "main": "promise.js", + "ignore": [ + ".*", + "**/.*", + "node_modules", + "test" + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..314af33 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "es6-promise-polyfill", + "namespace": "Promise", + "version": "1.0.0", + "author": "Roman Dvornov ", + "description": "A polyfill for ES6 Promise", + "main": "promise.js", + "directories": { + "lib": "lib" + }, + "devDependencies": { + }, + "scripts": { + }, + "repository": { + "type": "git", + "url": "git://github.com/lahmatiy/es6-promise-polyfill.git" + }, + "bugs": { + "url": "https://github.com/lahmatiy/es6-promise-polyfill/issues" + }, + "keywords": [ + "promises", + "futures", + "events" + ] +} diff --git a/promise.js b/promise.js new file mode 100644 index 0000000..c3f0295 --- /dev/null +++ b/promise.js @@ -0,0 +1,326 @@ +(function(global){ + +// +// Check for native Promise and it has correct interface +// + +var NativePromise = global['Promise']; +var nativePromiseSupported = + NativePromise && + // Some of these methods are missing from + // Firefox/Chrome experimental implementations + 'resolve' in NativePromise && + 'reject' in NativePromise && + 'all' in NativePromise && + 'race' in NativePromise && + // Older version of the spec had a resolver object + // as the arg rather than a function + (function(){ + var resolve; + new NativePromise(function(r){ resolve = r; }); + return typeof resolve === 'function'; + })(); + + +// +// export if necessary +// + +if (typeof exports !== 'undefined' && exports) +{ + // node.js + exports.Promise = Promise || NativePromise; +} +else +{ + // in browser add to global + if (!nativePromiseSupported) + global['Promise'] = Promise; +} + + +// +// Polyfill +// + +var PENDING = 'pending'; +var SEALED = 'sealed'; +var FULFILLED = 'fulfilled'; +var REJECTED = 'rejected'; +var NOOP = function(){}; + +// async calls +var asyncSetTimer = typeof setImmediate !== 'undefined' ? setImmediate : setTimeout; +var asyncQueue = []; +var asyncTimer; + +function asyncFlush(){ + // run promise callbacks + for (var i = 0; i < asyncQueue.length; i++) + asyncQueue[i][0](asyncQueue[i][1]); + + // reset async asyncQueue + asyncQueue = []; + asyncTimer = false; +} + +function asyncCall(callback, arg){ + asyncQueue.push([callback, arg]); + + if (!asyncTimer) + { + asyncTimer = true; + asyncSetTimer(asyncFlush, 0); + } +} + + +function invokeResolver(resolver, promise) { + function resolvePromise(value) { + resolve(promise, value); + } + + function rejectPromise(reason) { + reject(promise, reason); + } + + try { + resolver(resolvePromise, rejectPromise); + } catch(e) { + rejectPromise(e); + } +} + +function invokeCallback(subscriber){ + var owner = subscriber.owner; + var settled = owner.state_; + var value = owner.data_; + var callback = subscriber[settled]; + var promise = subscriber.then; + + if (typeof callback === 'function') + { + settled = FULFILLED; + try { + value = callback(value); + } catch(e) { + reject(promise, e); + } + } + + if (!handleThenable(promise, value)) + { + if (settled === FULFILLED) + resolve(promise, value); + + if (settled === REJECTED) + reject(promise, value); + } +} + +function handleThenable(promise, value) { + var resolved; + + try { + if (promise === value) + throw new TypeError('A promises callback cannot return that same promise.'); + + if (value && (typeof value === 'function' || typeof value === 'object')) + { + var then = value.then; // then should be retrived only once + + if (typeof then === 'function') + { + then.call(value, function(val){ + if (!resolved) + { + resolved = true; + + if (value !== val) + resolve(promise, val); + else + fulfill(promise, val); + } + }, function(reason){ + if (!resolved) + { + resolved = true; + + reject(promise, reason); + } + }); + + return true; + } + } + } catch (e) { + if (!resolved) + reject(promise, e); + + return true; + } + + return false; +} + +function resolve(promise, value){ + if (promise === value || !handleThenable(promise, value)) + fulfill(promise, value); +} + +function fulfill(promise, value){ + if (promise.state_ === PENDING) + { + promise.state_ = SEALED; + promise.data_ = value; + + asyncCall(publishFulfillment, promise); + } +} + +function reject(promise, reason){ + if (promise.state_ === PENDING) + { + promise.state_ = SEALED; + promise.data_ = reason; + + asyncCall(publishRejection, promise); + } +} + +function publish(promise) { + promise.then_ = promise.then_.forEach(invokeCallback); +} + +function publishFulfillment(promise){ + promise.state_ = FULFILLED; + publish(promise); +} + +function publishRejection(promise){ + promise.state_ = REJECTED; + publish(promise); +} + +/** +* @class +*/ +function Promise(resolver){ + if (typeof resolver !== 'function') + throw new TypeError('Promise constructor takes a function argument'); + + if (this instanceof Promise === false) + throw new TypeError('Failed to construct \'Promise\': Please use the \'new\' operator, this object constructor cannot be called as a function.'); + + this.then_ = []; + + invokeResolver(resolver, this); +} + +Promise.prototype = { + constructor: Promise, + + state_: PENDING, + then_: null, + data_: undefined, + + then: function(onFulfillment, onRejection){ + var subscriber = { + owner: this, + then: new this.constructor(NOOP), + fulfilled: onFulfillment, + rejected: onRejection + }; + + if (this.state_ === FULFILLED || this.state_ === REJECTED) + { + // already resolved, call callback async + asyncCall(invokeCallback, subscriber); + } + else + { + // subscribe + this.then_.push(subscriber); + } + + return subscriber.then; + }, + + 'catch': function(onRejection) { + return this.then(null, onRejection); + } +}; + +Promise.all = function(promises){ + var Class = this; + + if (!Array.isArray(promises)) + throw new TypeError('You must pass an array to Promise.all().'); + + return new Class(function(resolve, reject){ + var results = []; + var remaining = 0; + + function resolver(index){ + remaining++; + return function(value){ + results[index] = value; + if (!--remaining) + resolve(results); + }; + } + + for (var i = 0, promise; i < promises.length; i++) + { + promise = promises[i]; + + if (promise && typeof promise.then === 'function') + promise.then(resolver(i), reject); + else + results[i] = promise; + } + + if (!remaining) + resolve(results); + }); +}; + +Promise.race = function(promises){ + var Class = this; + + if (!Array.isArray(promises)) + throw new TypeError('You must pass an array to Promise.race().'); + + return new Class(function(resolve, reject) { + for (var i = 0, promise; i < promises.length; i++) + { + promise = promises[i]; + + if (promise && typeof promise.then === 'function') + promise.then(resolve, reject); + else + resolve(promise); + } + }); +}; + +Promise.resolve = function(value){ + var Class = this; + + if (value && typeof value === 'object' && value.constructor === Class) + return value; + + return new Class(function(resolve){ + resolve(value); + }); +}; + +Promise.reject = function(reason){ + var Class = this; + + return new Class(function(resolve, reject){ + reject(reason); + }); +}; + +})(new Function('return this')()); diff --git a/promise.min.js b/promise.min.js new file mode 100644 index 0000000..ee30fcc --- /dev/null +++ b/promise.min.js @@ -0,0 +1,5 @@ +(function(t){function x(){for(var a=0;a