Skip to content

Commit 5f38c49

Browse files
clueWyriHaximus
authored andcommitted
Add Fiber-based async() function
1 parent 543a11d commit 5f38c49

File tree

3 files changed

+163
-18
lines changed

3 files changed

+163
-18
lines changed

src/functions.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,31 @@
1010
use function React\Promise\reject;
1111
use function React\Promise\resolve;
1212

13+
/**
14+
* Execute an async Fiber-based function to "await" promises.
15+
*
16+
* @param callable(mixed ...$args):mixed $function
17+
* @param mixed ...$args Optional list of additional arguments that will be passed to the given `$function` as is
18+
* @return PromiseInterface<mixed>
19+
* @since 4.0.0
20+
* @see coroutine()
21+
*/
22+
function async(callable $coroutine, mixed ...$args): PromiseInterface
23+
{
24+
return new Promise(function (callable $resolve, callable $reject) use ($coroutine, $args): void {
25+
$fiber = new \Fiber(function () use ($resolve, $reject, $coroutine, $args): void {
26+
try {
27+
$resolve($coroutine(...$args));
28+
} catch (\Throwable $exception) {
29+
$reject($exception);
30+
}
31+
});
32+
33+
Loop::futureTick(static fn() => $fiber->start());
34+
});
35+
}
36+
37+
1338
/**
1439
* Block waiting for the given `$promise` to be fulfilled.
1540
*

tests/AsyncTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace React\Tests\Async;
4+
5+
use React;
6+
use React\EventLoop\Loop;
7+
use React\Promise\Promise;
8+
use function React\Async\async;
9+
use function React\Async\await;
10+
use function React\Promise\all;
11+
12+
class AsyncTest extends TestCase
13+
{
14+
public function testAsyncReturnsPendingPromise()
15+
{
16+
$promise = async(function () {
17+
return 42;
18+
});
19+
20+
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
21+
}
22+
23+
public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturns()
24+
{
25+
$promise = async(function () {
26+
return 42;
27+
});
28+
29+
$value = await($promise);
30+
31+
$this->assertEquals(42, $value);
32+
}
33+
34+
public function testAsyncReturnsPromiseThatRejectsWithExceptionWhenCallbackThrows()
35+
{
36+
$promise = async(function () {
37+
throw new \RuntimeException('Foo', 42);
38+
});
39+
40+
$this->expectException(\RuntimeException::class);
41+
$this->expectExceptionMessage('Foo');
42+
$this->expectExceptionCode(42);
43+
await($promise);
44+
}
45+
46+
public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsAfterAwaitingPromise()
47+
{
48+
$promise = async(function () {
49+
$promise = new Promise(function ($resolve) {
50+
Loop::addTimer(0.001, fn () => $resolve(42));
51+
});
52+
53+
return await($promise);
54+
});
55+
56+
$value = await($promise);
57+
58+
$this->assertEquals(42, $value);
59+
}
60+
61+
public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsAfterAwaitingTwoConcurrentPromises()
62+
{
63+
$promise1 = async(function () {
64+
$promise = new Promise(function ($resolve) {
65+
Loop::addTimer(0.11, fn () => $resolve(21));
66+
});
67+
68+
return await($promise);
69+
});
70+
71+
$promise2 = async(function () {
72+
$promise = new Promise(function ($resolve) {
73+
Loop::addTimer(0.11, fn () => $resolve(42));
74+
});
75+
76+
return await($promise);
77+
});
78+
79+
$time = microtime(true);
80+
$values = await(all([$promise1, $promise2]));
81+
$time = microtime(true) - $time;
82+
83+
$this->assertEquals([21, 42], $values);
84+
$this->assertGreaterThan(0.1, $time);
85+
$this->assertLessThan(0.12, $time);
86+
}
87+
}

tests/AwaitTest.php

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@
88

99
class AwaitTest extends TestCase
1010
{
11-
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException()
11+
/**
12+
* @dataProvider provideAwaiters
13+
*/
14+
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(callable $await)
1215
{
1316
$promise = new Promise(function () {
1417
throw new \Exception('test');
1518
});
1619

1720
$this->expectException(\Exception::class);
1821
$this->expectExceptionMessage('test');
19-
React\Async\await($promise);
22+
$await($promise);
2023
}
2124

