diff --git a/README.md b/README.md index 9967bc3f..d2e94534 100644 --- a/README.md +++ b/README.md @@ -376,7 +376,7 @@ reject a promise, any language error or user land exception can be used to rejec #### all() ```php -$promise = React\Promise\all(array $promisesOrValues); +$promise = React\Promise\all(iterable $promisesOrValues); ``` Returns a promise that will resolve only once all the items in @@ -387,7 +387,7 @@ will be an array containing the resolution values of each of the items in #### race() ```php -$promise = React\Promise\race(array $promisesOrValues); +$promise = React\Promise\race(iterable $promisesOrValues); ``` Initiates a competitive race that allows one winner. Returns a promise which is @@ -399,7 +399,7 @@ contains 0 items. #### any() ```php -$promise = React\Promise\any(array $promisesOrValues); +$promise = React\Promise\any(iterable $promisesOrValues); ``` Returns a promise that will resolve when any one of the items in diff --git a/src/functions.php b/src/functions.php index ad788f4f..fbe4961d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -68,36 +68,45 @@ function reject(\Throwable $reason): PromiseInterface * will be an array containing the resolution values of each of the items in * `$promisesOrValues`. * - * @param array $promisesOrValues + * @param iterable $promisesOrValues * @return PromiseInterface */ -function all(array $promisesOrValues): PromiseInterface +function all(iterable $promisesOrValues): PromiseInterface { - if (!$promisesOrValues) { - return resolve([]); - } - $cancellationQueue = new Internal\CancellationQueue(); return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void { - $toResolve = \count($promisesOrValues); + $toResolve = 0; + $continue = true; $values = []; foreach ($promisesOrValues as $i => $promiseOrValue) { $cancellationQueue->enqueue($promiseOrValue); $values[$i] = null; + ++$toResolve; + + resolve($promiseOrValue)->then( + function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void { + $values[$i] = $value; + + if (0 === --$toResolve && !$continue) { + $resolve($values); + } + }, + function (\Throwable $reason) use (&$continue, $reject): void { + $continue = false; + $reject($reason); + } + ); + + if (!$continue) { + break; + } + } - resolve($promiseOrValue) - ->then( - function ($mapped) use ($i, &$values, &$toResolve, $resolve): void { - $values[$i] = $mapped; - - if (0 === --$toResolve) { - $resolve($values); - } - }, - $reject - ); + $continue = false; + if ($toResolve === 0) { + $resolve($values); } }, $cancellationQueue); } @@ -109,23 +118,26 @@ function ($mapped) use ($i, &$values, &$toResolve, $resolve): void { * The returned promise will become **infinitely pending** if `$promisesOrValues` * contains 0 items. * - * @param array $promisesOrValues + * @param iterable $promisesOrValues * @return PromiseInterface */ -function race(array $promisesOrValues): PromiseInterface +function race(iterable $promisesOrValues): PromiseInterface { - if (!$promisesOrValues) { - return new Promise(function (): void {}); - } - $cancellationQueue = new Internal\CancellationQueue(); return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void { + $continue = true; + foreach ($promisesOrValues as $promiseOrValue) { $cancellationQueue->enqueue($promiseOrValue); - resolve($promiseOrValue) - ->then($resolve, $reject); + resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void { + $continue = false; + }); + + if (!$continue) { + break; + } } }, $cancellationQueue); } @@ -141,53 +153,54 @@ function race(array $promisesOrValues): PromiseInterface * The returned promise will also reject with a `React\Promise\Exception\LengthException` * if `$promisesOrValues` contains 0 items. * - * @param array $promisesOrValues + * @param iterable $promisesOrValues * @return PromiseInterface */ -function any(array $promisesOrValues): PromiseInterface +function any(iterable $promisesOrValues): PromiseInterface { - $len = \count($promisesOrValues); - - if (!$promisesOrValues) { - return reject( - new Exception\LengthException( - \sprintf( - 'Input array must contain at least 1 item but contains only %s item%s.', - $len, - 1 === $len ? '' : 's' - ) - ) - ); - } - $cancellationQueue = new Internal\CancellationQueue(); - return new Promise(function ($resolve, $reject) use ($len, $promisesOrValues, $cancellationQueue): void { - $toReject = $len; - $reasons = []; + return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void { + $toReject = 0; + $continue = true; + $reasons = []; foreach ($promisesOrValues as $i => $promiseOrValue) { - $fulfiller = function ($val) use ($resolve): void { - $resolve($val); - }; - - $rejecter = function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject): void { - $reasons[$i] = $reason; - - if (0 === --$toReject) { - $reject( - new CompositeException( + $cancellationQueue->enqueue($promiseOrValue); + ++$toReject; + + resolve($promiseOrValue)->then( + function ($value) use ($resolve, &$continue): void { + $continue = false; + $resolve($value); + }, + function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continue): void { + $reasons[$i] = $reason; + + if (0 === --$toReject && !$continue) { + $reject(new CompositeException( $reasons, 'All promises rejected.' - ) - ); + )); + } } - }; + ); - $cancellationQueue->enqueue($promiseOrValue); + if (!$continue) { + break; + } + } - resolve($promiseOrValue) - ->then($fulfiller, $rejecter); + $continue = false; + if ($toReject === 0 && !$reasons) { + $reject(new Exception\LengthException( + 'Must contain at least 1 item but contains only 0 items.' + )); + } elseif ($toReject === 0) { + $reject(new CompositeException( + $reasons, + 'All promises rejected.' + )); } }, $cancellationQueue); } diff --git a/tests/FunctionAllTest.php b/tests/FunctionAllTest.php index a268c21a..4d91eb04 100644 --- a/tests/FunctionAllTest.php +++ b/tests/FunctionAllTest.php @@ -58,6 +58,42 @@ public function shouldResolveSparseArrayInput() ->then($mock); } + /** @test */ + public function shouldResolveValuesGenerator() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(self::identicalTo([1, 2, 3])); + + $gen = (function () { + for ($i = 1; $i <= 3; ++$i) { + yield $i; + } + })(); + + all($gen)->then($mock); + } + + /** @test */ + public function shouldResolveValuesGeneratorEmpty() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(self::identicalTo([])); + + $gen = (function () { + if (false) { + yield; + } + })(); + + all($gen)->then($mock); + } + /** @test */ public function shouldRejectIfAnyInputPromiseRejects() { @@ -74,6 +110,24 @@ public function shouldRejectIfAnyInputPromiseRejects() ->then($this->expectCallableNever(), $mock); } + /** @test */ + public function shouldRejectInfiteGeneratorOrRejectedPromises() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(new \RuntimeException('Iteration 1')); + + $gen = (function () { + for ($i = 1; ; ++$i) { + yield reject(new \RuntimeException('Iteration ' . $i)); + } + })(); + + all($gen)->then(null, $mock); + } + /** @test */ public function shouldPreserveTheOrderOfArrayWhenResolvingAsyncPromises() { diff --git a/tests/FunctionAnyTest.php b/tests/FunctionAnyTest.php index 09e73548..57661641 100644 --- a/tests/FunctionAnyTest.php +++ b/tests/FunctionAnyTest.php @@ -18,7 +18,7 @@ public function shouldRejectWithLengthExceptionWithEmptyInputArray() ->with( self::callback(function ($exception) { return $exception instanceof LengthException && - 'Input array must contain at least 1 item but contains only 0 items.' === $exception->getMessage(); + 'Must contain at least 1 item but contains only 0 items.' === $exception->getMessage(); }) ); @@ -26,6 +26,24 @@ public function shouldRejectWithLengthExceptionWithEmptyInputArray() ->then($this->expectCallableNever(), $mock); } + /** @test */ + public function shouldRejectWithLengthExceptionWithEmptyInputGenerator() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(new LengthException('Must contain at least 1 item but contains only 0 items.')); + + $gen = (function () { + if (false) { + yield; + } + })(); + + any($gen)->then($this->expectCallableNever(), $mock); + } + /** @test */ public function shouldResolveWithAnInputValue() { @@ -52,6 +70,58 @@ public function shouldResolveWithAPromisedInputValue() ->then($mock); } + /** @test */ + public function shouldResolveWithAnInputValueFromDeferred() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(self::identicalTo(1)); + + $deferred = new Deferred(); + + any([$deferred->promise()])->then($mock); + + $deferred->resolve(1); + } + + /** @test */ + public function shouldResolveValuesGenerator() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(self::identicalTo(1)); + + $gen = (function () { + for ($i = 1; $i <= 3; ++$i) { + yield $i; + } + })(); + + any($gen)->then($mock); + } + + /** @test */ + public function shouldResolveValuesInfiniteGenerator() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(self::identicalTo(1)); + + $gen = (function () { + for ($i = 1; ; ++$i) { + yield $i; + } + })(); + + any($gen)->then($mock); + } + /** @test */ public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected() { @@ -74,6 +144,29 @@ public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected() ->then($this->expectCallableNever(), $mock); } + /** @test */ + public function shouldRejectWithAllRejectedInputValuesIfInputIsRejectedFromDeferred() + { + $exception = new Exception(); + + $compositeException = new CompositeException( + [2 => $exception], + 'All promises rejected.' + ); + + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with($compositeException); + + $deferred = new Deferred(); + + any([2 => $deferred->promise()])->then($this->expectCallableNever(), $mock); + + $deferred->reject($exception); + } + /** @test */ public function shouldResolveWhenFirstInputPromiseResolves() { diff --git a/tests/FunctionRaceTest.php b/tests/FunctionRaceTest.php index 40200256..6e69be07 100644 --- a/tests/FunctionRaceTest.php +++ b/tests/FunctionRaceTest.php @@ -65,6 +65,42 @@ public function shouldResolveSparseArrayInput() )->then($mock); } + /** @test */ + public function shouldResolveValuesGenerator() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(self::identicalTo(1)); + + $gen = (function () { + for ($i = 1; $i <= 3; ++$i) { + yield $i; + } + })(); + + race($gen)->then($mock); + } + + /** @test */ + public function shouldResolveValuesInfiniteGenerator() + { + $mock = $this->createCallableMock(); + $mock + ->expects(self::once()) + ->method('__invoke') + ->with(self::identicalTo(1)); + + $gen = (function () { + for ($i = 1; ; ++$i) { + yield $i; + } + })(); + + race($gen)->then($mock); + } + /** @test */ public function shouldRejectIfFirstSettledPromiseRejects() {