Skip to content

Commit 4d9d827

Browse files
committed
Fast forward resolved/rejected promises with await
This makes `await`ing an already resolved promise significantly faster. Ported from: #18
1 parent c989ee1 commit 4d9d827

File tree

2 files changed

+77
-16
lines changed

2 files changed

+77
-16
lines changed

src/functions.php

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,18 +53,47 @@
5353
function await(PromiseInterface $promise)
5454
{
5555
$wait = true;
56-
$resolved = null;
57-
$exception = null;
56+
$resolved = false;
5857
$rejected = false;
58+
$resolvedValue = null;
59+
$rejectedThrowable = null;
5960

6061
$promise->then(
61-
function ($c) use (&$resolved, &$wait) {
62-
$resolved = $c;
62+
function ($c) use (&$resolved, &$resolvedValue, &$wait) {
63+
$resolvedValue = $c;
64+
$resolved = true;
6365
$wait = false;
6466
Loop::stop();
6567
},
66-
function ($error) use (&$exception, &$rejected, &$wait) {
67-
$exception = $error;
68+
function ($error) use (&$rejected, &$rejectedThrowable, &$wait) {
69+
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
70+
if (!$error instanceof \Exception && !$error instanceof \Throwable) {
71+
$error = new \UnexpectedValueException(
72+
'Promise rejected with unexpected value of type ' . (is_object($error) ? get_class($error) : gettype($error))
73+
);
74+
75+
// avoid garbage references by replacing all closures in call stack.
76+
// what a lovely piece of code!
77+
$r = new \ReflectionProperty('Exception', 'trace');
78+
$r->setAccessible(true);
79+
$trace = $r->getValue($error);
80+
81+
// Exception trace arguments only available when zend.exception_ignore_args is not set
82+
// @codeCoverageIgnoreStart
83+
foreach ($trace as $ti => $one) {
84+
if (isset($one['args'])) {
85+
foreach ($one['args'] as $ai => $arg) {
86+
if ($arg instanceof \Closure) {
87+
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
88+
}
89+
}
90+
}
91+
}
92+
// @codeCoverageIgnoreEnd
93+
$r->setValue($error, $trace);
94+
}
95+
96+
$rejectedThrowable = $error;
6897
$rejected = true;
6998
$wait = false;
7099
Loop::stop();
@@ -75,25 +104,25 @@ function ($error) use (&$exception, &$rejected, &$wait) {
75104
// argument does not show up in the stack trace in PHP 7+ only.
76105
$promise = null;
77106

107+
if ($rejected) {
108+
throw $rejectedThrowable;
109+
}
110+
111+
if ($resolved) {
112+
return $resolvedValue;
113+
}
114+
78115
while ($wait) {
79116
Loop::run();
80117
}
81118

82119
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;
120+
throw $rejectedThrowable;
91121
}
92122

93-
return $resolved;
123+
return $resolvedValue;
94124
}
95125

96-
97126
/**
98127
* Execute a Generator-based coroutine to "await" promises.
99128
*

tests/AwaitTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,38 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
122122
$this->assertEquals(0, gc_collect_cycles());
123123
}
124124

125+
public function testAlreadyFulfilledPromiseShouldShortCircuitAndNotRunLoop()
126+
{
127+
for ($i = 0; $i < 6; $i++) {
128+
$this->assertSame($i, React\Async\await(React\Promise\resolve($i)));
129+
}
130+
}
131+
132+
public function testPendingPromiseShouldNotShortCircuitAndRunLoop()
133+
{
134+
Loop::futureTick($this->expectCallableOnce());
135+
136+
$this->assertSame(1, React\Async\await(new Promise(static function (callable $resolve) {
137+
Loop::futureTick(static function () use ($resolve) {
138+
$resolve(1);
139+
});
140+
})));
141+
}
142+
143+
public function testPendingPromiseShouldNotShortCircuitAndRunLoopAndThrowOnRejection()
144+
{
145+
Loop::futureTick($this->expectCallableOnce());
146+
147+
$this->expectException(\Exception::class);
148+
$this->expectExceptionMessage('test');
149+
150+
$this->assertSame(1, React\Async\await(new Promise(static function (callable $resolve, callable $reject) {
151+
Loop::futureTick(static function () use ($reject) {
152+
$reject(new \Exception('test'));
153+
});
154+
})));
155+
}
156+
125157
public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue()
126158
{
127159
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {

0 commit comments

Comments
 (0)