Skip to content

Commit 4cadacc

Browse files
authored
Merge pull request #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

Lines changed: 21 additions & 8 deletions
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

Lines changed: 60 additions & 0 deletions
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

Lines changed: 139 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy