diff --git a/README.md b/README.md index 132183c4..c258990b 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,6 @@ Table of Contents * [all()](#all) * [race()](#race) * [any()](#any) - * [some()](#some) - * [map()](#map) - * [reduce()](#reduce) 4. [Examples](#examples) * [How to use Deferred](#how-to-use-deferred) * [How promise forwarding works](#how-promise-forwarding-works) @@ -365,10 +362,9 @@ once all consumers called the `cancel()` method of the promise. ### Functions -Useful functions for creating, joining, mapping and reducing collections of -promises. +Useful functions for creating and joining collections of promises. -All functions working on promise collections (like `all()`, `race()`, `some()` +All functions working on promise collections (like `all()`, `race()`, etc.) support cancellation. This means, if you call `cancel()` on the returned promise, all promises in the collection are cancelled. @@ -442,49 +438,6 @@ which holds all rejection reasons. The rejection reasons can be obtained with The returned promise will also reject with a `React\Promise\Exception\LengthException` if `$promisesOrValues` contains 0 items. -#### some() - -```php -$promise = React\Promise\some(array $promisesOrValues, integer $howMany); -``` - -Returns a promise that will resolve when at least `$howMany` of the supplied items in -`$promisesOrValues` fulfill. The resolution value of the returned promise -will be an array of length `$howMany` containing the resolution values of -`$howMany` fulfilled promises that were resolved first. - -The returned promise will reject if it becomes impossible for `$howMany` items -to resolve (that is, when `(count($promisesOrValues) - $howMany) + 1` items -reject). The rejection value will be a `React\Promise\Exception\CompositeException` -which holds `(count($promisesOrValues) - $howMany) + 1` rejection reasons. -The rejection reasons can be obtained with `CompositeException::getExceptions()`. - -The returned promise will also reject with a `React\Promise\Exception\LengthException` -if `$promisesOrValues` contains less items than `$howMany`. - -#### map() - -```php -$promise = React\Promise\map(array $promisesOrValues, callable $mapFunc); -``` - -Traditional map function, similar to `array_map()`, but allows input to contain -promises and/or values, and `$mapFunc` may return either a value or a promise. - -The map function receives each item as argument, where item is a fully resolved -value of a promise or value in `$promisesOrValues`. - -#### reduce() - -```php -$promise = React\Promise\reduce(array $promisesOrValues, callable $reduceFunc, $initialValue = null); -``` - -Traditional reduce function, similar to `array_reduce()`, but input may contain -promises and/or values, and `$reduceFunc` may return either a value or a -promise, *and* `$initialValue` may be a promise or a value for the starting -value. - Examples -------- diff --git a/src/functions.php b/src/functions.php index 38b036a9..f640fe1b 100644 --- a/src/functions.php +++ b/src/functions.php @@ -73,9 +73,33 @@ function reject(\Throwable $reason): PromiseInterface */ function all(array $promisesOrValues): PromiseInterface { - return map($promisesOrValues, function ($val) { - return $val; - }); + if (!$promisesOrValues) { + return resolve([]); + } + + $cancellationQueue = new Internal\CancellationQueue(); + + return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void { + $toResolve = \count($promisesOrValues); + $values = []; + + foreach ($promisesOrValues as $i => $promiseOrValue) { + $cancellationQueue->enqueue($promiseOrValue); + $values[$i] = null; + + resolve($promiseOrValue) + ->done( + function ($mapped) use ($i, &$values, &$toResolve, $resolve): void { + $values[$i] = $mapped; + + if (0 === --$toResolve) { + $resolve($values); + } + }, + $reject + ); + } + }, $cancellationQueue); } /** @@ -122,45 +146,13 @@ function race(array $promisesOrValues): PromiseInterface */ function any(array $promisesOrValues): PromiseInterface { - return some($promisesOrValues, 1) - ->then(function ($val) { - return \array_shift($val); - }); -} - -/** - * Returns a promise that will resolve when `$howMany` of the supplied items in - * `$promisesOrValues` resolve. The resolution value of the returned promise - * will be an array of length `$howMany` containing the resolution values of the - * triggering items. - * - * The returned promise will reject if it becomes impossible for `$howMany` items - * to resolve (that is, when `(count($promisesOrValues) - $howMany) + 1` items - * reject). The rejection value will be an array of - * `(count($promisesOrValues) - $howMany) + 1` rejection reasons. - * - * The returned promise will also reject with a `React\Promise\Exception\LengthException` - * if `$promisesOrValues` contains less items than `$howMany`. - * - * @param array $promisesOrValues - * @param int $howMany - * @return PromiseInterface - */ -function some(array $promisesOrValues, int $howMany): PromiseInterface -{ - if ($howMany < 1) { - return resolve([]); - } - $len = \count($promisesOrValues); - if ($len < $howMany) { + if (!$promisesOrValues) { return reject( new Exception\LengthException( \sprintf( - 'Input array must contain at least %d item%s but contains only %s item%s.', - $howMany, - 1 === $howMany ? '' : 's', + 'Input array must contain at least 1 item but contains only %s item%s.', $len, 1 === $len ? '' : 's' ) @@ -170,37 +162,23 @@ function some(array $promisesOrValues, int $howMany): PromiseInterface $cancellationQueue = new Internal\CancellationQueue(); - return new Promise(function ($resolve, $reject) use ($len, $promisesOrValues, $howMany, $cancellationQueue): void { - $toResolve = $howMany; - $toReject = ($len - $toResolve) + 1; - $values = []; + return new Promise(function ($resolve, $reject) use ($len, $promisesOrValues, $cancellationQueue): void { + $toReject = $len; $reasons = []; foreach ($promisesOrValues as $i => $promiseOrValue) { - $fulfiller = function ($val) use ($i, &$values, &$toResolve, $toReject, $resolve): void { - if ($toResolve < 1 || $toReject < 1) { - return; - } - - $values[$i] = $val; - - if (0 === --$toResolve) { - $resolve($values); - } + $fulfiller = function ($val) use ($resolve): void { + $resolve($val); }; - $rejecter = function (\Throwable $reason) use ($i, &$reasons, &$toReject, $toResolve, $reject): void { - if ($toResolve < 1 || $toReject < 1) { - return; - } - + $rejecter = function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject): void { $reasons[$i] = $reason; if (0 === --$toReject) { $reject( new CompositeException( $reasons, - 'Too many promises rejected.' + 'All promises rejected.' ) ); } @@ -214,87 +192,6 @@ function some(array $promisesOrValues, int $howMany): PromiseInterface }, $cancellationQueue); } -/** - * Traditional map function, similar to `array_map()`, but allows input to contain - * promises and/or values, and `$mapFunc` may return either a value or a promise. - * - * The map function receives each item as argument, where item is a fully resolved - * value of a promise or value in `$promisesOrValues`. - * - * @param array $promisesOrValues - * @param callable $mapFunc - * @return PromiseInterface - */ -function map(array $promisesOrValues, callable $mapFunc): PromiseInterface -{ - if (!$promisesOrValues) { - return resolve([]); - } - - $cancellationQueue = new Internal\CancellationQueue(); - - return new Promise(function ($resolve, $reject) use ($promisesOrValues, $mapFunc, $cancellationQueue): void { - $toResolve = \count($promisesOrValues); - $values = []; - - foreach ($promisesOrValues as $i => $promiseOrValue) { - $cancellationQueue->enqueue($promiseOrValue); - $values[$i] = null; - - resolve($promiseOrValue) - ->then($mapFunc) - ->done( - function ($mapped) use ($i, &$values, &$toResolve, $resolve): void { - $values[$i] = $mapped; - - if (0 === --$toResolve) { - $resolve($values); - } - }, - $reject - ); - } - }, $cancellationQueue); -} - -/** - * Traditional reduce function, similar to `array_reduce()`, but input may contain - * promises and/or values, and `$reduceFunc` may return either a value or a - * promise, *and* `$initialValue` may be a promise or a value for the starting - * value. - * - * @param array $promisesOrValues - * @param callable $reduceFunc - * @param mixed $initialValue - * @return PromiseInterface - */ -function reduce(array $promisesOrValues, callable $reduceFunc, $initialValue = null): PromiseInterface -{ - $cancellationQueue = new Internal\CancellationQueue(); - - return new Promise(function ($resolve, $reject) use ($promisesOrValues, $reduceFunc, $initialValue, $cancellationQueue): void { - $total = \count($promisesOrValues); - $i = 0; - - $wrappedReduceFunc = function ($current, $val) use ($reduceFunc, $cancellationQueue, $total, &$i): PromiseInterface { - $cancellationQueue->enqueue($val); - - return $current - ->then(function ($c) use ($reduceFunc, $total, &$i, $val) { - return resolve($val) - ->then(function ($value) use ($reduceFunc, $total, &$i, $c) { - return $reduceFunc($c, $value, $i++, $total); - }); - }); - }; - - $cancellationQueue->enqueue($initialValue); - - \array_reduce($promisesOrValues, $wrappedReduceFunc, resolve($initialValue)) - ->done($resolve, $reject); - }, $cancellationQueue); -} - /** * @internal */ diff --git a/tests/FunctionAnyTest.php b/tests/FunctionAnyTest.php index 71c8aba5..09e73548 100644 --- a/tests/FunctionAnyTest.php +++ b/tests/FunctionAnyTest.php @@ -61,7 +61,7 @@ public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected() $compositeException = new CompositeException( [0 => $exception1, 1 => $exception2, 2 => $exception3], - 'Too many promises rejected.' + 'All promises rejected.' ); $mock = $this->createCallableMock(); @@ -126,6 +126,6 @@ public function shouldNotCancelOtherPendingInputArrayPromisesIfOnePromiseFulfill $promise2 = new Promise(function () {}, $this->expectCallableNever()); - some([$deferred->promise(), $promise2], 1)->cancel(); + any([$deferred->promise(), $promise2], 1)->cancel(); } } diff --git a/tests/FunctionMapTest.php b/tests/FunctionMapTest.php deleted file mode 100644 index 46441d37..00000000 --- a/tests/FunctionMapTest.php +++ /dev/null @@ -1,131 +0,0 @@ -createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([2, 4, 6])); - - map( - [1, 2, 3], - $this->mapper() - )->then($mock); - } - - /** @test */ - public function shouldMapInputPromisesArray() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([2, 4, 6])); - - map( - [resolve(1), resolve(2), resolve(3)], - $this->mapper() - )->then($mock); - } - - /** @test */ - public function shouldMapMixedInputArray() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([2, 4, 6])); - - map( - [1, resolve(2), 3], - $this->mapper() - )->then($mock); - } - - /** @test */ - public function shouldMapInputWhenMapperReturnsAPromise() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([2, 4, 6])); - - map( - [1, 2, 3], - $this->promiseMapper() - )->then($mock); - } - - /** @test */ - public function shouldPreserveTheOrderOfArrayWhenResolvingAsyncPromises() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([2, 4, 6])); - - $deferred = new Deferred(); - - map( - [resolve(1), $deferred->promise(), resolve(3)], - $this->mapper() - )->then($mock); - - $deferred->resolve(2); - } - - /** @test */ - public function shouldRejectWhenInputContainsRejection() - { - $exception2 = new Exception(); - $exception3 = new Exception(); - - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo($exception2)); - - map( - [resolve(1), reject($exception2), resolve($exception3)], - $this->mapper() - )->then($this->expectCallableNever(), $mock); - } - - /** @test */ - public function shouldCancelInputArrayPromises() - { - $promise1 = new Promise(function () {}, $this->expectCallableOnce()); - $promise2 = new Promise(function () {}, $this->expectCallableOnce()); - - map( - [$promise1, $promise2], - $this->mapper() - )->cancel(); - } -} diff --git a/tests/FunctionReduceTest.php b/tests/FunctionReduceTest.php deleted file mode 100644 index 3f6db624..00000000 --- a/tests/FunctionReduceTest.php +++ /dev/null @@ -1,275 +0,0 @@ -createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(6)); - - reduce( - [1, 2, 3], - $this->plus() - )->then($mock); - } - - /** @test */ - public function shouldReduceValuesWithInitialValue() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(7)); - - reduce( - [1, 2, 3], - $this->plus(), - 1 - )->then($mock); - } - - /** @test */ - public function shouldReduceValuesWithInitialPromise() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(7)); - - reduce( - [1, 2, 3], - $this->plus(), - resolve(1) - )->then($mock); - } - - /** @test */ - public function shouldReducePromisedValuesWithoutInitialValue() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(6)); - - reduce( - [resolve(1), resolve(2), resolve(3)], - $this->plus() - )->then($mock); - } - - /** @test */ - public function shouldReducePromisedValuesWithInitialValue() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(7)); - - reduce( - [resolve(1), resolve(2), resolve(3)], - $this->plus(), - 1 - )->then($mock); - } - - /** @test */ - public function shouldReducePromisedValuesWithInitialPromise() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(7)); - - reduce( - [resolve(1), resolve(2), resolve(3)], - $this->plus(), - resolve(1) - )->then($mock); - } - - /** @test */ - public function shouldReduceEmptyInputWithInitialValue() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(1)); - - reduce( - [], - $this->plus(), - 1 - )->then($mock); - } - - /** @test */ - public function shouldReduceEmptyInputWithInitialPromise() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(1)); - - reduce( - [], - $this->plus(), - resolve(1) - )->then($mock); - } - - /** @test */ - public function shouldRejectWhenInputContainsRejection() - { - $exception2 = new Exception(); - - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo($exception2)); - - reduce( - [resolve(1), reject($exception2), resolve(3)], - $this->plus(), - resolve(1) - )->then($this->expectCallableNever(), $mock); - } - - /** @test */ - public function shouldResolveWithNullWhenInputIsEmptyAndNoInitialValueOrPromiseProvided() - { - // Note: this is different from when.js's behavior! - // In when.reduce(), this rejects with a TypeError exception (following - // JavaScript's [].reduce behavior. - // We're following PHP's array_reduce behavior and resolve with NULL. - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(null)); - - reduce( - [], - $this->plus() - )->then($mock); - } - - /** @test */ - public function shouldAllowSparseArrayInputWithoutInitialValue() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(3)); - - reduce( - [null, null, 1, null, 1, 1], - $this->plus() - )->then($mock); - } - - /** @test */ - public function shouldAllowSparseArrayInputWithInitialValue() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo(4)); - - reduce( - [null, null, 1, null, 1, 1], - $this->plus(), - 1 - )->then($mock); - } - - /** @test */ - public function shouldReduceInInputOrder() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo('123')); - - reduce( - [1, 2, 3], - $this->append(), - '' - )->then($mock); - } - - /** @test */ - public function shouldProvideCorrectBasisValue() - { - $insertIntoArray = function ($arr, $val, $i) { - $arr[$i] = $val; - - return $arr; - }; - - $d1 = new Deferred(); - $d2 = new Deferred(); - $d3 = new Deferred(); - - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([1, 2, 3])); - - reduce( - [$d1->promise(), $d2->promise(), $d3->promise()], - $insertIntoArray, - [] - )->then($mock); - - $d3->resolve(3); - $d1->resolve(1); - $d2->resolve(2); - } - - /** @test */ - public function shouldCancelInputArrayPromises() - { - $promise1 = new Promise(function () {}, $this->expectCallableOnce()); - $promise2 = new Promise(function () {}, $this->expectCallableOnce()); - - reduce( - [$promise1, $promise2], - $this->plus(), - 1 - )->cancel(); - } -} diff --git a/tests/FunctionSomeTest.php b/tests/FunctionSomeTest.php deleted file mode 100644 index f5c55df9..00000000 --- a/tests/FunctionSomeTest.php +++ /dev/null @@ -1,168 +0,0 @@ -createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with( - self::callback(function ($exception) { - return $exception instanceof LengthException && - 'Input array must contain at least 1 item but contains only 0 items.' === $exception->getMessage(); - }) - ); - - some( - [], - 1 - )->then($this->expectCallableNever(), $mock); - } - - /** @test */ - public function shouldRejectWithLengthExceptionWithInputArrayContainingNotEnoughItems() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with( - self::callback(function ($exception) { - return $exception instanceof LengthException && - 'Input array must contain at least 4 items but contains only 3 items.' === $exception->getMessage(); - }) - ); - - some( - [1, 2, 3], - 4 - )->then($this->expectCallableNever(), $mock); - } - - /** @test */ - public function shouldResolveValuesArray() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([1, 2])); - - some( - [1, 2, 3], - 2 - )->then($mock); - } - - /** @test */ - public function shouldResolvePromisesArray() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([1, 2])); - - some( - [resolve(1), resolve(2), resolve(3)], - 2 - )->then($mock); - } - - /** @test */ - public function shouldResolveSparseArrayInput() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([null, 1])); - - some( - [null, 1, null, 2, 3], - 2 - )->then($mock); - } - - /** - * @test - * @group 123 - */ - public function shouldRejectIfAnyInputPromiseRejectsBeforeDesiredNumberOfInputsAreResolved() - { - $exception2 = new Exception(); - $exception3 = new Exception(); - - $compositeException = new CompositeException( - [1 => $exception2, 2 => $exception3], - 'Too many promises rejected.' - ); - - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with($compositeException); - - some( - [resolve(1), reject($exception2), reject($exception3)], - 2 - )->then($this->expectCallableNever(), $mock); - } - - - /** @test */ - public function shouldResolveWithEmptyArrayIfHowManyIsLessThanOne() - { - $mock = $this->createCallableMock(); - $mock - ->expects(self::once()) - ->method('__invoke') - ->with(self::identicalTo([])); - - some( - [1], - 0 - )->then($mock); - } - - /** @test */ - public function shouldCancelInputArrayPromises() - { - $promise1 = new Promise(function () {}, $this->expectCallableOnce()); - $promise2 = new Promise(function () {}, $this->expectCallableOnce()); - - some([$promise1, $promise2], 1)->cancel(); - } - - /** @test */ - public function shouldCancelOtherPendingInputArrayPromisesIfEnoughPromisesFulfill() - { - $deferred = new Deferred($this->expectCallableNever()); - $deferred->resolve(null); - - $promise2 = new Promise(function () {}, $this->expectCallableNever()); - - some([$deferred->promise(), $promise2], 1); - } - - /** @test */ - public function shouldNotCancelOtherPendingInputArrayPromisesIfEnoughPromisesReject() - { - $deferred = new Deferred($this->expectCallableNever()); - $deferred->reject(new Exception()); - - $promise2 = new Promise(function () {}, $this->expectCallableNever()); - - some([$deferred->promise(), $promise2], 2); - } -}