From fcfa37a27be0c66f11c0ecbf6c2d1e18326497cc Mon Sep 17 00:00:00 2001 From: Simon Frings Date: Thu, 24 Feb 2022 11:05:01 +0100 Subject: [PATCH] Only stop loop if a pending promise resolves/rejects --- src/functions.php | 16 +++-- tests/AwaitTest.php | 143 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 4 deletions(-) diff --git a/src/functions.php b/src/functions.php index 0f2ef7e..e4d3fec 100644 --- a/src/functions.php +++ b/src/functions.php @@ -54,18 +54,25 @@ function await(PromiseInterface $promise) $resolved = null; $exception = null; $rejected = false; + $loopStarted = false; $promise->then( - function ($c) use (&$resolved, &$wait) { + function ($c) use (&$resolved, &$wait, &$loopStarted) { $resolved = $c; $wait = false; - Loop::stop(); + + if ($loopStarted) { + Loop::stop(); + } }, - function ($error) use (&$exception, &$rejected, &$wait) { + function ($error) use (&$exception, &$rejected, &$wait, &$loopStarted) { $exception = $error; $rejected = true; $wait = false; - Loop::stop(); + + if ($loopStarted) { + Loop::stop(); + } } ); @@ -74,6 +81,7 @@ function ($error) use (&$exception, &$rejected, &$wait) { $promise = null; while ($wait) { + $loopStarted = true; Loop::run(); } diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 1acc7e3..9e03be6 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -82,6 +82,149 @@ public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerSto $this->assertEquals(2, React\Async\await($promise)); } + public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutRunningLoop() + { + $now = true; + + Loop::futureTick(function () use (&$now) { + $now = false; + }); + + $promise = new Promise(function ($resolve) { + $resolve(42); + }); + + React\Async\await($promise); + $this->assertTrue($now); + } + + public function testAwaitWithAlreadyFulfilledPromiseWillReturnWithoutStoppingLoop() + { + $ticks = 0; + + $promise = new Promise(function ($resolve) { + $resolve(42); + }); + + // Loop will execute this tick first + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + // Loop will execute this tick third + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + // Loop will execute this tick second + Loop::futureTick(function () use (&$promise){ + // await won't stop the loop if promise already resolved -> third tick will trigger + React\Async\await($promise); + }); + + Loop::run(); + + $this->assertEquals(2, $ticks); + } + + public function testAwaitWithPendingPromiseThatWillResolveWillStopLoopBeforeLastTimerFinishes() + { + $promise = new Promise(function ($resolve) { + Loop::addTimer(0.02, function () use ($resolve) { + $resolve(2); + }); + }); + + $ticks = 0; + + // Loop will execute this tick first + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + // This timer will never finish because Loop gets stopped by await + // Loop needs to be manually started again to finish this timer + Loop::addTimer(0.04, function () use (&$ticks) { + ++$ticks; + }); + }); + + // await stops the loop when promise resolves after 0.02s + Loop::futureTick(function () use (&$promise){ + React\Async\await($promise); + }); + + Loop::run(); + + // This bahvior exists in v2 & v3 of async, we recommend to use fibers in v4 (PHP>=8.1) + $this->assertEquals(1, $ticks); + } + + public function testAwaitWithAlreadyRejectedPromiseWillReturnWithoutStoppingLoop() + { + $ticks = 0; + + $promise = new Promise(function ($_, $reject) { + throw new \Exception(); + }); + + // Loop will execute this tick first + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + // Loop will execute this tick third + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + // Loop will execute this tick second + Loop::futureTick(function () use (&$promise){ + try { + // await won't stop the loop if promise already rejected -> third tick will trigger + React\Async\await($promise); + } catch (\Exception $e) { + // no-op + } + }); + + Loop::run(); + + $this->assertEquals(2, $ticks); + } + + public function testAwaitWithPendingPromiseThatWillRejectWillStopLoopBeforeLastTimerFinishes() + { + $promise = new Promise(function ($_, $reject) { + Loop::addTimer(0.02, function () use (&$reject) { + $reject(new \Exception()); + }); + }); + + $ticks = 0; + + // Loop will execute this tick first + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + // This timer will never finish because Loop gets stopped by await + // Loop needs to be manually started again to finish this timer + Loop::addTimer(0.04, function () use (&$ticks) { + ++$ticks; + }); + }); + + // Loop will execute this tick second + // await stops the loop when promise rejects after 0.02s + Loop::futureTick(function () use (&$promise){ + try { + React\Async\await($promise); + } catch (\Exception $e) { + // no-op + } + }); + + Loop::run(); + + // This bahvior exists in v2 & v3 of async, we recommend to use fibers in v4 (PHP>=8.1) + $this->assertEquals(1, $ticks); + } + public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise() { if (class_exists('React\Promise\When') && PHP_VERSION_ID >= 50400) {