From 6acdfe52c4313eebfec4dcefad6dcc995b2a11f6 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Wed, 4 Nov 2020 18:09:03 -0800 Subject: [PATCH] Chore: Refactor Pledge.any()/race() to better match spec --- src/pledge.js | 106 ++++++++++++++++++++++++++++---------- src/utilities.js | 109 ++++++++++++++++++++++++++++++++++++++- tests/pledge.test.js | 118 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 306 insertions(+), 27 deletions(-) diff --git a/src/pledge.js b/src/pledge.js index 2d594c6..b1a88eb 100644 --- a/src/pledge.js +++ b/src/pledge.js @@ -9,7 +9,15 @@ import { PledgeSymbol } from "./pledge-symbol.js"; import { PledgeReactionJob, hostEnqueuePledgeJob } from "./pledge-jobs.js"; -import { isObject, isCallable, isConstructor, PledgeAggregateError } from "./utilities.js"; +import { + isObject, + isCallable, + isConstructor, + PledgeAggregateError, + iteratorStep, + iteratorValue, + getIterator +} from "./utilities.js"; import { isPledge, createResolvingFunctions, @@ -93,12 +101,12 @@ export class Pledge { try { const pledgeResolve = getPledgeResolve(C); - const iteratorRecord = iterable[Symbol.iterator](); + const iteratorRecord = getIterator(iterable); const result = performPledgeAny(iteratorRecord, C, pledgeCapability, pledgeResolve); return result; } catch (error) { pledgeCapability.reject(error); - return error; + return pledgeCapability.pledge; } } @@ -109,7 +117,7 @@ export class Pledge { try { const pledgeResolve = getPledgeResolve(C); - const iteratorRecord = iterable[Symbol.iterator](); + const iteratorRecord = getIterator(iterable); const result = performPledgeRace(iteratorRecord, C, pledgeCapability, pledgeResolve); return result; } catch (error) { @@ -274,9 +282,9 @@ function pledgeResolve(C, x) { function getPledgeResolve(pledgeConstructor) { assertIsConstructor(pledgeConstructor); - const promiseResolve = pledgeConstructor.resolve; + const pledgeResolve = pledgeConstructor.resolve; - if (!isCallable(promiseResolve)) { + if (!isCallable(pledgeResolve)) { throw new TypeError("resolve is not callable."); } @@ -297,11 +305,46 @@ function performPledgeAny(iteratorRecord, constructor, resultCapability, pledgeR const remainingElementsCount = { value: 1 }; let index = 0; - for (const nextValue of iteratorRecord) { + while (true) { + let next; + + try { + next = iteratorStep(iteratorRecord); + } catch (error) { + iteratorRecord.done = true; + resultCapability.reject(error); + return resultCapability.pledge; + } - errors.push(undefined); + if (next === false) { + 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; + } - const nextPledge = pledgeResolve(constructor, nextValue); + let nextValue; + + try { + nextValue = iteratorValue(next); + } catch(error) { + iteratorRecord.done = true; + resultCapability.reject(error); + return resultCapability.pledge; + } + + errors.push(undefined); + const nextPledge = pledgeResolve.call(constructor, nextValue); const rejectElement = createPledgeAnyRejectElement(index, errors, resultCapability, remainingElementsCount); remainingElementsCount.value = remainingElementsCount.value + 1; @@ -309,20 +352,6 @@ function performPledgeAny(iteratorRecord, constructor, resultCapability, pledgeR 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; } //----------------------------------------------------------------------------- @@ -372,10 +401,35 @@ function performPledgeRace(iteratorRecord, constructor, resultCapability, pledge assertIsConstructor(constructor); assertIsCallable(pledgeResolve); - for (const nextValue of iteratorRecord) { - const nextPledge = pledgeResolve(constructor, nextValue); + while (true) { + + let next; + + try { + next = iteratorStep(iteratorRecord); + } catch (error) { + iteratorRecord.done = true; + resultCapability.reject(error); + return resultCapability.pledge; + } + + if (next === false) { + iteratorRecord.done = true; + return resultCapability.pledge; + } + + let nextValue; + + try { + nextValue = iteratorValue(next); + } catch (error) { + iteratorRecord.done = true; + resultCapability.reject(error); + return resultCapability.pledge; + } + + const nextPledge = pledgeResolve.call(constructor, nextValue); nextPledge.then(resultCapability.resolve, resultCapability.reject); } - return resultCapability.pledge; } diff --git a/src/utilities.js b/src/utilities.js index e474fe6..11a1f00 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -60,7 +60,6 @@ export function isConstructor(argument) { return typeof argument === "function" && typeof argument.prototype !== "undefined"; } - //----------------------------------------------------------------------------- // 19.5.7 AggregateError Objects //----------------------------------------------------------------------------- @@ -92,3 +91,111 @@ export function PledgeAggregateError(errors=[], message) { return O; } + +//----------------------------------------------------------------------------- +// 7.4.1 GetIterator ( obj [ , hint [ , method ] ] ) +//----------------------------------------------------------------------------- + +export function getIterator(obj, hint="sync", method) { + + if (hint !== "sync" && hint !== "async") { + throw new TypeError("Invalid hint."); + } + + if (method === undefined) { + + if (hint === "async") { + + method = obj[Symbol.asyncIterator]; + + if (method === undefined) { + const syncMethod = obj[Symbol.iterator]; + const syncIteratorRecord = getIterator(obj, "sync", syncMethod); + + // can't accurately represent CreateAsyncFromSyncIterator() + return syncIteratorRecord; + } + } else { + method = obj[Symbol.iterator]; + } + } + + const iterator = method.call(obj); + + if (!isObject(iterator)) { + throw new TypeError("Iterator must be an object."); + } + + const nextMethod = iterator.next; + + return { + iterator, + nextMethod, + done: false + }; + +} + +//----------------------------------------------------------------------------- +// 7.4.2 IteratorNext ( iteratorRecord [ , value ] ) +//----------------------------------------------------------------------------- + +export function iteratorNext(iteratorRecord, value) { + + let result; + + if (value === undefined) { + result = iteratorRecord.nextMethod.call(iteratorRecord.iterator); + } else { + result = iteratorRecord.nextMethod.call(iteratorRecord.iterator, value); + } + + if (!isObject(result)) { + throw new TypeError("Result must be an object."); + } + + return result; + +} + +//----------------------------------------------------------------------------- +// 7.4.3 IteratorComplete(iterResult) +//----------------------------------------------------------------------------- + +export function iteratorComplete(iterResult) { + + if (!isObject(iterResult)) { + throw new TypeError("Argument must be an object."); + } + + return Boolean(iterResult.done); +} + +//----------------------------------------------------------------------------- +// 7.4.4 IteratorValue(iterResult) +//----------------------------------------------------------------------------- + +export function iteratorValue(iterResult) { + + if (!isObject(iterResult)) { + throw new TypeError("Argument must be an object."); + } + + return iterResult.value; +} + +//----------------------------------------------------------------------------- +// 7.4.5 IteratorStep ( iteratorRecord ) +//----------------------------------------------------------------------------- + +export function iteratorStep(iteratorRecord) { + + const result = iteratorNext(iteratorRecord); + const done = iteratorComplete(result); + + if (done) { + return false; + } + + return result; +} diff --git a/tests/pledge.test.js b/tests/pledge.test.js index eef8849..79d210d 100644 --- a/tests/pledge.test.js +++ b/tests/pledge.test.js @@ -385,6 +385,66 @@ describe("Pledge", () => { }).to.throw(/constructor/); }); + it("should reject a pledge when retrieving the iterator throws an error", done => { + + const iterable = { + [Symbol.iterator]() { + throw new Error("Uh oh"); + } + }; + + const pledge = Pledge.race(iterable); + pledge.catch(error => { + expect(error.message).to.equal("Uh oh"); + done(); + }); + }); + + it("should reject a pledge when iterator.next() throws an error", done => { + + const iterable = { + [Symbol.iterator]() { + return { + next() { + throw new Error("Oops"); + } + }; + } + }; + + const pledge = Pledge.race(iterable); + pledge.catch(error => { + expect(error.message).to.equal("Oops"); + done(); + }); + }); + + it("should reject a pledge when iterator.next().value throws an error", done => { + + const iterable = { + [Symbol.iterator]() { + return { + next() { + return { + done: false, + get value() { + throw new Error("Sorry"); + } + }; + } + }; + } + }; + + const pledge = Pledge.race(iterable); + pledge.catch(error => { + expect(error.message).to.equal("Sorry"); + done(); + }); + }); + + + it("should return the first value that was resolved", done => { const pledge = Pledge.race([ @@ -454,6 +514,64 @@ describe("Pledge", () => { }).to.throw(/constructor/); }); + it("should reject a pledge when retrieving the iterator throws an error", done => { + + const iterable = { + [Symbol.iterator]() { + throw new Error("Uh oh"); + } + }; + + const pledge = Pledge.any(iterable); + pledge.catch(error => { + expect(error.message).to.equal("Uh oh"); + done(); + }); + }); + + it("should reject a pledge when iterator.next() throws an error", done => { + + const iterable = { + [Symbol.iterator]() { + return { + next() { + throw new Error("Oops"); + } + }; + } + }; + + const pledge = Pledge.any(iterable); + pledge.catch(error => { + expect(error.message).to.equal("Oops"); + done(); + }); + }); + + it("should reject a pledge when iterator.next().value throws an error", done => { + + const iterable = { + [Symbol.iterator]() { + return { + next() { + return { + done: false, + get value() { + throw new Error("Sorry"); + } + }; + } + }; + } + }; + + const pledge = Pledge.any(iterable); + pledge.catch(error => { + expect(error.message).to.equal("Sorry"); + done(); + }); + }); + it("should return the first value that was resolved", done => { const pledge = Pledge.any([