From 3bb0a6287f907440298d07187143e027c2d8ae78 Mon Sep 17 00:00:00 2001 From: Leo Horie Date: Sat, 28 May 2016 01:12:20 -0400 Subject: [PATCH] promise bug fixes (work in progress) --- promise/promise.js | 58 +++++++++++++-------- promise/tests/test-promise.js | 94 +++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 20 deletions(-) diff --git a/promise/promise.js b/promise/promise.js index 9a63c398..d859bd62 100644 --- a/promise/promise.js +++ b/promise/promise.js @@ -1,39 +1,55 @@ "use strict" - +{ function Promise(executor) { if (!(this instanceof Promise)) throw new Error("Promise must be called with `new`") if (typeof executor !== "function") throw new Error("executor must be a function") var self = this, resolvers = [], rejectors = [], resolveCurrent = handler(resolvers, true), rejectCurrent = handler(rejectors, false) function handler(list, shouldAbsorb) { + var done = false return function execute(value) { - if (shouldAbsorb && (typeof value === "object" || typeof value === "function") && typeof value.then === "function") { - if (value === self) rejectCurrent(new Error("Promise cannot be resolved with itself")) - value.then(execute, rejectCurrent) + if (done) return + done = true + + var then + try { + if (shouldAbsorb && value != null && (typeof value === "object" || typeof value === "function") && typeof (then = value.then) === "function") { + if (value === self) rejectCurrent(new TypeError("Promise can't be resolved w/ itself")) + then.call(value, handler(list, shouldAbsorb), rejectCurrent) + } + else { + setTimeout(function() { + for (var i = 0; i < list.length; i++) list[i](value) + instance.retry = function() { + done = false + execute(value) + } + }, 0) + } } - else { - setTimeout(function() { - for (var i = 0; i < list.length; i++) list[i](value) - resolvers.length = 0, rejectors.length = 0 - }, 0) + catch (e) { + rejectCurrent(e) } } } + var instance = this._instance = {resolvers: resolvers, rejectors: rejectors} - this._instance = {resolvers: resolvers, rejectors: rejectors} try {executor(resolveCurrent, rejectCurrent)} catch (e) {rejectCurrent(e)} } Promise.prototype.then = function(onFulfilled, onRejection) { - function handle(callback, list) { - if (typeof callback === "function") { - list.push(function(value) { - try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)} - }) - } + var self = this + function handle(callback, list, next, state) { + list.push(function(value) { + if (typeof callback !== "function") next(value) + try {resolveNext(callback(value))} catch (e) {if (rejectNext) rejectNext(e)} + }) + var retry = self._instance.retry + if (retry) retry() } var resolveNext, rejectNext - handle(onFulfilled, this._instance.resolvers), handle(onRejection, this._instance.rejectors) - return new Promise(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) + var promise = new Promise(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) + handle(onFulfilled, this._instance.resolvers, resolveNext, true), handle(onRejection, this._instance.rejectors, rejectNext, false) + return promise } Promise.prototype.catch = function(onRejection) { return this.then(null, onRejection) @@ -55,8 +71,9 @@ Promise.all = function(list) { values[i] = value if (count === total) resolve(values) } - if ((typeof list[i] === "object" || typeof list[i] === "function") && typeof list[i].then === "function") { - list[i].then(consume, reject) + var then + if (list[i] != null && (typeof list[i] === "object" || typeof list[i] === "function") && typeof (then = list[i].then) === "function") { + then.call(list[i], consume, reject) } else consume(list[i]) }(i) @@ -72,3 +89,4 @@ Promise.race = function(list) { } module.exports = Promise +} \ No newline at end of file diff --git a/promise/tests/test-promise.js b/promise/tests/test-promise.js index c1a2498a..87182425 100644 --- a/promise/tests/test-promise.js +++ b/promise/tests/test-promise.js @@ -133,6 +133,22 @@ o.spec("promise", function() { promise.then(null, done) }) + o("non-function onFulfilled is ignored", function(done) { + var promise = Promise.resolve(1) + + promise.then(null, null).then(function(value) { + o(value).equals(1) + done() + }) + }) + o("non-function onFulfilled is ignored", function(done) { + var promise = Promise.resolve(1) + + promise.then(null).then(function(value) { + o(value).equals(1) + done() + }) + }) }) o.spec("reject", function() { o("rejects once", function(done) { @@ -247,6 +263,14 @@ o.spec("promise", function() { done() }) }) + o("non-function onRejected is ignored", function(done) { + var promise = Promise.reject(1) + + promise.then(function() {}, null).then(null, function(value) { + o(value).equals(1) + done() + }) + }) }) o.spec("promise absorption", function() { o("absorbs resolved promise via static resolver", function(done) { @@ -440,6 +464,37 @@ o.spec("promise", function() { done() }) }) + o("promise stays pending if absorbed promise is pending", function(done) { + var promise = new Promise(function(resolve) {resolve()}) + var fulfilled = false, rejected = false + + promise.then(function() { + return new Promise(function() {}) + }).then(function() { + fulfilled = true + }, function() { + rejected = false + }) + + setTimeout(function() { + o(fulfilled).equals(false) + o(rejected).equals(false) + done() + }, 10) + }) + o("absorbs early resolved promise", function(done, t) { + var resolved = Promise.resolve(1) + var promise = new Promise(function(resolve) { + setTimeout(function() { + resolve(resolved) + }, 10) + }) + + promise.then(function(value) { + o(value).equals(1) + done() + }) + }) }) o.spec("race", function() { o("resolves to first resolved", function(done) { @@ -496,4 +551,43 @@ o.spec("promise", function() { }) }) }) + o.spec("A+ compliance", function() { + o("accesses then only once", function(done) { + var readCount = 0 + var promise = Promise.resolve(1).then(function() { + return Object.create(null, { + then: { + get: function () { + ++readCount + return function(onFulfilled) { + onFulfilled() + } + } + } + }) + }) + + promise.then(function(value) { + o(readCount).equals(1) + done() + }) + }) + o("works if thennable resolves twice", function(done) { + var promise = Promise.resolve({ + then: function(res) { + res({ + then: function(resolve) { + setTimeout(function() {resolve(2)}) + } + }) + res(1) + } + }) + + promise.then(function(value) { + o(value).equals(2) + done() + }) + }) + }) }) \ No newline at end of file