From fa0007c9f2a5722c87935a0978752bb06744f4c8 Mon Sep 17 00:00:00 2001 From: Eli Thorkelson Date: Sat, 4 Jan 2014 11:41:25 -0600 Subject: [PATCH] Add allSettled to framework. Add tests --- lib/rsvp.js | 3 +- lib/rsvp/all_settled.js | 106 +++++++++++++++++++++++++++++++++++ test/tests/extension_test.js | 93 +++++++++++++++++++++++++++++- 3 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 lib/rsvp/all_settled.js diff --git a/lib/rsvp.js b/lib/rsvp.js index efe7fbdc..5f625858 100644 --- a/lib/rsvp.js +++ b/lib/rsvp.js @@ -2,6 +2,7 @@ import Promise from "./rsvp/promise"; import EventTarget from "./rsvp/events"; import denodeify from "./rsvp/node"; import all from "./rsvp/all"; +import allSettled from "./rsvp/all_settled"; import race from "./rsvp/race"; import hash from "./rsvp/hash"; import rethrow from "./rsvp/rethrow"; @@ -38,4 +39,4 @@ if (typeof window !== 'undefined' && typeof window.__PROMISE_INSTRUMENTATION__ = } } -export { Promise, EventTarget, all, race, hash, rethrow, defer, denodeify, configure, on, off, resolve, reject, async, map, filter }; +export { Promise, EventTarget, all, allSettled, race, hash, rethrow, defer, denodeify, configure, on, off, resolve, reject, async, map, filter }; diff --git a/lib/rsvp/all_settled.js b/lib/rsvp/all_settled.js new file mode 100644 index 00000000..42ca990b --- /dev/null +++ b/lib/rsvp/all_settled.js @@ -0,0 +1,106 @@ +import Promise from "./promise"; +import { isArray, isNonThenable } from "./utils"; + +/** + `RSVP.allSettled` is similar to `RSVP.all`, but instead of implementing + a fail-fast method, it waits until all the promises have returned and + shows you all the results. This is useful if you want to handle multiple + promises' failure states together as a set. + + Returns a promise that is fulfilled when all the given promises have been + settled. The return promise is fulfilled with an array of the states of + the promises passed into the `promises` array argument. + + Each state object will either indicate fulfillment or rejection, and + provide the corresponding value or reason. The states will take one of + the following formats: + + ```javascript + { state: "fulfilled", value: value } + or + { state: "rejected", reason: reason } + ``` + + Example: + + ```javascript + var promise1 = RSVP.resolve(1); + var promise2 = RSVP.reject(new Error("2")); + var promise3 = RSVP.reject(new Error("3")); + var promises = [ promise1, promise2, promise3 ]; + + RSVP.allSettled(promises).then(function(array){ + // array == [ + // { state: "fulfilled", value: 1 }, + // { state: "rejected", reason: Error }, + // { state: "rejected", reason: Error } + // ] + // Note that for the second item, reason.message will be "2", and for the + // third item, reason.message will be "3". + }, function(error) { + // Not run. (This block would only be called if allSettled had failed, + // for instance if passed an incorrect argument type.) + }); + ``` + + @method @allSettled + @for RSVP + @param {Array} promises; + @param {String} label - optional string that describes the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled with an array of the settled + states of the constituent promises. +*/ + +export default function allSettled(entries, label) { + return new Promise(function(resolve, reject) { + if (!isArray(entries)) { + throw new TypeError('You must pass an array to allSettled.'); + } + + var remaining = entries.length; + var results = new Array(remaining); + var entry; + + if (remaining === 0) { + resolve([]); + } + + function fulfilledState(value) { + return { state: "fulfilled", value: value }; + } + + function rejectedState(reason) { + return { state: "rejected", reason: reason }; + } + + function fulfilledResolver(index) { + return function(value) { + resolveAll(index, fulfilledState(value) ); + }; + } + + function rejectedResolver(index) { + return function(reason) { + resolveAll(index, rejectedState(reason) ); + }; + } + + function resolveAll(index, value) { + results[index] = value; + if (--remaining === 0) { + resolve(results); + } + } + + for (var index = 0; index < entries.length; index++) { + entry = entries[index]; + + if (isNonThenable(entry)) { + resolveAll(index, fulfilledState(entry) ); + } else { + Promise.cast(entry).then( fulfilledResolver(index), rejectedResolver(index) ); + } + } + }, label); +}; diff --git a/test/tests/extension_test.js b/test/tests/extension_test.js index c5b869f0..bfde8b09 100644 --- a/test/tests/extension_test.js +++ b/test/tests/extension_test.js @@ -730,6 +730,95 @@ describe("RSVP extensions", function() { }); } + describe("RSVP.allSettled", function() { + it('should exist', function() { + assert(RSVP.allSettled); + }); + + it('throws when not passed an array', function() { + var nothing = assertRejection(RSVP.allSettled()); + var string = assertRejection(RSVP.allSettled('')); + var object = assertRejection(RSVP.allSettled({})); + + RSVP.Promise.all([ + nothing, + string, + object + ]).then(function(){ done(); }); + }); + + specify('resolves an empty array passed to allSettled()', function(done) { + RSVP.allSettled([]).then(function(results) { + assert(results.length === 0); + done(); + }); + }); + + specify('works with promises, thenables, non-promises and rejected promises', + function(done) { + var promise = new RSVP.Promise(function(resolve) { resolve(1); }); + var syncThenable = { then: function (onFulfilled) { onFulfilled(2); } }; + var asyncThenable = { + then: function (onFulfilled) { + setTimeout(function() { onFulfilled(3); }, 0); + } + }; + var nonPromise = 4; + var rejectedPromise = new RSVP.Promise(function(resolve, reject) { + reject(new Error('WHOOPS')); + }); + + + var entries = new Array( + promise, syncThenable, asyncThenable, nonPromise, rejectedPromise + ); + + RSVP.allSettled(entries).then(function(results) { + assert(objectEquals(results[0], {state: "fulfilled", value: 1} )); + assert(objectEquals(results[1], {state: "fulfilled", value: 2} )); + assert(objectEquals(results[2], {state: "fulfilled", value: 3} )); + assert(objectEquals(results[3], {state: "fulfilled", value: 4} )); + assert(results[4].state, "rejected"); + assert(results[4].reason.message, "WHOOPS"); + done(); + }); + } + ); + + specify('fulfilled only after all of the other promises are fulfilled', function(done) { + var firstResolved, secondResolved, firstResolver, secondResolver; + + var first = new RSVP.Promise(function(resolve) { + firstResolver = resolve; + }); + first.then(function() { + firstResolved = true; + }); + + var second = new RSVP.Promise(function(resolve) { + secondResolver = resolve; + }); + second.then(function() { + secondResolved = true; + }); + + setTimeout(function() { + firstResolver(true); + }, 0); + + setTimeout(function() { + secondResolver(true); + }, 0); + + RSVP.allSettled([first, second]).then(function() { + assert(firstResolved); + assert(secondResolved); + done(); + }); + }); + + }); + describe("RSVP.reject", function(){ specify("it should exist", function(){ assert(RSVP.reject); @@ -1349,7 +1438,7 @@ describe("RSVP extensions", function() { return { key: matches[1], index: parseInt(matches[2], 10) - } + } } else { throw new Error('unknown guid:' + guid); } @@ -1501,7 +1590,7 @@ describe("RSVP extensions", function() { var values, originalAsync; beforeEach(function() { - originalAsync = RSVP.configure('async'); + originalAsync = RSVP.configure('async'); values = []; });