diff --git a/promise/promise.js b/promise/promise.js new file mode 100644 index 00000000..9a63c398 --- /dev/null +++ b/promise/promise.js @@ -0,0 +1,74 @@ +"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) { + 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) + } + else { + setTimeout(function() { + for (var i = 0; i < list.length; i++) list[i](value) + resolvers.length = 0, rejectors.length = 0 + }, 0) + } + } + } + + 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 resolveNext, rejectNext + handle(onFulfilled, this._instance.resolvers), handle(onRejection, this._instance.rejectors) + return new Promise(function(resolve, reject) {resolveNext = resolve, rejectNext = reject}) +} +Promise.prototype.catch = function(onRejection) { + return this.then(null, onRejection) +} +Promise.resolve = function(value) { + if (value instanceof Promise) return value + return new Promise(function(resolve, reject) {resolve(value)}) +} +Promise.reject = function(value) { + return new Promise(function(resolve, reject) {reject(value)}) +} +Promise.all = function(list) { + return new Promise(function(resolve, reject) { + var total = list.length, count = 0, values = [] + for (var i = 0; i < list.length; i++) { + new function(i) { + function consume(value) { + count++ + 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) + } + else consume(list[i]) + }(i) + } + }) +} +Promise.race = function(list) { + return new Promise(function(resolve, reject) { + for (var i = 0; i < list.length; i++) { + list[i].then(resolve, reject) + } + }) +} + +module.exports = Promise diff --git a/promise/tests/index.html b/promise/tests/index.html new file mode 100644 index 00000000..a1e96522 --- /dev/null +++ b/promise/tests/index.html @@ -0,0 +1,16 @@ + + +
+ + + + + + + + + + + + + \ No newline at end of file diff --git a/promise/tests/test-promise.js b/promise/tests/test-promise.js new file mode 100644 index 00000000..c1a2498a --- /dev/null +++ b/promise/tests/test-promise.js @@ -0,0 +1,499 @@ +"use strict" + +var o = require("../../ospec/ospec") +var callAsync = require("../../test-utils/callAsync") +var Promise = require("../../promise/promise") + +o.spec("promise", function() { + o.spec("constructor", function() { + o("constructor throws if called without new", function(done) { + try {Promise(function() {})} catch(e) {done()} + }) + o("constructor throws if called without executor", function(done) { + try {new Promise()} catch(e) {done()} + }) + o("constructor has correct methods", function() { + o(typeof Promise.prototype.then).equals("function") + o(typeof Promise.prototype.catch).equals("function") + o(typeof Promise.resolve).equals("function") + o(typeof Promise.reject).equals("function") + o(typeof Promise.race).equals("function") + o(typeof Promise.all).equals("function") + }) + }) + o.spec("return value", function() { + o("static resolve returns promise", function() { + var promise = Promise.resolve() + + o(promise instanceof Promise).equals(true) + }) + o("static resolve returns promise", function() { + var promise = Promise.reject() + promise.catch(function() {}) + + o(promise instanceof Promise).equals(true) + }) + o("static resolve with promise input returns same promise", function() { + var resolved = Promise.resolve(1) + var promise = Promise.resolve(resolved) + + o(promise).equals(resolved) + }) + o("then returns promise", function(done) { + var promise = Promise.resolve(1) + + promise.then(function(value) { + o(value).equals(1) + }).then(done) + }) + o("catch returns promise", function(done) { + var promise = Promise.reject(1) + + promise.catch(function(value) { + o(value).equals(1) + }).then(done) + }) + }) + o.spec("resolve", function() { + o("resolves once", function(done) { + var callCount = 0 + var promise = new Promise(function(resolve, reject) { + resolve(1) + resolve(2) + callAsync(function() {resolve(3)}) + }) + + promise.then(function(value) { + callCount++ + + o(value).equals(1) + o(callCount).equals(1) + done() + }) + }) + o("does not reject after resolve", function(done) { + var promise = new Promise(function(resolve, reject) { + resolve(1) + reject(2) + callAsync(function() {reject(3)}) + }) + + promise.then(function(value) { + o(value).equals(1) + done() + }) + }) + o("resolves asynchronously", function(done) { + var state = 0 + + var promise = Promise.resolve() + + state = 1 + promise.then(function(value) { + o(state).equals(2) + done() + }) + state = 2 + }) + o("resolves via static method", function(done) { + var promise = Promise.resolve(1) + + promise.then(function(value) { + o(value).equals(1) + done() + }) + }) + o("resolves asynchronously via executor", function(done) { + var promise = new Promise(function(resolve, reject) { + callAsync(function() {resolve(1)}) + }) + + promise.then(function(value) { + o(value).equals(1) + done() + }) + }) + o("downstreams correctly", function(done) { + var promise = Promise.resolve(1) + var a = promise.then(function(value) {return value + 1}) + var b = promise.then(function(value) {return value + 2}) + + a.then(function(aValue) { + b.then(function(bValue) { + o(aValue).equals(2) + o(bValue).equals(3) + done() + }) + }) + }) + o("cannot resolve to itself", function(done) { + var promise = new Promise(function(resolve) { + callAsync(function() {resolve(promise)}) + }) + + promise.then(null, done) + }) + }) + o.spec("reject", function() { + o("rejects once", function(done) { + var callCount = 0 + var promise = new Promise(function(resolve, reject) { + reject(1) + reject(2) + callAsync(function() {reject(3)}) + }) + + promise.then(null, function(value) { + callCount++ + + o(value).equals(1) + o(callCount).equals(1) + done() + }) + }) + o("does not resolve after reject", function(done) { + var promise = new Promise(function(resolve, reject) { + reject(1) + resolve(2) + callAsync(function() {resolve(3)}) + }) + + promise.then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("rejects asynchronously", function(done) { + var state = 0 + + var promise = Promise.reject() + + state = 1 + promise.then(null, function(value) { + o(state).equals(2) + done() + }) + state = 2 + }) + o("does not catch itself", function(done) { + var callCount = 0 + var promise = Promise.resolve().then(function() {throw 1}, function() {callCount++}) + + promise.then(null, function() { + o(callCount).equals(0) + done() + }) + }) + o("rejects via static method", function(done) { + var promise = Promise.reject(1) + + promise.then(null, function(value) { + o(value).equals(1) + return value + }).then(function(value) { + o(value).equals(1) + done() + }) + }) + o("rejects synchronously via executor", function(done) { + var promise = new Promise(function(resolve, reject) { + reject(1) + }) + + promise.then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("rejects asynchronously via executor", function(done) { + var promise = new Promise(function(resolve, reject) { + callAsync(function() {reject(1)}) + }) + + promise.then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("rejects via executor on error", function(done) { + var promise = new Promise(function(resolve, reject) { + throw 1 + }) + + promise.then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("rejects on fulfillment error", function(done) { + var promise = Promise.resolve() + + promise.then(function() { + throw 1 + }).then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("rejects on rejection error", function(done) { + var promise = Promise.resolve() + + promise.then(function() { + throw 1 + }).then(null, function() { + throw 2 + }).then(null, function(value) { + o(value).equals(2) + done() + }) + }) + }) + o.spec("promise absorption", function() { + o("absorbs resolved promise via static resolver", function(done) { + var promise = Promise.resolve(Promise.resolve(1)) + + promise.then(function(value) { + o(value).equals(1) + }).then(done) + }) + o("absorbs resolved promise in executor resolve", function(done) { + var promise = new Promise(function(resolve, reject) { + resolve(Promise.resolve(1)) + }) + + promise.then(function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs resolved promise on fulfillment", function(done) { + var promise = Promise.resolve() + + promise.then(function() { + return Promise.resolve(1) + }).then(function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs rejected promise via static resolver", function(done) { + var promise = Promise.resolve(Promise.reject(1)) + + promise.then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs rejected promise in executor resolve", function(done) { + var promise = new Promise(function(resolve, reject) { + resolve(Promise.reject(1)) + }) + + promise.then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs rejected promise on fulfillment", function(done) { + var promise = Promise.resolve() + + promise.then(function() { + return Promise.reject(1) + }).then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs pending promise that resolves via static resolver", function(done) { + var pending = new Promise(function(resolve, reject) { + setTimeout(function() {resolve(1)}, 10) + }) + var promise = Promise.resolve(pending) + + promise.then(function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs pending promise that resolves in executor resolve", function(done) { + var pending = new Promise(function(resolve, reject) { + setTimeout(function() {resolve(1)}, 10) + }) + var promise = new Promise(function(resolve, reject) { + resolve(pending) + }) + + promise.then(function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs pending promise that resolves on fulfillment", function(done) { + var pending = new Promise(function(resolve, reject) { + setTimeout(function() {resolve(1)}, 10) + }) + var promise = Promise.resolve() + + promise.then(function() { + return pending + }).then(function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs pending promise that rejects via static resolver", function(done) { + var pending = new Promise(function(resolve, reject) { + setTimeout(function() {reject(1)}, 10) + }) + var promise = Promise.resolve(pending) + + promise.then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs pending promise that rejects in executor resolve", function(done) { + var pending = new Promise(function(resolve, reject) { + setTimeout(function() {reject(1)}, 10) + }) + var promise = new Promise(function(resolve, reject) { + resolve(pending) + }) + + promise.then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("absorbs pending promise that rejects on fulfillment", function(done) { + var pending = new Promise(function(resolve, reject) { + setTimeout(function() {reject(1)}, 10) + }) + var promise = Promise.resolve() + + promise.then(function() { + return pending + }).then(null, function(value) { + o(value).equals(1) + done() + }) + }) + o("does not absorb resolved promise via static rejector", function(done) { + var promise = Promise.reject(Promise.resolve(1)) + + promise.then(null, function(value) { + o(value instanceof Promise).equals(true) + done() + }) + }) + o("does not absorb rejected promise via static rejector", function(done) { + var rejected = Promise.reject(1) + rejected.catch(function() {}) + var promise = Promise.reject(rejected) + + promise.then(null, function(value) { + o(value instanceof Promise).equals(true) + done() + }) + }) + o("does not absorb resolved promise in executor reject", function(done) { + var promise = new Promise(function(resolve, reject) { + reject(Promise.resolve(1)) + }) + + promise.then(null, function(value) { + o(value instanceof Promise).equals(true) + done() + }) + }) + o("does not absorb rejected promise in executor reject", function(done) { + var promise = new Promise(function(resolve, reject) { + var rejected = Promise.reject(1) + rejected.catch(function() {}) + reject(rejected) + }) + + promise.then(null, function(value) { + o(value instanceof Promise).equals(true) + done() + }) + }) + o("does not absorb resolved promise on fulfillment error", function(done) { + var promise = Promise.resolve() + + promise.then(function() { + throw Promise.resolve(1) + }).then(null, function(value) { + o(value instanceof Promise).equals(true) + done() + }) + }) + o("does not absorb rejected promise on fulfillment error", function(done) { + var promise = Promise.resolve() + + promise.then(function() { + var rejected = Promise.reject(1) + rejected.catch(function() {}) + throw rejected + }).then(null, function(value) { + o(value instanceof Promise).equals(true) + done() + }) + }) + }) + o.spec("race", function() { + o("resolves to first resolved", function(done) { + var a = Promise.resolve(1) + var b = new Promise(function(resolve, reject) { + callAsync(function() {resolve(2)}) + }) + Promise.race([a, b]).then(function(value) { + o(value).equals(1) + done() + }) + }) + o("rejects to first rejected", function(done) { + var a = Promise.reject(1) + var b = new Promise(function(resolve, reject) { + callAsync(function() {reject(2)}) + }) + Promise.race([a, b]).then(null, function(value) { + o(value).equals(1) + done() + }) + }) + }) + o.spec("all", function() { + o("resolves to array", function(done) { + var a = new Promise(function(resolve, reject) { + callAsync(function() {resolve(1)}) + }) + var b = Promise.resolve(2) + Promise.all([a, b]).then(function(value) { + o(value).deepEquals([1, 2]) + done() + }) + }) + o("resolves non-promise to itself", function(done) { + var a = new Promise(function(resolve, reject) { + callAsync(function() {resolve(1)}) + }) + var b = Promise.resolve(2) + var c = 3 + Promise.all([a, b, c]).then(function(value) { + o(value).deepEquals([1, 2, 3]) + done() + }) + }) + o("rejects to first rejected", function(done) { + var a = Promise.reject(1) + var b = new Promise(function(resolve, reject) { + callAsync(function() {reject(2)}) + }) + Promise.all([a, b]).then(null, function(value) { + o(value).equals(1) + done() + }) + }) + }) +}) \ No newline at end of file