Skip to content

Commit 4cadacc

Browse files
authored
Merge pull request reactphp#32 from clue-labs/the-future-is-now
Improve `await()` to avoid unneeded `futureTick()` calls
2 parents 1986075 + 4d8331f commit 4cadacc

File tree

3 files changed

+220
-8
lines changed

3 files changed

+220
-8
lines changed

src/SimpleFiber.php

+21-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
final class SimpleFiber implements FiberInterface
1111
{
1212
private static ?\Fiber $scheduler = null;
13+
private static ?\Closure $suspend = null;
1314
private ?\Fiber $fiber = null;
1415

1516
public function __construct()
@@ -19,22 +20,34 @@ public function __construct()
1920

2021
public function resume(mixed $value): void
2122
{
22-
if ($this->fiber === null) {
23-
Loop::futureTick(static fn() => \Fiber::suspend(static fn() => $value));
24-
return;
23+
if ($this->fiber !== null) {
24+
$this->fiber->resume($value);
25+
} else {
26+
self::$suspend = static fn() => $value;
2527
}
2628

27-
Loop::futureTick(fn() => $this->fiber->resume($value));
29+
if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) {
30+
$suspend = self::$suspend;
31+
self::$suspend = null;
32+
33+
\Fiber::suspend($suspend);
34+
}
2835
}
2936

3037
public function throw(\Throwable $throwable): void
3138
{
32-
if ($this->fiber === null) {
33-
Loop::futureTick(static fn() => \Fiber::suspend(static fn() => throw $throwable));
34-
return;
39+
if ($this->fiber !== null) {
40+
$this->fiber->throw($throwable);
41+
} else {
42+
self::$suspend = static fn() => throw $throwable;
3543
}
3644

37-
Loop::futureTick(fn() => $this->fiber->throw($throwable));
45+
if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) {
46+
$suspend = self::$suspend;
47+
self::$suspend = null;
48+
49+
\Fiber::suspend($suspend);
50+
}
3851
}
3952

4053
public function suspend(): mixed

tests/AsyncTest.php

+60
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use React;
66
use React\EventLoop\Loop;
7+
use React\Promise\Deferred;
78
use React\Promise\Promise;
89
use function React\Async\async;
910
use function React\Async\await;
@@ -84,6 +85,49 @@ public function testAsyncReturnsPendingPromiseWhenCallbackReturnsPendingPromise(
8485
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
8586
}
8687

88+
public function testAsyncWithAwaitReturnsReturnsPromiseFulfilledWithValueImmediatelyWhenPromiseIsFulfilled()
89+
{
90+
$deferred = new Deferred();
91+
92+
$promise = async(function () use ($deferred) {
93+
return await($deferred->promise());
94+
})();
95+
96+
$return = null;
97+
$promise->then(function ($value) use (&$return) {
98+
$return = $value;
99+
});
100+
101+
$this->assertNull($return);
102+
103+
$deferred->resolve(42);
104+
105+
$this->assertEquals(42, $return);
106+
}
107+
108+
public function testAsyncWithAwaitReturnsPromiseRejectedWithExceptionImmediatelyWhenPromiseIsRejected()
109+
{
110+
$deferred = new Deferred();
111+
112+
$promise = async(function () use ($deferred) {
113+
return await($deferred->promise());
114+
})();
115+
116+
$exception = null;
117+
$promise->then(null, function ($reason) use (&$exception) {
118+
$exception = $reason;
119+
});
120+
121+
$this->assertNull($exception);
122+
123+
$deferred->reject(new \RuntimeException('Test', 42));
124+
125+
$this->assertInstanceof(\RuntimeException::class, $exception);
126+
assert($exception instanceof \RuntimeException);
127+
$this->assertEquals('Test', $exception->getMessage());
128+
$this->assertEquals(42, $exception->getCode());
129+
}
130+
87131
public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsAfterAwaitingPromise()
88132
{
89133
$promise = async(function () {
@@ -99,6 +143,22 @@ public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsA
99143
$this->assertEquals(42, $value);
100144
}
101145

146+
public function testAsyncReturnsPromiseThatRejectsWithExceptionWhenCallbackThrowsAfterAwaitingPromise()
147+
{
148+
$promise = async(function () {
149+
$promise = new Promise(function ($_, $reject) {
150+
Loop::addTimer(0.001, fn () => $reject(new \RuntimeException('Foo', 42)));
151+
});
152+
153+
return await($promise);
154+
})();
155+
156+
$this->expectException(\RuntimeException::class);
157+
$this->expectExceptionMessage('Foo');
158+
$this->expectExceptionCode(42);
159+
await($promise);
160+
}
161+
102162
public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsAfterAwaitingTwoConcurrentPromises()
103163
{
104164
$promise1 = async(function () {

tests/AwaitTest.php

+139
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
use React;
66
use React\EventLoop\Loop;
7+
use React\Promise\Deferred;
78
use React\Promise\Promise;
9+
use function React\Async\async;
810

911
class AwaitTest extends TestCase
1012
{
@@ -22,6 +24,79 @@ public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(calla
2224
$await($promise);
2325
}
2426

27+
/**
28+
* @dataProvider provideAwaiters
29+
*/
30+
public function testAwaitThrowsExceptionWithoutRunningLoop(callable $await)
31+
{
32+
$now = true;
33+
Loop::futureTick(function () use (&$now) {
34+
$now = false;
35+
});
36+
37+
$promise = new Promise(function () {
38+
throw new \Exception('test');
39+
});
40+
41+
try {
42+
$await($promise);
43+
} catch (\Exception $e) {
44+
$this->assertTrue($now);
45+
}
46+
}
47+
48+
/**
49+
* @dataProvider provideAwaiters
50+
*/
51+
public function testAwaitThrowsExceptionImmediatelyWhenPromiseIsRejected(callable $await)
52+
{
53+
$deferred = new Deferred();
54+
55+
$ticks = 0;
56+
Loop::futureTick(function () use (&$ticks) {
57+
++$ticks;
58+
Loop::futureTick(function () use (&$ticks) {
59+
++$ticks;
60+
});
61+
});
62+
63+
Loop::futureTick(fn() => $deferred->reject(new \RuntimeException()));
64+
65+
try {
66+
$await($deferred->promise());
67+
} catch (\RuntimeException $e) {
68+
$this->assertEquals(1, $ticks);
69+
}
70+
}
71+
72+
/**
73+
* @dataProvider provideAwaiters
74+
*/
75+
public function testAwaitAsyncThrowsExceptionImmediatelyWhenPromiseIsRejected(callable $await)
76+
{
77+
$deferred = new Deferred();
78+
79+
$ticks = 0;
80+
Loop::futureTick(function () use (&$ticks) {
81+
++$ticks;
82+
Loop::futureTick(function () use (&$ticks) {
83+
++$ticks;
84+
});
85+
});
86+
87+
Loop::futureTick(fn() => $deferred->reject(new \RuntimeException()));
88+
89+
$promise = async(function () use ($deferred, $await) {
90+
return $await($deferred->promise());
91+
})();
92+
93+
try {
94+
$await($promise);
95+
} catch (\RuntimeException $e) {
96+
$this->assertEquals(1, $ticks);
97+
}
98+
}
99+
25100
/**
26101
* @dataProvider provideAwaiters
27102
*/
@@ -91,6 +166,70 @@ public function testAwaitReturnsValueWhenPromiseIsFullfilled(callable $await)
91166
$this->assertEquals(42, $await($promise));
92167
}
93168

169+
/**
170+
* @dataProvider provideAwaiters
171+
*/
172+
public function testAwaitReturnsValueImmediatelyWithoutRunningLoop(callable $await)
173+
{
174+
$now = true;
175+
Loop::futureTick(function () use (&$now) {
176+
$now = false;
177+
});
178+
179+
$promise = new Promise(function ($resolve) {
180+
$resolve(42);
181+
});
182+
183+
$this->assertEquals(42, $await($promise));
184+
$this->assertTrue($now);
185+
}
186+
187+
/**
188+
* @dataProvider provideAwaiters
189+
*/
190+
public function testAwaitReturnsValueImmediatelyWhenPromiseIsFulfilled(callable $await)
191+
{
192+
$deferred = new Deferred();
193+
194+
$ticks = 0;
195+
Loop::futureTick(function () use (&$ticks) {
196+
++$ticks;
197+
Loop::futureTick(function () use (&$ticks) {
198+
++$ticks;
199+
});
200+
});
201+
202+
Loop::futureTick(fn() => $deferred->resolve(42));
203+
204+
$this->assertEquals(42, $await($deferred->promise()));
205+
$this->assertEquals(1, $ticks);
206+
}
207+
208+
/**
209+
* @dataProvider provideAwaiters
210+
*/
211+
public function testAwaitAsyncReturnsValueImmediatelyWhenPromiseIsFulfilled(callable $await)
212+
{
213+
$deferred = new Deferred();
214+
215+
$ticks = 0;
216+
Loop::futureTick(function () use (&$ticks) {
217+
++$ticks;
218+
Loop::futureTick(function () use (&$ticks) {
219+
++$ticks;
220+
});
221+
});
222+
223+
Loop::futureTick(fn() => $deferred->resolve(42));
224+
225+
$promise = async(function () use ($deferred, $await) {
226+
return $await($deferred->promise());
227+
})();
228+
229+
$this->assertEquals(42, $await($promise));
230+
$this->assertEquals(1, $ticks);
231+
}
232+
94233
/**
95234
* @dataProvider provideAwaiters
96235
*/

0 commit comments

Comments
 (0)