Skip to content

Commit

Permalink
Add Fiber-based async() function
Browse files Browse the repository at this point in the history
  • Loading branch information
clue authored and WyriHaximus committed Nov 21, 2021
1 parent 4ca487c commit 11642b4
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 18 deletions.
23 changes: 23 additions & 0 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,29 @@
use function React\Promise\reject;
use function React\Promise\resolve;

/**
*
* @template T
* @param callable(...$args):T $coroutine
* @param mixed ...$args
* @return PromiseInterface<T>
*/
function async(callable $coroutine, ...$args): PromiseInterface
{
return new Promise(function (callable $resolve, callable $reject) use ($coroutine, $args): void {
$fiber = new \Fiber(function () use ($resolve, $reject, $coroutine, $args): void {
try {
$resolve($coroutine(...$args));
} catch (\Throwable $exception) {
$reject($exception);
}
});

Loop::futureTick(static fn() => $fiber->start());
});
}


/**
* Block waiting for the given `$promise` to be fulfilled.
*
Expand Down
69 changes: 51 additions & 18 deletions tests/AwaitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@

class AwaitTest extends TestCase
{
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitThrowsExceptionWhenPromiseIsRejectedWithException(callable $await)
{
$promise = new Promise(function () {
throw new \Exception('test');
});

$this->expectException(\Exception::class);
$this->expectExceptionMessage('test');
React\Async\await($promise);
$await($promise);
}

public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithFalse(callable $await)
{
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
Expand All @@ -31,10 +37,13 @@ public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWith

$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessage('Promise rejected with unexpected value of type bool');
React\Async\await($promise);
$await($promise);
}

public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWithNull(callable $await)
{
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
Expand All @@ -46,10 +55,13 @@ public function testAwaitThrowsUnexpectedValueExceptionWhenPromiseIsRejectedWith

$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessage('Promise rejected with unexpected value of type NULL');
React\Async\await($promise);
$await($promise);
}

public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError(callable $await)
{
$promise = new Promise(function ($_, $reject) {
throw new \Error('Test', 42);
Expand All @@ -58,19 +70,25 @@ public function testAwaitThrowsErrorWhenPromiseIsRejectedWithError()
$this->expectException(\Error::class);
$this->expectExceptionMessage('Test');
$this->expectExceptionCode(42);
React\Async\await($promise);
$await($promise);
}

public function testAwaitReturnsValueWhenPromiseIsFullfilled()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitReturnsValueWhenPromiseIsFullfilled(callable $await)
{
$promise = new Promise(function ($resolve) {
$resolve(42);
});

$this->assertEquals(42, React\Async\await($promise));
$this->assertEquals(42, $await($promise));
}

public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerStopsLoop(callable $await)
{
$this->markTestIncomplete();

Expand All @@ -83,10 +101,13 @@ public function testAwaitReturnsValueWhenPromiseIsFulfilledEvenWhenOtherTimerSto
Loop::stop();
});

$this->assertEquals(2, React\Async\await($promise));
$this->assertEquals(2, $await($promise));
}

public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise(callable $await)
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
Expand All @@ -97,13 +118,16 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForResolvedPromise()
$promise = new Promise(function ($resolve) {
$resolve(42);
});
React\Async\await($promise);
$await($promise);
unset($promise);

$this->assertEquals(0, gc_collect_cycles());
}

public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise(callable $await)
{
if (class_exists('React\Promise\When')) {
$this->markTestSkipped('Not supported on legacy Promise v1 API');
Expand All @@ -115,7 +139,7 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
throw new \RuntimeException();
});
try {
React\Async\await($promise);
$await($promise);
} catch (\Exception $e) {
// no-op
}
Expand All @@ -124,7 +148,10 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForRejectedPromise()
$this->assertEquals(0, gc_collect_cycles());
}

public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue()
/**
* @dataProvider provideAwaiters
*/
public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWithNullValue(callable $await)
{
if (!interface_exists('React\Promise\CancellablePromiseInterface')) {
$this->markTestSkipped('Promises must be rejected with a \Throwable instance since Promise v3');
Expand All @@ -140,12 +167,18 @@ public function testAwaitShouldNotCreateAnyGarbageReferencesForPromiseRejectedWi
$reject(null);
});
try {
React\Async\await($promise);
$await($promise);
} catch (\Exception $e) {
// no-op
}
unset($promise, $e);

$this->assertEquals(0, gc_collect_cycles());
}

public function provideAwaiters(): iterable
{
yield 'await' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await($promise)];
yield 'async' => [static fn (React\Promise\PromiseInterface $promise): mixed => React\Async\await(React\Async\async(static fn(): mixed => $promise))];
}
}

0 comments on commit 11642b4

Please sign in to comment.