Skip to content

Commit a2deae8

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 ab03f4d commit a2deae8

File tree

2 files changed

+76
-15
lines changed

2 files changed

+76
-15
lines changed

src/functions.php

+45-15
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,47 @@
5151
function await(PromiseInterface $promise)
5252
{
5353
$wait = true;
54-
$resolved = null;
55-
$exception = null;
54+
$resolved = false;
5655
$rejected = false;
56+
$resolvedValue = null;
57+
$rejectedThrowable = null;
5758

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

105+
if ($rejected) {
106+
throw $rejectedThrowable;
107+
}
108+
109+
if ($resolved) {
110+
return $resolvedValue;
111+
}
112+
76113
while ($wait) {
77114
Loop::run();
78115
}
79116

80117
if ($rejected) {
81-
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
82-
if (!$exception instanceof \Exception && !$exception instanceof \Throwable) {
83-
$exception = new \UnexpectedValueException(
84-
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
85-
);
86-
}
87-
88-
throw $exception;
118+
throw $rejectedThrowable;
89119
}
90120

91-
return $resolved;
121+
return $resolvedValue;
92122
}
93123

94124
/**

tests/AwaitTest.php

+31
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,37 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWi
145145
$this->assertEquals(0, gc_collect_cycles());
146146
}
147147

148+
public function testAlreadyFulfilledPromiseShouldShortCircuitAndNotRunLoop()
149+
{
150+
for ($i = 0; $i < 6; $i++) {
151+
$this->assertSame($i, React\Async\await(React\Promise\resolve($i)));
152+
}
153+
}
154+
155+
public function testPendingPromiseShouldNotShortCircuitAndRunLoop()
156+
{
157+
Loop::futureTick($this->expectCallableOnce());
158+
159+
$this->assertSame(1, React\Async\await(new Promise(static function (callable $resolve) {
160+
Loop::futureTick(static function () use ($resolve) {
161+
$resolve(1);
162+
});
163+
})));
164+
}
165+
166+
public function testPendingPromiseShouldNotShortCircuitAndRunLoopAndThrowOnRejection()
167+
{
168+
Loop::futureTick($this->expectCallableOnce());
169+
170+
$this->setExpectedException('Exception', 'test');
171+
172+
$this->assertSame(1, React\Async\await(new Promise(static function (callable $resolve, callable $reject) {
173+
Loop::futureTick(static function () use ($reject) {
174+
$reject(new \Exception('test'));
175+
});
176+
})));
177+
}
178+
148179
public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null)
149180
{
150181
if (method_exists($this, 'expectException')) {

0 commit comments

Comments
 (0)