22-
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse()
25+
/**
26+
* @dataProvider provideAwaiters
27+
*/
28+
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse(callable $await)
2329
{
2430
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
2531
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
@@ -31,10 +37,13 @@ public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWith
3137

3238
$this->expectException(\UnexpectedValueException::class);
3339
$this->expectExceptionMessage('Promise rejected with unexpected value of type bool');
34-
React\Async\await($promise);
40+
$await($promise);
3541
}
3642

37-
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull()
43+
/**
44+
* @dataProvider provideAwaiters
45+
*/
46+
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull(callable $await)
3847
{
3948
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
4049
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
@@ -46,10 +55,13 @@ public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWith
4655

4756
$this->expectException(\UnexpectedValueException::class);
4857
$this->expectExceptionMessage('Promise rejected with unexpected value of type NULL');
49-
React\Async\await($promise);
58+
$await($promise);
5059
}
5160

52-
public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
61+
/**
62+
* @dataProvider provideAwaiters
63+
*/
64+
public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError(callable $await)
5365
{
5466
$promise = new Promise(function ($_, $reject) {
5567
throw new \Error('Test', 42);
@@ -58,19 +70,25 @@ public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
5870
$this->expectException(\Error::class);
5971
$this->expectExceptionMessage('Test');
6072
$this->expectExceptionCode(42);
61-
React\Async\await($promise);
73+
$await($promise);
6274
}
6375

64-
public function testAwaitReturnsValueWhenPromiseIsFullfilled()
76+
/**
77+
* @dataProvider provideAwaiters
78+
*/
79+
public function testAwaitReturnsValueWhenPromiseIsFullfilled(callable $await)
6580
{
6681
$promise = new Promise(function ($resolve) {
6782
$resolve(42);
6883
});
6984

70-
$this->assertEquals(42, React\Async\await($promise));
85+
$this->assertEquals(42, $await($promise));
7186
}
7287

73-
public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop()
88+
/**
89+
* @dataProvider provideAwaiters
90+
*/
91+
public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop(callable $await)
7492
{
7593
$this->markTestIncomplete();
7694

@@ -83,10 +101,13 @@ public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerSto
83101
Loop::stop();
84102
});
85103

86-
$this->assertEquals(2, React\Async\await($promise));
104+
$this->assertEquals(2, $await($promise));
87105
}
88106

89-
public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
107+
/**
108+
* @dataProvider provideAwaiters
109+
*/
110+
public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise(callable $await)
90111
{
91112
if (class_exists('React\Promise\When')) {
92113
$this->markTestSkipped('Not supported on legacy Promise v1 API');
@@ -97,13 +118,16 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
97118
$promise = new Promise(function ($resolve) {
98119
$resolve(42);
99120
});
100-
React\Async\await($promise);
121+
$await($promise);
101122
unset($promise);
102123

103124
$this->assertEquals(0, gc_collect_cycles());
104125
}
105126

106-
public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
127+
/**
128+
* @dataProvider provideAwaiters
129+
*/
130+
public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise(callable $await)
107131
{
108132
if (class_exists('React\Promise\When')) {
109133
$this->markTestSkipped('Not supported on legacy Promise v1 API');
@@ -115,7 +139,7 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
115139
throw new \RuntimeException();
116140
});
117141
try {
118-
React\Async\await($promise);
142+
$await($promise);
119143
} catch (\Exception $e) {
120144
// no-op
121145
}
@@ -124,7 +148,10 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
124148
$this->assertEquals(0, gc_collect_cycles());
125149
}
126150

127-
public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue()
151+
/**
152+
* @dataProvider provideAwaiters
153+
*/
154+
public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue(callable $await)
128155
{
129156
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
130157
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
@@ -140,12 +167,18 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWi
140167
$reject(null);
141168
});
142169
try {
143-
React\Async\await($promise);
170+
$await($promise);
144171
} catch (\Exception $e) {
145172
// no-op
146173
}
147174
unset($promise, $e);
148175

149176
$this->assertEquals(0, gc_collect_cycles());
150177
}
178+
179+
public function provideAwaiters(): iterable
180+
{
181+
yield 'await' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await($promise)];
182+
yield 'async' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await(React\Async\async(static fn(): mixed => $promise))];
183+
}
151184
}

0 commit comments

Comments
 (0)