Skip to content

Commit

Permalink
Merge pull request #32 from clue-labs/the-future-is-now
Browse files Browse the repository at this point in the history
Improve `await()` to avoid unneeded `futureTick()` calls
  • Loading branch information
WyriHaximus authored Feb 18, 2022
2 parents 1986075 + 4d8331f commit 4cadacc
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 8 deletions.
29 changes: 21 additions & 8 deletions src/SimpleFiber.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down
60 changes: 60 additions & 0 deletions tests/AsyncTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 () {
Expand All @@ -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 () {
Expand Down
139 changes: 139 additions & 0 deletions tests/AwaitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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
*/
Expand Down Expand Up @@ -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
*/
Expand Down

0 comments on commit 4cadacc

Please sign in to comment.