Skip to content

Commit

Permalink
Fast forward resolved/rejected promises with await
Browse files Browse the repository at this point in the history
This makes `await`ing an already resolved promise significantly faster.

Ported from: reactphp#18
  • Loading branch information
WyriHaximus committed Feb 11, 2022
1 parent c989ee1 commit 5fc67d7
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 16 deletions.
60 changes: 44 additions & 16 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,46 @@
function await(PromiseInterface $promise)
{
$wait = true;
$resolved = null;
$exception = null;
$resolved = false;
$rejected = false;
$resolvedValue = null;
$rejectedThrowable = null;

$promise->then(
function ($c) use (&$resolved, &$wait) {
$resolved = $c;
function ($c) use (&$resolved, &$resolvedValue, &$wait) {
$resolvedValue = $c;
$resolved = true;
$wait = false;
Loop::stop();
},
function ($error) use (&$exception, &$rejected, &$wait) {
$exception = $error;
function ($error) use (&$rejected, &$rejectedThrowable, &$wait) {
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
if (!$error instanceof \Exception && !$error instanceof \Throwable) {
$error = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($error) ? get_class($error) : gettype($error))
);

// avoid garbage references by replacing all closures in call stack.
// what a lovely piece of code!
$r = new \ReflectionProperty('Exception', 'trace');
$trace = $r->getValue($error);

// Exception trace arguments only available when zend.exception_ignore_args is not set
// @codeCoverageIgnoreStart
foreach ($trace as $ti => $one) {
if (isset($one['args'])) {
foreach ($one['args'] as $ai => $arg) {
if ($arg instanceof \Closure) {
$trace[$ti]['args'][$ai] = 'Object(' . \get_class($arg) . ')';
}
}
}
}
// @codeCoverageIgnoreEnd
$r->setValue($error, $trace);
}

$rejectedThrowable = $error;
$rejected = true;
$wait = false;
Loop::stop();
Expand All @@ -75,25 +103,25 @@ function ($error) use (&$exception, &$rejected, &$wait) {
// argument does not show up in the stack trace in PHP 7+ only.
$promise = null;

if ($rejected) {
throw $rejectedThrowable;
}

if ($resolved) {
return $resolvedValue;
}

while ($wait) {
Loop::run();
}

if ($rejected) {
// promise is rejected with an unexpected value (Promise API v1 or v2 only)
if (!$exception instanceof \Throwable) {
$exception = new \UnexpectedValueException(
'Promise rejected with unexpected value of type ' . (is_object($exception) ? get_class($exception) : gettype($exception))
);
}

throw $exception;
throw $rejectedThrowable;
}

return $resolved;
return $resolvedValue;
}


/**
* Execute a Generator-based coroutine to "await" promises.
*
Expand Down
7 changes: 7 additions & 0 deletions tests/AwaitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,13 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
$this->assertEquals(0, gc_collect_cycles());
}

public function testAlreadyFulfilledPromiseShouldNotSuspendFiber()
{
for ($i = 0; $i < 6; $i++) {
$this->assertSame($i, React\Async\await(React\Promise\resolve($i)));
}
}

public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue()
{
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
Expand Down

0 comments on commit 5fc67d7

Please sign in to comment.