diff --git a/README.md b/README.md index 45b8006..50d7f22 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,14 @@ var setAsap = require('setasap'); Promise._setImmedateFn(setAsap); ``` +## Unhandled Rejections +promise-polyfill will warn you about possibly unhandled rejections. It will show a console warning if a Promise is rejected, but no `.catch` is used. You can turn off this behavior by setting `Promise._setUnhandledRejectionFn()`. +If you would like to disable unhandled rejections. Use a noop like below. +```js +Promise._setUnhandledRejectionFn(function(rejectError) {}); +``` + + ## Testing ``` npm install diff --git a/package.json b/package.json index df1eb7a..e94fe34 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "promise-polyfill", - "version": "4.0.0", + "version": "4.0.1", "description": "Lightweight promise polyfill. A+ compliant", "main": "Promise.js", "scripts": { "test": "eslint promise.js && mocha && karma start --single-run", - "build": "uglifyjs --compress --mangle -o Promise.min.js -- Promise.js " + "build": "uglifyjs --compress --mangle -o promise.min.js -- promise.js " }, "repository": { "type": "git", diff --git a/promise.js b/promise.js new file mode 100644 index 0000000..f54a1e1 --- /dev/null +++ b/promise.js @@ -0,0 +1,230 @@ +(function (root) { + + // Store setTimeout reference so promise-polyfill will be unaffected by + // other code modifying setTimeout (like sinon.useFakeTimers()) + var setTimeoutFunc = setTimeout; + + function noop() { + } + + // Use polyfill for setImmediate for performance gains + var asap = (typeof setImmediate === 'function' && setImmediate) || + function (fn) { + setTimeoutFunc(fn, 1); + }; + + var onUnhandledRejection = function onUnhandledRejection(err) { + console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console + }; + + // Polyfill for Function.prototype.bind + function bind(fn, thisArg) { + return function () { + fn.apply(thisArg, arguments); + }; + } + + var isArray = Array.isArray || function (value) { + return Object.prototype.toString.call(value) === '[object Array]'; + }; + + function Promise(fn) { + if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new'); + if (typeof fn !== 'function') throw new TypeError('not a function'); + this._state = 0; + this._handled = false; + this._value = undefined; + this._deferreds = []; + + doResolve(fn, this); + } + + function handle(self, deferred) { + while (self._state === 3) { + self = self._value; + } + if (self._state === 0) { + self._deferreds.push(deferred); + return; + } + self._handled = true; + asap(function () { + var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected; + if (cb === null) { + (self._state === 1 ? resolve : reject)(deferred.promise, self._value); + return; + } + var ret; + try { + ret = cb(self._value); + } catch (e) { + reject(deferred.promise, e); + return; + } + resolve(deferred.promise, ret); + }); + } + + function resolve(self, newValue) { + try { + // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure + if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.'); + if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) { + var then = newValue.then; + if (newValue instanceof Promise) { + self._state = 3; + self._value = newValue; + finale(self); + return; + } else if (typeof then === 'function') { + doResolve(bind(then, newValue), self); + return; + } + } + self._state = 1; + self._value = newValue; + finale(self); + } catch (e) { + reject(self, e); + } + } + + function reject(self, newValue) { + self._state = 2; + self._value = newValue; + finale(self); + } + + function finale(self) { + if (self._state === 2 && self._deferreds.length === 0) { + setTimeout(function() { + if (!self._handled) { + onUnhandledRejection(self._value); + } + }, 1); + } + + for (var i = 0, len = self._deferreds.length; i < len; i++) { + handle(self, self._deferreds[i]); + } + self._deferreds = null; + } + + function Handler(onFulfilled, onRejected, promise) { + this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null; + this.onRejected = typeof onRejected === 'function' ? onRejected : null; + this.promise = promise; + } + + /** + * Take a potentially misbehaving resolver function and make sure + * onFulfilled and onRejected are only called once. + * + * Makes no guarantees about asynchrony. + */ + function doResolve(fn, self) { + var done = false; + try { + fn(function (value) { + if (done) return; + done = true; + resolve(self, value); + }, function (reason) { + if (done) return; + done = true; + reject(self, reason); + }); + } catch (ex) { + if (done) return; + done = true; + reject(self, ex); + } + } + + Promise.prototype['catch'] = function (onRejected) { + return this.then(null, onRejected); + }; + + Promise.prototype.then = function (onFulfilled, onRejected) { + var prom = new Promise(noop); + handle(this, new Handler(onFulfilled, onRejected, prom)); + return prom; + }; + + Promise.all = function () { + var args = Array.prototype.slice.call(arguments.length === 1 && isArray(arguments[0]) ? arguments[0] : arguments); + + return new Promise(function (resolve, reject) { + if (args.length === 0) return resolve([]); + var remaining = args.length; + + function res(i, val) { + try { + if (val && (typeof val === 'object' || typeof val === 'function')) { + var then = val.then; + if (typeof then === 'function') { + then.call(val, function (val) { + res(i, val); + }, reject); + return; + } + } + args[i] = val; + if (--remaining === 0) { + resolve(args); + } + } catch (ex) { + reject(ex); + } + } + + for (var i = 0; i < args.length; i++) { + res(i, args[i]); + } + }); + }; + + Promise.resolve = function (value) { + if (value && typeof value === 'object' && value.constructor === Promise) { + return value; + } + + return new Promise(function (resolve) { + resolve(value); + }); + }; + + Promise.reject = function (value) { + return new Promise(function (resolve, reject) { + reject(value); + }); + }; + + Promise.race = function (values) { + return new Promise(function (resolve, reject) { + for (var i = 0, len = values.length; i < len; i++) { + values[i].then(resolve, reject); + } + }); + }; + + /** + * Set the immediate function to execute callbacks + * @param fn {function} Function to execute + * @private + */ + Promise._setImmediateFn = function _setImmediateFn(fn) { + asap = fn; + }; + + Promise._setUnhandledRejectionFn = function _setUnhandledRejectionFn(fn) { + onUnhandledRejection = fn; + }; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = Promise; + } else if (!root.Promise) { + root.Promise = Promise; + } + +})(this); diff --git a/promise.min.js b/promise.min.js new file mode 100644 index 0000000..b4be4c6 --- /dev/null +++ b/promise.min.js @@ -0,0 +1 @@ +!function(t){function e(){}function n(t,e){return function(){t.apply(e,arguments)}}function o(t){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof t)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],a(t,this)}function r(t,e){for(;3===t._state;)t=t._value;return 0===t._state?void t._deferreds.push(e):(t._handled=!0,void l(function(){var n=1===t._state?e.onFulfilled:e.onRejected;if(null===n)return void(1===t._state?i:u)(e.promise,t._value);var o;try{o=n(t._value)}catch(r){return void u(e.promise,r)}i(e.promise,o)}))}function i(t,e){try{if(e===t)throw new TypeError("A promise cannot be resolved with itself.");if(e&&("object"==typeof e||"function"==typeof e)){var r=e.then;if(e instanceof o)return t._state=3,t._value=e,void f(t);if("function"==typeof r)return void a(n(r,e),t)}t._state=1,t._value=e,f(t)}catch(i){u(t,i)}}function u(t,e){t._state=2,t._value=e,f(t)}function f(t){2===t._state&&0===t._deferreds.length&&setTimeout(function(){t._handled||o._onUnhandledRejection(t._value)},1);for(var e=0,n=t._deferreds.length;n>e;e++)r(t,t._deferreds[e]);t._deferreds=null}function c(t,e,n){this.onFulfilled="function"==typeof t?t:null,this.onRejected="function"==typeof e?e:null,this.promise=n}function a(t,e){var n=!1;try{t(function(t){n||(n=!0,i(e,t))},function(t){n||(n=!0,u(e,t))})}catch(o){if(n)return;n=!0,u(e,o)}}var s=setTimeout,l="function"==typeof setImmediate&&setImmediate||function(t){s(t,1)},d=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};o.prototype["catch"]=function(t){return this.then(null,t)},o.prototype.then=function(t,n){var i=new o(e);return r(this,new c(t,n,i)),i},o.all=function(){var t=Array.prototype.slice.call(1===arguments.length&&d(arguments[0])?arguments[0]:arguments);return new o(function(e,n){function o(i,u){try{if(u&&("object"==typeof u||"function"==typeof u)){var f=u.then;if("function"==typeof f)return void f.call(u,function(t){o(i,t)},n)}t[i]=u,0===--r&&e(t)}catch(c){n(c)}}if(0===t.length)return e([]);for(var r=t.length,i=0;io;o++)t[o].then(e,n)})},o._setImmediateFn=function(t){l=t},o._onUnhandledRejection=function(t){console.warn("Possible Unhandled Promise Rejection:",t)},"undefined"!=typeof module&&module.exports?module.exports=o:t.Promise||(t.Promise=o)}(this); \ No newline at end of file diff --git a/test/adapter.js b/test/adapter.js index bd4f722..f0c92a6 100644 --- a/test/adapter.js +++ b/test/adapter.js @@ -11,4 +11,4 @@ module.exports = { obj.promise = prom; return obj; } -} \ No newline at end of file +}; diff --git a/test/promise.js b/test/promise.js new file mode 100644 index 0000000..4cac27b --- /dev/null +++ b/test/promise.js @@ -0,0 +1,149 @@ +var Promise = require('../Promise'); +var sinon = require('sinon'); +var assert = require('assert'); +var adapter = require('./adapter'); +describe("Promises/A+ Tests", function () { + require("promises-aplus-tests").mocha(adapter); +}); +describe('Promise', function () { + describe('Promise._setImmediateFn', function () { + it('changes immediate fn', function () { + var spy = sinon.spy(); + + function immediateFn(fn) { + spy(); + fn(); + }; + Promise._setImmediateFn(immediateFn); + var done = false; + new Promise(function (resolve) { + resolve(); + }).then(function () { + done = true; + }); + assert(spy.calledOnce); + assert(done); + }); + it('changes immediate fn multiple', function () { + var spy1 = sinon.spy(); + + function immediateFn1(fn) { + spy1(); + fn(); + } + + var spy2 = sinon.spy(); + + function immediateFn2(fn) { + spy2(); + fn(); + } + + Promise._setImmediateFn(immediateFn1); + var done = false; + new Promise(function (resolve) { + resolve(); + }).then(function () { + }); + Promise._setImmediateFn(immediateFn2); + new Promise(function (resolve) { + resolve(); + }).then(function () { + done = true; + }); + assert(spy2.called); + assert(spy1.calledOnce); + assert(done); + }); + }); + describe('Promise._onUnhandledRejection', function () { + var stub, sandbox; + beforeEach(function() { + sandbox = sinon.sandbox.create(); + stub = sandbox.stub(console, 'warn'); + }); + afterEach(function() { + sandbox.restore(); + }); + it('no error on resolve', function (done) { + Promise.resolve(true).then(function(result) { + return result; + }).then(function(result) { + return result; + }); + + setTimeout(function() { + assert(!stub.called); + done(); + }, 200); + }); + it('error single Promise', function (done) { + new Promise(function(resolve, reject) { + abc.abc = 1; + }); + setTimeout(function() { + assert(stub.calledOnce); + done(); + }, 200); + }); + it('multi promise error', function (done) { + new Promise(function(resolve, reject) { + abc.abc = 1; + }).then(function(result) { + return result; + }); + setTimeout(function() { + assert(stub.calledOnce); + done(); + }, 200); + }); + it('promise catch no error', function (done) { + new Promise(function(resolve, reject) { + abc.abc = 1; + }).catch(function(result) { + return result; + }); + setTimeout(function() { + assert(!stub.called); + done(); + }, 200); + }); + it('promise catch no error', function (done) { + new Promise(function(resolve, reject) { + abc.abc = 1; + }).then(function(result) { + return result; + }).catch(function(result) { + return result; + }); + setTimeout(function() { + assert(!stub.called); + done(); + }, 200); + }); + it('promise reject error', function (done) { + Promise.reject('hello'); + setTimeout(function() { + assert(stub.calledOnce); + done(); + }, 200); + }); + it('promise reject error late', function (done) { + var prom = Promise.reject('hello'); + prom.catch(function() { + + }); + setTimeout(function() { + assert(!stub.called); + done(); + }, 200); + }); + it('promise reject error late', function (done) { + Promise.reject('hello'); + setTimeout(function() { + assert.equal(stub.args[0][1], 'hello'); + done(); + }, 200); + }); + }); +});