diff --git a/src/functions.php b/src/functions.php index ad91688..478623d 100644 --- a/src/functions.php +++ b/src/functions.php @@ -53,18 +53,47 @@ 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'); + $r->setAccessible(true); + $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(); @@ -75,25 +104,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. * diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 95a8b5f..e1274dd 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -122,6 +122,38 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise() $this->assertEquals(0, gc_collect_cycles()); } + public function testAlreadyFulfilledPromiseShouldShortCircuitAndNotRunLoop() + { + for ($i = 0; $i < 6; $i++) { + $this->assertSame($i, React\Async\await(React\Promise\resolve($i))); + } + } + + public function testPendingPromiseShouldNotShortCircuitAndRunLoop() + { + Loop::futureTick($this->expectCallableOnce()); + + $this->assertSame(1, React\Async\await(new Promise(static function (callable $resolve) { + Loop::futureTick(static function () use ($resolve) { + $resolve(1); + }); + }))); + } + + public function testPendingPromiseShouldNotShortCircuitAndRunLoopAndThrowOnRejection() + { + Loop::futureTick($this->expectCallableOnce()); + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('test'); + + $this->assertSame(1, React\Async\await(new Promise(static function (callable $resolve, callable $reject) { + Loop::futureTick(static function () use ($reject) { + $reject(new \Exception('test')); + }); + }))); + } + public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue() { if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: