diff --git a/src/pledge.js b/src/pledge.js index 3f7efa5..2d594c6 100644 --- a/src/pledge.js +++ b/src/pledge.js @@ -9,7 +9,7 @@ import { PledgeSymbol } from "./pledge-symbol.js"; import { PledgeReactionJob, hostEnqueuePledgeJob } from "./pledge-jobs.js"; -import { isObject, isCallable, isConstructor } from "./utilities.js"; +import { isObject, isCallable, isConstructor, PledgeAggregateError } from "./utilities.js"; import { isPledge, createResolvingFunctions, @@ -86,6 +86,22 @@ export class Pledge { return this; } + static any(iterable) { + + const C = this; + const pledgeCapability = new PledgeCapability(C); + + try { + const pledgeResolve = getPledgeResolve(C); + const iteratorRecord = iterable[Symbol.iterator](); + const result = performPledgeAny(iteratorRecord, C, pledgeCapability, pledgeResolve); + return result; + } catch (error) { + pledgeCapability.reject(error); + return error; + } + } + static race(iterable) { const C = this; @@ -267,6 +283,85 @@ function getPledgeResolve(pledgeConstructor) { return pledgeResolve; } +//----------------------------------------------------------------------------- +// 26.6.4.3.1 PerformPromiseAny ( iteratorRecord, constructor, +// resultCapability, promiseResolve ) +//----------------------------------------------------------------------------- + +function performPledgeAny(iteratorRecord, constructor, resultCapability, pledgeResolve) { + + assertIsConstructor(constructor); + assertIsCallable(pledgeResolve); + + const errors = []; + const remainingElementsCount = { value: 1 }; + let index = 0; + + for (const nextValue of iteratorRecord) { + + errors.push(undefined); + + const nextPledge = pledgeResolve(constructor, nextValue); + const rejectElement = createPledgeAnyRejectElement(index, errors, resultCapability, remainingElementsCount); + + remainingElementsCount.value = remainingElementsCount.value + 1; + nextPledge.then(resultCapability.resolve, rejectElement); + index = index + 1; + } + + remainingElementsCount.value = remainingElementsCount.value - 1; + if (remainingElementsCount.value === 0) { + const error = new PledgeAggregateError(); + Object.defineProperty(error, "errors", { + configurable: true, + enumerable: false, + writable: true, + value: errors + }); + + resultCapability.reject(error); + } + + return resultCapability.pledge; +} + +//----------------------------------------------------------------------------- +// 26.6.4.3.2 Promise.any Reject Element Functions +//----------------------------------------------------------------------------- + +// Note: this function doesn't exist in the spec, I've added it for clarity + +function createPledgeAnyRejectElement(index, errors, pledgeCapability, remainingElementsCount) { + + const alreadyCalled = { value: false }; + + return x => { + + if (alreadyCalled.value) { + return; + } + + alreadyCalled.value = true; + + errors[index] = x; + remainingElementsCount.value = remainingElementsCount.value - 1; + + if (remainingElementsCount.value === 0) { + const error = new PledgeAggregateError(); + Object.defineProperty(error, "errors", { + configurable: true, + enumerable: false, + writable: true, + value: errors + }); + + return pledgeCapability.reject(error); + + } + + }; +} + //----------------------------------------------------------------------------- // 26.6.4.5.1 PerformPromiseRace ( iteratorRecord, constructor, // resultCapability, promiseResolve ) diff --git a/src/utilities.js b/src/utilities.js index a473553..e474fe6 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -59,3 +59,36 @@ export function isCallable(argument) { export function isConstructor(argument) { return typeof argument === "function" && typeof argument.prototype !== "undefined"; } + + +//----------------------------------------------------------------------------- +// 19.5.7 AggregateError Objects +//----------------------------------------------------------------------------- + +export function PledgeAggregateError(errors=[], message) { + + const O = new.target === undefined ? new PledgeAggregateError() : this; + + if (typeof message !== "undefined") { + const msg = String(message); + + Object.defineProperty(O, "message", { + value: msg, + writable: true, + enumerable: false, + configurable: true + }); + } + + // errors can be an iterable + const errorsList = [...errors]; + + Object.defineProperty(O, "errors", { + configurable: true, + enumerable: false, + writable: true, + value: errorsList + }); + + return O; +} diff --git a/tests/pledge.test.js b/tests/pledge.test.js index b943f2f..eef8849 100644 --- a/tests/pledge.test.js +++ b/tests/pledge.test.js @@ -446,6 +446,75 @@ describe("Pledge", () => { }); }); + describe("Pledge.any()", () => { + + it("should throw an error when `this` is not a constructor", () => { + expect(() => { + Pledge.any.call({}, []); + }).to.throw(/constructor/); + }); + + it("should return the first value that was resolved", done => { + + const pledge = Pledge.any([ + Pledge.resolve(42), + Pledge.resolve(43), + Pledge.resolve(44) + ]); + + pledge.then(value => { + expect(value).to.equal(42); + done(); + }); + + }); + + it("should return the second pledge resolution when it is resolved first", done => { + + const pledge = Pledge.any([ + Pledge.reject(42), + Pledge.resolve(43), + Pledge.resolve(44) + ]); + + pledge.then(value => { + expect(value).to.equal(43); + done(); + }); + + }); + + it("should return an aggregate error when all pledges are rejected", done => { + + const pledge = Pledge.any([ + Pledge.reject(42), + Pledge.reject(43), + Pledge.reject(44) + ]); + + pledge.catch(reason => { + expect(reason.errors).to.deep.equal([42, 43, 44]); + done(); + }); + + }); + + it("should return the third pledge value when it is resolved first", done => { + + const pledge = Pledge.any([ + delayResolvePledge(42, 500), + Pledge.reject(43), + Pledge.resolve(44) + ]); + + pledge.then(value => { + expect(value).to.equal(44); + done(); + }); + + }); + }); +