Skip to content

Commit 57fa798

Browse files
committed
Take advantage of iterators instead of converting to array first
1 parent 9219299 commit 57fa798

File tree

4 files changed

+197
-67
lines changed

4 files changed

+197
-67
lines changed

src/functions.php

Lines changed: 68 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -73,35 +73,40 @@ function reject(\Throwable $reason): PromiseInterface
7373
*/
7474
function all(iterable $promisesOrValues): PromiseInterface
7575
{
76-
if (!\is_array($promisesOrValues)) {
77-
$promisesOrValues = \iterator_to_array($promisesOrValues);
78-
}
79-
80-
if (!$promisesOrValues) {
81-
return resolve([]);
82-
}
83-
8476
$cancellationQueue = new Internal\CancellationQueue();
8577

8678
return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void {
87-
$toResolve = \count($promisesOrValues);
79+
$toResolve = 0;
80+
$continue = true;
8881
$values = [];
8982

9083
foreach ($promisesOrValues as $i => $promiseOrValue) {
9184
$cancellationQueue->enqueue($promiseOrValue);
9285
$values[$i] = null;
86+
++$toResolve;
87+
88+
resolve($promiseOrValue)->then(
89+
function ($value) use ($i, &$values, &$toResolve, &$continue, $resolve): void {
90+
$values[$i] = $value;
91+
92+
if (0 === --$toResolve && !$continue) {
93+
$resolve($values);
94+
}
95+
},
96+
function (\Throwable $reason) use (&$continue, $reject): void {
97+
$continue = false;
98+
$reject($reason);
99+
}
100+
);
93101

94-
resolve($promiseOrValue)
95-
->then(
96-
function ($mapped) use ($i, &$values, &$toResolve, $resolve): void {
97-
$values[$i] = $mapped;
98-
99-
if (0 === --$toResolve) {
100-
$resolve($values);
101-
}
102-
},
103-
$reject
104-
);
102+
if (!$continue) {
103+
break;
104+
}
105+
}
106+
107+
$continue = false;
108+
if ($toResolve === 0) {
109+
$resolve($values);
105110
}
106111
}, $cancellationQueue);
107112
}
@@ -118,22 +123,21 @@ function ($mapped) use ($i, &$values, &$toResolve, $resolve): void {
118123
*/
119124
function race(iterable $promisesOrValues): PromiseInterface
120125
{
121-
if (!\is_array($promisesOrValues)) {
122-
$promisesOrValues = \iterator_to_array($promisesOrValues);
123-
}
124-
125-
if (!$promisesOrValues) {
126-
return new Promise(function (): void {});
127-
}
128-
129126
$cancellationQueue = new Internal\CancellationQueue();
130127

131128
return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void {
129+
$continue = true;
130+
132131
foreach ($promisesOrValues as $promiseOrValue) {
133132
$cancellationQueue->enqueue($promiseOrValue);
134133

135-
resolve($promiseOrValue)
136-
->then($resolve, $reject);
134+
resolve($promiseOrValue)->then($resolve, $reject)->finally(function () use (&$continue): void {
135+
$continue = false;
136+
});
137+
138+
if (!$continue) {
139+
break;
140+
}
137141
}
138142
}, $cancellationQueue);
139143
}
@@ -154,52 +158,49 @@ function race(iterable $promisesOrValues): PromiseInterface
154158
*/
155159
function any(iterable $promisesOrValues): PromiseInterface
156160
{
157-
if (!\is_array($promisesOrValues)) {
158-
$promisesOrValues = \iterator_to_array($promisesOrValues);
159-
}
160-
161-
$len = \count($promisesOrValues);
162-
163-
if (!$promisesOrValues) {
164-
return reject(
165-
new Exception\LengthException(
166-
\sprintf(
167-
'Must contain at least 1 item but contains only %s item%s.',
168-
$len,
169-
1 === $len ? '' : 's'
170-
)
171-
)
172-
);
173-
}
174-
175161
$cancellationQueue = new Internal\CancellationQueue();
176162

177-
return new Promise(function ($resolve, $reject) use ($len, $promisesOrValues, $cancellationQueue): void {
178-
$toReject = $len;
179-
$reasons = [];
163+
return new Promise(function ($resolve, $reject) use ($promisesOrValues, $cancellationQueue): void {
164+
$toReject = 0;
165+
$continue = true;
166+
$reasons = [];
180167

181168
foreach ($promisesOrValues as $i => $promiseOrValue) {
182-
$fulfiller = function ($val) use ($resolve): void {
183-
$resolve($val);
184-
};
185-
186-
$rejecter = function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject): void {
187-
$reasons[$i] = $reason;
188-
189-
if (0 === --$toReject) {
190-
$reject(
191-
new CompositeException(
169+
$cancellationQueue->enqueue($promiseOrValue);
170+
++$toReject;
171+
172+
resolve($promiseOrValue)->then(
173+
function ($value) use ($resolve, &$continue): void {
174+
$continue = false;
175+
$resolve($value);
176+
},
177+
function (\Throwable $reason) use ($i, &$reasons, &$toReject, $reject, &$continue): void {
178+
$reasons[$i] = $reason;
179+
180+
if (0 === --$toReject && !$continue) {
181+
$reject(new CompositeException(
192182
$reasons,
193183
'All promises rejected.'
194-
)
195-
);
184+
));
185+
}
196186
}
197-
};
187+
);
198188

199-
$cancellationQueue->enqueue($promiseOrValue);
189+
if (!$continue) {
190+
break;
191+
}
192+
}
200193

201-
resolve($promiseOrValue)
202-
->then($fulfiller, $rejecter);
194+
$continue = false;
195+
if ($toReject === 0 && !$reasons) {
196+
$reject(new Exception\LengthException(
197+
'Must contain at least 1 item but contains only 0 items.'
198+
));
199+
} elseif ($toReject === 0) {
200+
$reject(new CompositeException(
201+
$reasons,
202+
'All promises rejected.'
203+
));
203204
}
204205
}, $cancellationQueue);
205206
}

tests/FunctionAllTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,24 @@ public function shouldResolveValuesGenerator()
7676
all($gen)->then($mock);
7777
}
7878

79+
/** @test */
80+
public function shouldResolveValuesGeneratorEmpty()
81+
{
82+
$mock = $this->createCallableMock();
83+
$mock
84+
->expects(self::once())
85+
->method('__invoke')
86+
->with(self::identicalTo([]));
87+
88+
$gen = (function () {
89+
if (false) {
90+
yield;
91+
}
92+
})();
93+
94+
all($gen)->then($mock);
95+
}
96+
7997
/** @test */
8098
public function shouldRejectIfAnyInputPromiseRejects()
8199
{
@@ -92,6 +110,24 @@ public function shouldRejectIfAnyInputPromiseRejects()
92110
->then($this->expectCallableNever(), $mock);
93111
}
94112

113+
/** @test */
114+
public function shouldRejectInfiteGeneratorOrRejectedPromises()
115+
{
116+
$mock = $this->createCallableMock();
117+
$mock
118+
->expects(self::once())
119+
->method('__invoke')
120+
->with(new \RuntimeException('Iteration 1'));
121+
122+
$gen = (function () {
123+
for ($i = 1; ; ++$i) {
124+
yield reject(new \RuntimeException('Iteration ' . $i));
125+
}
126+
})();
127+
128+
all($gen)->then(null, $mock);
129+
}
130+
95131
/** @test */
96132
public function shouldPreserveTheOrderOfArrayWhenResolvingAsyncPromises()
97133
{

tests/FunctionAnyTest.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,24 @@ public function shouldRejectWithLengthExceptionWithEmptyInputArray()
2626
->then($this->expectCallableNever(), $mock);
2727
}
2828

29+
/** @test */
30+
public function shouldRejectWithLengthExceptionWithEmptyInputGenerator()
31+
{
32+
$mock = $this->createCallableMock();
33+
$mock
34+
->expects(self::once())
35+
->method('__invoke')
36+
->with(new LengthException('Must contain at least 1 item but contains only 0 items.'));
37+
38+
$gen = (function () {
39+
if (false) {
40+
yield;
41+
}
42+
})();
43+
44+
any($gen)->then($this->expectCallableNever(), $mock);
45+
}
46+
2947
/** @test */
3048
public function shouldResolveWithAnInputValue()
3149
{
@@ -52,6 +70,22 @@ public function shouldResolveWithAPromisedInputValue()
5270
->then($mock);
5371
}
5472

73+
/** @test */
74+
public function shouldResolveWithAnInputValueFromDeferred()
75+
{
76+
$mock = $this->createCallableMock();
77+
$mock
78+
->expects(self::once())
79+
->method('__invoke')
80+
->with(self::identicalTo(1));
81+
82+
$deferred = new Deferred();
83+
84+
any([$deferred->promise()])->then($mock);
85+
86+
$deferred->resolve(1);
87+
}
88+
5589
/** @test */
5690
public function shouldResolveValuesGenerator()
5791
{
@@ -70,6 +104,24 @@ public function shouldResolveValuesGenerator()
70104
any($gen)->then($mock);
71105
}
72106

107+
/** @test */
108+
public function shouldResolveValuesInfiniteGenerator()
109+
{
110+
$mock = $this->createCallableMock();
111+
$mock
112+
->expects(self::once())
113+
->method('__invoke')
114+
->with(self::identicalTo(1));
115+
116+
$gen = (function () {
117+
for ($i = 1; ; ++$i) {
118+
yield $i;
119+
}
120+
})();
121+
122+
any($gen)->then($mock);
123+
}
124+
73125
/** @test */
74126
public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected()
75127
{
@@ -92,6 +144,29 @@ public function shouldRejectWithAllRejectedInputValuesIfAllInputsAreRejected()
92144
->then($this->expectCallableNever(), $mock);
93145
}
94146

147+
/** @test */
148+
public function shouldRejectWithAllRejectedInputValuesIfInputIsRejectedFromDeferred()
149+
{
150+
$exception = new Exception();
151+
152+
$compositeException = new CompositeException(
153+
[2 => $exception],
154+
'All promises rejected.'
155+
);
156+
157+
$mock = $this->createCallableMock();
158+
$mock
159+
->expects(self::once())
160+
->method('__invoke')
161+
->with($compositeException);
162+
163+
$deferred = new Deferred();
164+
165+
any([2 => $deferred->promise()])->then($this->expectCallableNever(), $mock);
166+
167+
$deferred->reject($exception);
168+
}
169+
95170
/** @test */
96171
public function shouldResolveWhenFirstInputPromiseResolves()
97172
{

tests/FunctionRaceTest.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,24 @@ public function shouldResolveValuesGenerator()
8383
race($gen)->then($mock);
8484
}
8585

86+
/** @test */
87+
public function shouldResolveValuesInfiniteGenerator()
88+
{
89+
$mock = $this->createCallableMock();
90+
$mock
91+
->expects(self::once())
92+
->method('__invoke')
93+
->with(self::identicalTo(1));
94+
95+
$gen = (function () {
96+
for ($i = 1; ; ++$i) {
97+
yield $i;
98+
}
99+
})();
100+
101+
race($gen)->then($mock);
102+
}
103+
86104
/** @test */
87105
public function shouldRejectIfFirstSettledPromiseRejects()
88106
{

0 commit comments

Comments
 (0)