diff --git a/src/async/ExtensiblePromise.ts b/src/async/ExtensiblePromise.ts index b51253f9..60f6da78 100644 --- a/src/async/ExtensiblePromise.ts +++ b/src/async/ExtensiblePromise.ts @@ -1,4 +1,4 @@ -import { Iterable, forOf } from 'dojo-shim/iterator'; +import { Iterable, forOf, isIterable, isArrayLike } from 'dojo-shim/iterator'; import Promise, { Executor } from 'dojo-shim/Promise'; import { Thenable } from 'dojo-shim/interfaces'; @@ -17,6 +17,9 @@ function unwrapPromises(iterable: Iterable | any[]): any[] { return unwrapped; } +export type DictionaryOfPromises = { [_: string]: T | Promise | Thenable }; +export type ListOfPromises = Iterable<(T | Thenable)> | (T | Thenable); + /** * An extensible base to allow Promises to be extended in ES5. This class basically wraps a native Promise object, * giving an API like a native promise. @@ -46,14 +49,37 @@ export default class ExtensiblePromise { } /** - * Return a ExtensiblePromise that resolves when all of the passed in objects have resolved + * Return a ExtensiblePromise that resolves when all of the passed in objects have resolved. When used with a key/value + * pair, the returned promise's argument is a key/value pair of the original keys with their resolved values. * - * @param iterable An iterable of values to resolve. These can be Promises, ExtensiblePromises, or other objects + * @example + * ExtensiblePromise.all({ one: 1, two: 2 }).then(results => console.log(results)); + * // { one: 1, two: 2 } + * + * @param iterable An iterable of values to resolve, or a key/value pair of values to resolve. These can be Promises, ExtensiblePromises, or other objects * @returns {ExtensiblePromise} */ - static all, T>(iterable: Iterable<(T | Thenable)> | (T | Thenable)[]): F { + static all, T>(iterable: DictionaryOfPromises): F; + static all, T>(iterable: ListOfPromises): F; + static all, T>(iterable: DictionaryOfPromises | ListOfPromises): F { + if (!isArrayLike(iterable) && !isIterable(iterable)) { + const promiseKeys = Object.keys(iterable); + + return new this((resolve, reject) => { + Promise.all(promiseKeys.map(key => (> iterable)[ key ])).then((promiseResults: T[]) => { + const returnValue: {[_: string]: T} = {}; + + promiseResults.forEach((value: T, index: number) => { + returnValue[ promiseKeys[ index ] ] = value; + }); + + resolve(returnValue); + }, reject); + }); + } + return new this((resolve, reject) => { - Promise.all(unwrapPromises(iterable)).then(resolve, reject); + Promise.all(unwrapPromises(> iterable)).then(resolve, reject); }); } @@ -113,7 +139,7 @@ export default class ExtensiblePromise { */ then(onFulfilled?: ((value: T) => (U | Thenable | undefined)) | undefined, onRejected?: (reason: Error) => void): this; then(onFulfilled?: ((value: T) => (U | Thenable | undefined)) | undefined, onRejected?: (reason: Error) => (U | Thenable)): this { - let e: Executor = (resolve, reject) => { + const e: Executor = (resolve, reject) => { function handler(rejected: boolean, valueOrError: T | U | Error) { const callback: ((value: T | U | Error) => (U | Thenable | void)) | undefined = rejected ? onRejected : onFulfilled; diff --git a/tests/unit/async/ExtensiblePromise.ts b/tests/unit/async/ExtensiblePromise.ts index eefa4e51..77e574bf 100644 --- a/tests/unit/async/ExtensiblePromise.ts +++ b/tests/unit/async/ExtensiblePromise.ts @@ -47,5 +47,38 @@ registerSuite({ assert.isTrue(false, 'Should not have resolved'); }), undefined).catch(dfd.callback(() => { })); + }, + + '.all': { + 'with array'() { + return ExtensiblePromise.all([ 1, ExtensiblePromise.resolve(2), new ExtensiblePromise((resolve) => { + setTimeout(() => resolve(3), 100); + }) ]).then((results: any) => { + assert.deepEqual(results, [ 1, 2, 3 ]); + }); + }, + + 'with object'() { + return ExtensiblePromise.all({ + one: 1, two: ExtensiblePromise.resolve(2), three: new ExtensiblePromise((resolve) => { + setTimeout(() => resolve(3), 100); + }) + }).then((results: any) => { + assert.deepEqual(results, { one: 1, two: 2, three: 3 }); + }); + }, + + 'errors with object'(this: any) { + let dfd = this.async(); + + ExtensiblePromise.all({ + one: ExtensiblePromise.resolve(1), + two: ExtensiblePromise.reject(new Error('error message')) + }).then(dfd.rejectOnError(() => { + assert.fail('should have failed'); + }), dfd.callback((error: Error) => { + assert.equal(error.message, 'error message'); + })); + } } });