Skip to content

Support iterable type for all() + race() + any() #225

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
135 changes: 74 additions & 61 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand All @@ -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);
}
Expand Down
54 changes: 54 additions & 0 deletions tests/FunctionAllTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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()
{
Expand Down
Loading