diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index dbe993d..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,12 +0,0 @@ -# These are supported funding model platforms - -github: [ForbesLindesay]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: npm/promise # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b0b5a36..0000000 --- a/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -components -build -node_modules -/lib -/domains -/setimmediate -coverage -package-lock.json \ No newline at end of file diff --git a/.npmignore b/.npmignore index ad5be4a..8e9e3ea 100644 --- a/.npmignore +++ b/.npmignore @@ -2,6 +2,6 @@ node_modules test .gitignore -.travis.yml +.github component.json coverage diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 84cd78b..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: node_js -sudo: false - -node_js: - - "8" - - "10" - - "12" - -after_success: - - npm run coverage - - npm i coveralls - - cat ./coverage/lcov.info | coveralls diff --git a/Readme.md b/Readme.md index aa0b5f5..5933338 100644 --- a/Readme.md +++ b/Readme.md @@ -7,19 +7,10 @@ **N.B.** This promise exposes internals via underscore (`_`) prefixed properties. If you use these, your code will break with each new release. -[![travis][travis-image]][travis-url] -[![dep][dep-image]][dep-url] -[![npm][npm-image]][npm-url] -[![downloads][downloads-image]][downloads-url] - -[travis-image]: https://img.shields.io/travis/then/promise.svg?style=flat -[travis-url]: https://travis-ci.org/then/promise -[dep-image]: https://img.shields.io/david/then/promise.svg?style=flat -[dep-url]: https://david-dm.org/then/promise -[npm-image]: https://img.shields.io/npm/v/promise.svg?style=flat -[npm-url]: https://npmjs.org/package/promise -[downloads-image]: https://img.shields.io/npm/dm/promise.svg?style=flat -[downloads-url]: https://npmjs.org/package/promise +[![Build Status](https://img.shields.io/github/workflow/status/then/promise/Publish%20Canary/master?style=for-the-badge)](https://github.com/then/promise/actions?query=workflow%3APublish%20Canary+branch%3Amaster) +[![Rolling Versions](https://img.shields.io/badge/Rolling%20Versions-Enabled-brightgreen?style=for-the-badge)](https://rollingversions.com/then/promise) +[![NPM version](https://img.shields.io/npm/v/promise?style=for-the-badge)](https://www.npmjs.com/package/promise) +[![Downloads](https://img.shields.io/npm/dm/promise.svg?style=for-the-badge)](https://www.npmjs.org/package/promise) ## Installation @@ -150,6 +141,19 @@ }) ``` +#### Promise.allSettled(array) + +Returns a promise that resolves after all of the given promises have either fulfilled or rejected, with an array of objects that each describes the outcome of each promise. + +```js +Promise.allSettled([Promise.resolve('a'), Promise.reject('error'), Promise.resolve('c')]) + .then(function (res) { + res[0] // { status: "fulfilled", value: 'a' } + res[1] // { status: "rejected", reason: 'error' } + res[2] // { status: "fulfilled", value: 'c' } + }) +``` + #### Promise.race(array) Returns a promise that resolves or rejects with the result of the first promise to resolve/reject, e.g. diff --git a/index.d.ts b/index.d.ts index e83d65f..f168a7b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -34,6 +34,18 @@ nodeify(callback: (err: Error, value: T) => void): void; } +interface PromiseFulfilledResult { + status: "fulfilled"; + value: T; +} + +interface PromiseRejectedResult { + status: "rejected"; + reason: any; +} + +type PromiseSettledResult = PromiseFulfilledResult | PromiseRejectedResult; + interface ThenPromiseConstructor { /** * A reference to the prototype. @@ -49,6 +61,86 @@ new (executor: (resolve: (value?: T | PromiseLike) => void, reject: (reason?: any) => void) => any): ThenPromise; /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike , T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike, T9 | PromiseLike, T10 | PromiseLike]): ThenPromise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike , T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike, T9 | PromiseLike]): ThenPromise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike , T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike, T8 | PromiseLike]): ThenPromise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike , T5 | PromiseLike, T6 | PromiseLike, T7 | PromiseLike]): ThenPromise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike , T5 | PromiseLike, T6 | PromiseLike]): ThenPromise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike , T5 | PromiseLike]): ThenPromise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike, T4 | PromiseLike ]): ThenPromise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike, T3 | PromiseLike]): ThenPromise<[PromiseSettledResult, PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: [T1 | PromiseLike, T2 | PromiseLike]): ThenPromise<[PromiseSettledResult, PromiseSettledResult]>; + + /** + * Creates a Promise that is resolved with an array of results when all + * of the provided Promises resolve or reject. + * @param values An array of Promises. + * @returns A new Promise. + */ + allSettled(values: (T | PromiseLike)[]): ThenPromise[]>; + + /** * Creates a ThenPromise that is resolved with an array of results when all of the provided Promises * resolve, or rejected when any ThenPromise is rejected. * @param values An array of Promises. @@ -243,4 +335,4 @@ declare var ThenPromise: ThenPromiseConstructor; -export = ThenPromise; \ No newline at end of file +export = ThenPromise; diff --git a/package.json b/package.json index 072dbe6..4895a1f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Bare bones Promises/A+ implementation", "main": "index.js", "scripts": { - "prepublish": "node build", + "build": "node build", "pretest": "node build", "pretest-resolve": "node build", "pretest-extensions": "node build", diff --git a/src/es6-extensions.js b/src/es6-extensions.js index 1161b57..3f48e4a 100644 --- a/src/es6-extensions.js +++ b/src/es6-extensions.js @@ -98,6 +98,29 @@ }); }; +function onSettledFulfill(value) { + return { status: 'fulfilled', value: value }; +} +function onSettledReject(reason) { + return { status: 'rejected', reason: reason }; +} +function mapAllSettled(item) { + if(item && (typeof item === 'object' || typeof item === 'function')){ + if(item instanceof Promise && item.then === Promise.prototype.then){ + return item.then(onSettledFulfill, onSettledReject); + } + var then = item.then; + if (typeof then === 'function') { + return new Promise(then.bind(item)).then(onSettledFulfill, onSettledReject) + } + } + + return onSettledFulfill(item); +} +Promise.allSettled = function (iterable) { + return Promise.all(iterableToArray(iterable).map(mapAllSettled)); +}; + Promise.reject = function (value) { return new Promise(function (resolve, reject) { reject(value); diff --git a/test/adapter-a.js b/test/adapter-a.js index ebda3ed..15c86d5 100644 --- a/test/adapter-a.js +++ b/test/adapter-a.js @@ -1,5 +1,4 @@ var Promise = require('../'); - exports.deferred = function () { var resolve, reject; diff --git a/test/extensions-tests.js b/test/extensions-tests.js index b3ff636..321a168 100644 --- a/test/extensions-tests.js +++ b/test/extensions-tests.js @@ -4,8 +4,8 @@ var promise = new Promise(function (resolve) { resolve(sentinel) }) -var thenable = {then: function (fullfilled, rejected) { fullfilled(sentinel) }} -var thenableRejected = {then: function (fullfilled, rejected) { rejected(sentinel) }} +var thenable = {then: function (fulfilled, rejected) { fulfilled(sentinel) }} +var thenableRejected = {then: function (fulfilled, rejected) { rejected(sentinel) }} var a = {} var b = {} @@ -128,6 +128,262 @@ assert(this === ctx) done() }) + }) + }) + describe('Promise.allSettled(...)', function () { + describe('an array', function () { + describe('that is empty', function () { + it('returns a promise for an empty array', function (done) { + var res = Promise.allSettled([]) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res.length === 0) + }) + .nodeify(done) + }) + }) + describe('of objects', function () { + it('returns a promise for the array', function (done) { + var res = Promise.allSettled([a, b, c]) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "fulfilled") + assert(res[1].value === b) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + }) + .nodeify(done) + }) + }) + describe('of promises', function () { + it('returns a promise for an array containing the fulfilled values', function (done) { + var d = {} + var resolveD + var res = Promise.allSettled([A, B, C, new Promise(function (resolve) { resolveD = resolve })]) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "fulfilled") + assert(res[1].value === b) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + assert(res[3].status === "fulfilled") + assert(res[3].value === d) + }) + .nodeify(done) + resolveD(d) + }) + }) + describe('of mixed values', function () { + it('returns a promise for an array containing the fulfilled values', function (done) { + var res = Promise.allSettled([A, b, C]) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "fulfilled") + assert(res[1].value === b) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + }) + .nodeify(done) + }) + }) + describe('containing at least one rejected promise', function () { + it('should not rejects the resulting promise', function (done) { + var res = Promise.allSettled([A, rejected, C]) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "rejected") + assert(res[1].reason === rejection) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + }) + .nodeify(done) + }) + }) + describe('containing at least one eventually rejected promise', function () { + it('rejects the resulting promise', function (done) { + var rejectB + var rejected = new Promise(function (resolve, reject) { rejectB = reject }) + var res = Promise.allSettled([A, rejected, C]) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "rejected") + assert(res[1].reason === rejection) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + }) + .nodeify(done) + rejectB(rejection) + }) + }) + describe('with a promise that resolves twice', function () { + it('still waits for all the other promises', function (done) { + var a = 1; + var fakePromise = {then: function (onFulfilled) { onFulfilled(a); onFulfilled(2) }} + var eventuallyRejected = {then: function (_, onRejected) { this.onRejected = onRejected }} + var res = Promise.allSettled([fakePromise, eventuallyRejected]) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "rejected") + assert(res[1].reason === rejection) + }) + .nodeify(done) + eventuallyRejected.onRejected(rejection); + }) + }) + describe('when given a foreign promise', function () { + it('should provide the correct value of `this`', function (done) { + var p = {then: function (onFulfilled) { onFulfilled({self: this}); }}; + Promise.allSettled([p]).then(function (results) { + assert(p === results[0].value.self); + }).nodeify(done); + }); + }); + }) + describe('a Set', function () { + describe('that is empty', function () { + it('returns a promise for an empty array', function (done) { + var res = Promise.allSettled(new Set([])) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res.length === 0) + }) + .nodeify(done) + }) + }) + describe('of objects', function () { + it('returns a promise for the array', function (done) { + var res = Promise.allSettled(new Set([a, b, c])) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "fulfilled") + assert(res[1].value === b) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + }) + .nodeify(done) + }) + }) + describe('of promises', function () { + it('returns a promise for an array containing the fulfilled values', function (done) { + var d = {} + var resolveD + var res = Promise.allSettled(new Set([A, B, C, new Promise(function (resolve) { resolveD = resolve })])) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "fulfilled") + assert(res[1].value === b) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + assert(res[3].status === "fulfilled") + assert(res[3].value === d) + }) + .nodeify(done) + resolveD(d) + }) + }) + describe('of mixed values', function () { + it('returns a promise for an array containing the fulfilled values', function (done) { + var res = Promise.allSettled(new Set([A, b, C])) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "fulfilled") + assert(res[1].value === b) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + }) + .nodeify(done) + }) + }) + describe('containing at least one rejected promise', function () { + it('rejects the resulting promise', function (done) { + var res = Promise.allSettled(new Set([A, rejected, C])) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "rejected") + assert(res[1].reason === rejection) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + }) + .nodeify(done) + }) + }) + describe('containing at least one eventually rejected promise', function () { + it('rejects the resulting promise', function (done) { + var rejectB + var rejected = new Promise(function (resolve, reject) { rejectB = reject }) + var res = Promise.allSettled(new Set([A, rejected, C])) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "rejected") + assert(res[1].reason === rejection) + assert(res[2].status === "fulfilled") + assert(res[2].value === c) + }) + .nodeify(done) + rejectB(rejection) + }) + }) + describe('with a promise that resolves twice', function () { + it('still waits for all the other promises', function (done) { + var a = 1 + var fakePromise = {then: function (onFulfilled) { onFulfilled(a); onFulfilled(2) }} + var eventuallyRejected = {then: function (_, onRejected) { this.onRejected = onRejected }} + var res = Promise.allSettled(new Set([fakePromise, eventuallyRejected])) + assert(res instanceof Promise) + res.then(function (res) { + assert(Array.isArray(res)) + assert(res[0].status === "fulfilled") + assert(res[0].value === a) + assert(res[1].status === "rejected") + assert(res[1].reason === rejection) + }) + .nodeify(done) + eventuallyRejected.onRejected(rejection); + }) + }) + describe('when given a foreign promise', function () { + it('should provide the correct value of `this`', function (done) { + var p = {then: function (onFulfilled) { onFulfilled({self: this}); }}; + Promise.allSettled(new Set([p])).then(function (results) { + assert(p === results[0].value.self); + }).nodeify(done); + }); + }); }) }) describe('Promise.all(...)', function () { diff --git a/test/nested-promises.js b/test/nested-promises.js index b8565d2..d5fc724 100644 --- a/test/nested-promises.js +++ b/test/nested-promises.js @@ -4,7 +4,7 @@ var Promise = require('../'); describe('nested promises', function () { - it('does not result in any wierd behaviour - 1', function (done) { + it('does not result in any weird behaviour - 1', function (done) { var resolveA, resolveB, resolveC; var A = new Promise(function (resolve, reject) { resolveA = resolve; @@ -23,7 +23,7 @@ done(); }); }); - it('does not result in any wierd behaviour - 2', function (done) { + it('does not result in any weird behaviour - 2', function (done) { var resolveA, resolveB, resolveC, resolveD; var A = new Promise(function (resolve, reject) { resolveA = resolve; @@ -47,7 +47,7 @@ done(); }); }); - it('does not result in any wierd behaviour - 2', function (done) { + it('does not result in any weird behaviour - 2', function (done) { var promises = []; var resolveFns = []; for (var i = 0; i < 100; i++) {