diff --git a/src/SimpleFiber.php b/src/SimpleFiber.php index f45e628..acf3fad 100644 --- a/src/SimpleFiber.php +++ b/src/SimpleFiber.php @@ -10,6 +10,7 @@ final class SimpleFiber implements FiberInterface { private static ?\Fiber $scheduler = null; + private static ?\Closure $suspend = null; private ?\Fiber $fiber = null; public function __construct() @@ -19,22 +20,34 @@ public function __construct() public function resume(mixed $value): void { - if ($this->fiber === null) { - Loop::futureTick(static fn() => \Fiber::suspend(static fn() => $value)); - return; + if ($this->fiber !== null) { + $this->fiber->resume($value); + } else { + self::$suspend = static fn() => $value; } - Loop::futureTick(fn() => $this->fiber->resume($value)); + if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) { + $suspend = self::$suspend; + self::$suspend = null; + + \Fiber::suspend($suspend); + } } public function throw(\Throwable $throwable): void { - if ($this->fiber === null) { - Loop::futureTick(static fn() => \Fiber::suspend(static fn() => throw $throwable)); - return; + if ($this->fiber !== null) { + $this->fiber->throw($throwable); + } else { + self::$suspend = static fn() => throw $throwable; } - Loop::futureTick(fn() => $this->fiber->throw($throwable)); + if (self::$suspend !== null && \Fiber::getCurrent() === self::$scheduler) { + $suspend = self::$suspend; + self::$suspend = null; + + \Fiber::suspend($suspend); + } } public function suspend(): mixed diff --git a/tests/AsyncTest.php b/tests/AsyncTest.php index dccbe54..a4287fd 100644 --- a/tests/AsyncTest.php +++ b/tests/AsyncTest.php @@ -4,6 +4,7 @@ use React; use React\EventLoop\Loop; +use React\Promise\Deferred; use React\Promise\Promise; use function React\Async\async; use function React\Async\await; @@ -84,6 +85,49 @@ public function testAsyncReturnsPendingPromiseWhenCallbackReturnsPendingPromise( $promise->then($this->expectCallableNever(), $this->expectCallableNever()); } + public function testAsyncWithAwaitReturnsReturnsPromiseFulfilledWithValueImmediatelyWhenPromiseIsFulfilled() + { + $deferred = new Deferred(); + + $promise = async(function () use ($deferred) { + return await($deferred->promise()); + })(); + + $return = null; + $promise->then(function ($value) use (&$return) { + $return = $value; + }); + + $this->assertNull($return); + + $deferred->resolve(42); + + $this->assertEquals(42, $return); + } + + public function testAsyncWithAwaitReturnsPromiseRejectedWithExceptionImmediatelyWhenPromiseIsRejected() + { + $deferred = new Deferred(); + + $promise = async(function () use ($deferred) { + return await($deferred->promise()); + })(); + + $exception = null; + $promise->then(null, function ($reason) use (&$exception) { + $exception = $reason; + }); + + $this->assertNull($exception); + + $deferred->reject(new \RuntimeException('Test', 42)); + + $this->assertInstanceof(\RuntimeException::class, $exception); + assert($exception instanceof \RuntimeException); + $this->assertEquals('Test', $exception->getMessage()); + $this->assertEquals(42, $exception->getCode()); + } + public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsAfterAwaitingPromise() { $promise = async(function () { @@ -99,6 +143,22 @@ public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsA $this->assertEquals(42, $value); } + public function testAsyncReturnsPromiseThatRejectsWithExceptionWhenCallbackThrowsAfterAwaitingPromise() + { + $promise = async(function () { + $promise = new Promise(function ($_, $reject) { + Loop::addTimer(0.001, fn () => $reject(new \RuntimeException('Foo', 42))); + }); + + return await($promise); + })(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Foo'); + $this->expectExceptionCode(42); + await($promise); + } + public function testAsyncReturnsPromiseThatFulfillsWithValueWhenCallbackReturnsAfterAwaitingTwoConcurrentPromises() { $promise1 = async(function () { diff --git a/tests/AwaitTest.php b/tests/AwaitTest.php index 2bf1314..2dd8159 100644 --- a/tests/AwaitTest.php +++ b/tests/AwaitTest.php @@ -4,7 +4,9 @@ use React; use React\EventLoop\Loop; +use React\Promise\Deferred; use React\Promise\Promise; +use function React\Async\async; class AwaitTest extends TestCase { @@ -22,6 +24,79 @@ public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(calla $await($promise); } + /** + * @dataProvider provideAwaiters + */ + public function testAwaitThrowsExceptionWithoutRunningLoop(callable $await) + { + $now = true; + Loop::futureTick(function () use (&$now) { + $now = false; + }); + + $promise = new Promise(function () { + throw new \Exception('test'); + }); + + try { + $await($promise); + } catch (\Exception $e) { + $this->assertTrue($now); + } + } + + /** + * @dataProvider provideAwaiters + */ + public function testAwaitThrowsExceptionImmediatelyWhenPromiseIsRejected(callable $await) + { + $deferred = new Deferred(); + + $ticks = 0; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + Loop::futureTick(fn() => $deferred->reject(new \RuntimeException())); + + try { + $await($deferred->promise()); + } catch (\RuntimeException $e) { + $this->assertEquals(1, $ticks); + } + } + + /** + * @dataProvider provideAwaiters + */ + public function testAwaitAsyncThrowsExceptionImmediatelyWhenPromiseIsRejected(callable $await) + { + $deferred = new Deferred(); + + $ticks = 0; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + Loop::futureTick(fn() => $deferred->reject(new \RuntimeException())); + + $promise = async(function () use ($deferred, $await) { + return $await($deferred->promise()); + })(); + + try { + $await($promise); + } catch (\RuntimeException $e) { + $this->assertEquals(1, $ticks); + } + } + /** * @dataProvider provideAwaiters */ @@ -91,6 +166,70 @@ public function testAwaitReturnsValueWhenPromiseIsFullfilled(callable $await) $this->assertEquals(42, $await($promise)); } + /** + * @dataProvider provideAwaiters + */ + public function testAwaitReturnsValueImmediatelyWithoutRunningLoop(callable $await) + { + $now = true; + Loop::futureTick(function () use (&$now) { + $now = false; + }); + + $promise = new Promise(function ($resolve) { + $resolve(42); + }); + + $this->assertEquals(42, $await($promise)); + $this->assertTrue($now); + } + + /** + * @dataProvider provideAwaiters + */ + public function testAwaitReturnsValueImmediatelyWhenPromiseIsFulfilled(callable $await) + { + $deferred = new Deferred(); + + $ticks = 0; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + Loop::futureTick(fn() => $deferred->resolve(42)); + + $this->assertEquals(42, $await($deferred->promise())); + $this->assertEquals(1, $ticks); + } + + /** + * @dataProvider provideAwaiters + */ + public function testAwaitAsyncReturnsValueImmediatelyWhenPromiseIsFulfilled(callable $await) + { + $deferred = new Deferred(); + + $ticks = 0; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + Loop::futureTick(function () use (&$ticks) { + ++$ticks; + }); + }); + + Loop::futureTick(fn() => $deferred->resolve(42)); + + $promise = async(function () use ($deferred, $await) { + return $await($deferred->promise()); + })(); + + $this->assertEquals(42, $await($promise)); + $this->assertEquals(1, $ticks); + } + /** * @dataProvider provideAwaiters */