Skip to content

Commit 6f7f05b

Browse files
clueWyriHaximus
authored andcommitted
Add Fiber-based await() function
1 parent 249f9f6 commit 6f7f05b

File tree

5 files changed

+76
-56
lines changed

5 files changed

+76
-56
lines changed

README.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,8 @@ $result = React\Async\await($promise);
6363
```
6464

6565
This function will only return after the given `$promise` has settled, i.e.
66-
either fulfilled or rejected.
67-
68-
While the promise is pending, this function will assume control over the event
69-
loop. Internally, it will `run()` the [default loop](https://github.com/reactphp/event-loop#loop)
70-
until the promise settles and then calls `stop()` to terminate execution of the
71-
loop. This means this function is more suited for short-lived promise executions
72-
when using promise-based APIs is not feasible. For long-running applications,
73-
using promise-based APIs by leveraging chained `then()` calls is usually preferable.
66+
either fulfilled or rejected. While the promise is pending, this function will
67+
suspend the fiber it's called from until the promise is settled.
7468

7569
Once the promise is fulfilled, this function will return whatever the promise
7670
resolved to.

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
"phpunit/phpunit": "^9.3"
3535
},
3636
"autoload": {
37+
"psr-4": {
38+
"React\\Async\\": "src/"
39+
},
3740
"files": [
3841
"src/functions_include.php"
3942
]

src/SimpleFiber.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace React\Async;
4+
5+
use React\EventLoop\Loop;
6+
7+
/**
8+
* @internal
9+
*/
10+
final class SimpleFiber
11+
{
12+
private static ?\Fiber $scheduler = null;
13+
private ?\Fiber $fiber = null;
14+
15+
public function __construct()
16+
{
17+
$this->fiber = \Fiber::getCurrent();
18+
}
19+
20+
public function resume(mixed $value): void
21+
{
22+
if ($this->fiber === null) {
23+
Loop::futureTick(static fn() => \Fiber::suspend(static fn() => $value));
24+
return;
25+
}
26+
27+
Loop::futureTick(fn() => $this->fiber->resume($value));
28+
}
29+
30+
public function throw(mixed $throwable): void
31+
{
32+
if (!$throwable instanceof \Throwable) {
33+
$throwable = new \UnexpectedValueException(
34+
'Promise rejected with unexpected value of type ' . (is_object($throwable) ? get_class($throwable) : gettype($throwable))
35+
);
36+
}
37+
38+
if ($this->fiber === null) {
39+
Loop::futureTick(static fn() => \Fiber::suspend(static fn() => throw $throwable));
40+
return;
41+
}
42+
43+
Loop::futureTick(fn() => $this->fiber->throw($throwable));
44+
}
45+
46+
public function suspend(): mixed
47+
{
48+
if ($this->fiber === null) {
49+
if (self::$scheduler === null || self::$scheduler->isTerminated()) {
50+
self::$scheduler = new \Fiber(static fn() => Loop::run());
51+
// Run event loop to completion on shutdown.
52+
\register_shutdown_function(static function (): void {
53+
if (self::$scheduler->isSuspended()) {
54+
self::$scheduler->resume();
55+
}
56+
});
57+
}
58+
59+
return (self::$scheduler->isStarted() ? self::$scheduler->resume() : self::$scheduler->start())();
60+
}
61+
62+
return \Fiber::suspend();
63+
}
64+
}

src/functions.php

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use React\EventLoop\Loop;
66
use React\Promise\CancellablePromiseInterface;
77
use React\Promise\Deferred;
8+
use React\Promise\Promise;
89
use React\Promise\PromiseInterface;
910
use function React\Promise\reject;
1011
use function React\Promise\resolve;
@@ -52,48 +53,20 @@
5253
*/
5354
function await(PromiseInterface $promise): mixed
5455
{
55-
$wait = true;
56-
$resolved = null;
57-
$exception = null;
58-
$rejected = false;
56+
$fiber = new SimpleFiber();
5957

6058
$promise->then(
61-
function ($c) use (&$resolved, &$wait) {
62-
$resolved = $c;
63-
$wait = false;
64-
Loop::stop();
59+
function (mixed $value) use (&$resolved, $fiber): void {
60+
$fiber->resume($value);
6561
},
66-
function ($error) use (&$exception, &$rejected, &$wait) {
67-
$exception = $error;
68-
$rejected = true;
69-
$wait = false;
70-
Loop::stop();
62+
function (mixed $throwable) use (&$resolved, $fiber): void {
63+
$fiber->throw($throwable);
7164
}
7265
);
7366

74-
// Explicitly overwrite argument with null value. This ensure that this
75-
// argument does not show up in the stack trace in PHP 7+ only.
76-
$promise = null;
77-
78-
while ($wait) {
79-
Loop::run();
80-
}
81-
82-
if ($rejected) {
83-
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
84-
if (!$exception instanceof \Throwable) {
85-
$exception = new \UnexpectedValueException(
86-
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
87-
);
88-
}
89-
90-
throw $exception;
91-
}
92-
93-
return $resolved;
67+
return $fiber->suspend();
9468
}
9569

96-
9770
/**
9871
* Execute a Generator-based coroutine to "await" promises.
9972
*

tests/AwaitTest.php

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,20 +70,6 @@ public function testAwaitReturnsValueWhenPromiseIsFullfilled()
7070
$this->assertEquals(42, React\Async\await($promise));
7171
}
7272

73-
public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop()
74-
{
75-
$promise = new Promise(function ($resolve) {
76-
Loop::addTimer(0.02, function () use ($resolve) {
77-
$resolve(2);
78-
});
79-
});
80-
Loop::addTimer(0.01, function () {
81-
Loop::stop();
82-
});
83-
84-
$this->assertEquals(2, React\Async\await($promise));
85-
}
86-
8773
public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
8874
{
8975
if (class_exists('React\Promise\When')) {

0 commit comments

Comments
 (0)