From 362884022f2a01df543daa7109b01cfa84ac8225 Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 3 Nov 2018 04:10:45 +0800 Subject: [PATCH 1/4] add timeoutWithDefault function --- lib/functions.php | 41 ++++++++++ test/TimeoutWithDefaultTest.php | 129 ++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 test/TimeoutWithDefaultTest.php diff --git a/lib/functions.php b/lib/functions.php index 24cc5eb3..d4cf29b8 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -223,6 +223,47 @@ function timeout($promise, int $timeout): Promise return $deferred->promise(); } + /** + * Creates an artificial timeout for any `Promise`. + * + * If the promise is resolved before the timeout expires, the result is returned + * + * If the timeout expires before the promise is resolved, a default value is returned + * + * @param \Amp\Promise|\React\Promise\PromiseInterface $promise Promise to which the timeout is applied. + * @param int $timeout Timeout in milliseconds. + * @param mixed $default + * + * @return \Amp\Promise + * + * @throws \TypeError If $promise is not an instance of \Amp\Promise or \React\Promise\PromiseInterface. + */ + function timeoutWithDefault($promise, int $timeout, $default = null): Promise + { + if (!$promise instanceof Promise) { + if ($promise instanceof ReactPromise) { + $promise = adapt($promise); + } else { + throw createTypeError([Promise::class, ReactPromise::class], $promise); + } + } + + $promise = timeout($promise, $timeout); + + $deferred = new Deferred(); + $newPromise = $deferred->promise(); + + $promise->onResolve(function ($exception, $value) use ($deferred, $default) { + if ($exception) { + $deferred->resolve($default); + } else { + $deferred->resolve($value); + } + }); + + return $newPromise; + } + /** * Adapts any object with a done(callable $onFulfilled, callable $onRejected) or then(callable $onFulfilled, * callable $onRejected) method to a promise usable by components depending on placeholders implementing diff --git a/test/TimeoutWithDefaultTest.php b/test/TimeoutWithDefaultTest.php new file mode 100644 index 00000000..96e23b7c --- /dev/null +++ b/test/TimeoutWithDefaultTest.php @@ -0,0 +1,129 @@ +assertInstanceOf(Promise::class, $promise); + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + $promise->onResolve($callback); + + $this->assertSame($value, $result); + }); + } + + public function testFailedPromise() + { + Loop::run(function () { + $exception = new \Exception; + $expected = 2; + + $promise = new Failure($exception); + + $promise = Promise\timeoutWithDefault($promise, 100, $expected); + $this->assertInstanceOf(Promise::class, $promise); + + $callback = function ($exception, $value) use (&$actual) { + $actual = $value; + }; + + $promise->onResolve($callback); + + $this->assertEquals($expected, $actual); + }); + } + + /** + * @depends testSuccessfulPromise + */ + public function testFastPending() + { + $value = 1; + + Loop::run(function () use (&$result, $value) { + $promise = new Delayed(50, $value); + + $promise = Promise\timeoutWithDefault($promise, 100); + $this->assertInstanceOf(Promise::class, $promise); + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + $promise->onResolve($callback); + }); + + $this->assertSame($value, $result); + } + + /** + * @depends testSuccessfulPromise + */ + public function testSlowPending() + { + $expected = 2; + + Loop::run(function () use (&$actual, $expected) { + $promise = new Delayed(200); + + $promise = Promise\timeoutWithDefault($promise, 100, $expected); + $this->assertInstanceOf(Promise::class, $promise); + + $callback = function ($exception, $value) use (&$actual) { + $actual = $value; + }; + + $promise->onResolve($callback); + }); + + $this->assertEquals($expected, $actual); + } + + /** + * @depends testSuccessfulPromise + */ + public function testReactPromise() + { + Loop::run(function () { + $value = 1; + + $promise = resolve($value); + + $promise = Promise\timeoutWithDefault($promise, 100, 2); + $this->assertInstanceOf(Promise::class, $promise); + + $callback = function ($exception, $value) use (&$result) { + $result = $value; + }; + + $promise->onResolve($callback); + + $this->assertSame($value, $result); + }); + } + + public function testNonPromise() + { + $this->expectException(\TypeError::class); + Promise\timeoutWithDefault(42, 42); + } +} From b9b22b47fa591cbb942623132d8c2bc56b4e4987 Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 3 Nov 2018 15:33:58 +0800 Subject: [PATCH 2/4] change timeoutWithDefault function --- lib/functions.php | 21 ++++++++++++--------- test/TimeoutWithDefaultTest.php | 11 +++++++---- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/lib/functions.php b/lib/functions.php index d4cf29b8..91b14602 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -248,20 +248,23 @@ function timeoutWithDefault($promise, int $timeout, $default = null): Promise } } - $promise = timeout($promise, $timeout); + $deferred = new Deferred; - $deferred = new Deferred(); - $newPromise = $deferred->promise(); + $watcher = Loop::delay($timeout, function () use (&$deferred, $default) { + $temp = $deferred; // prevent double resolve + $deferred = null; + $temp->resolve($default); + }); + Loop::unreference($watcher); - $promise->onResolve(function ($exception, $value) use ($deferred, $default) { - if ($exception) { - $deferred->resolve($default); - } else { - $deferred->resolve($value); + $promise->onResolve(function ($exception) use (&$deferred, $promise, $watcher) { + if ($deferred !== null) { + Loop::cancel($watcher); + $deferred->resolve($promise); } }); - return $newPromise; + return $deferred->promise(); } /** diff --git a/test/TimeoutWithDefaultTest.php b/test/TimeoutWithDefaultTest.php index 96e23b7c..48702ac3 100644 --- a/test/TimeoutWithDefaultTest.php +++ b/test/TimeoutWithDefaultTest.php @@ -35,20 +35,23 @@ public function testFailedPromise() { Loop::run(function () { $exception = new \Exception; - $expected = 2; $promise = new Failure($exception); - $promise = Promise\timeoutWithDefault($promise, 100, $expected); + $promise = Promise\timeoutWithDefault($promise, 100, 2); $this->assertInstanceOf(Promise::class, $promise); $callback = function ($exception, $value) use (&$actual) { - $actual = $value; + if ($exception) { + $actual = $exception; + } else { + $actual = $value; + } }; $promise->onResolve($callback); - $this->assertEquals($expected, $actual); + $this->assertSame($exception, $actual); }); } From 8c16f6cbbcec7ed061dc667e784a58139a47aad7 Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 3 Nov 2018 22:55:49 +0800 Subject: [PATCH 3/4] simplify timeoutWithDefault function --- lib/functions.php | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/lib/functions.php b/lib/functions.php index 91b14602..22137405 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -95,7 +95,9 @@ function asyncCall(callable $callback, ...$args) namespace Amp\Promise { + use function Amp\call; use Amp\Deferred; + use Amp\Failure; use Amp\Loop; use Amp\MultiReasonException; use Amp\Promise; @@ -103,6 +105,8 @@ function asyncCall(callable $callback, ...$args) use Amp\TimeoutException; use React\Promise\PromiseInterface as ReactPromise; use function Amp\Internal\createTypeError; + use RuntimeException; + use TypeError; /** * Registers a callback that will forward the failure reason to the event loop's error handler if the promise fails. @@ -240,31 +244,15 @@ function timeout($promise, int $timeout): Promise */ function timeoutWithDefault($promise, int $timeout, $default = null): Promise { - if (!$promise instanceof Promise) { - if ($promise instanceof ReactPromise) { - $promise = adapt($promise); - } else { - throw createTypeError([Promise::class, ReactPromise::class], $promise); - } - } + $promise = timeout($promise, $timeout); - $deferred = new Deferred; - - $watcher = Loop::delay($timeout, function () use (&$deferred, $default) { - $temp = $deferred; // prevent double resolve - $deferred = null; - $temp->resolve($default); - }); - Loop::unreference($watcher); - - $promise->onResolve(function ($exception) use (&$deferred, $promise, $watcher) { - if ($deferred !== null) { - Loop::cancel($watcher); - $deferred->resolve($promise); + return call(function () use ($promise, $default) { + try { + return yield $promise; + } catch (TimeoutException $excetpion) { + return $default; } }); - - return $deferred->promise(); } /** From e203260fa731a7b9a80a29d646b0aa0f3680624c Mon Sep 17 00:00:00 2001 From: prolic Date: Sat, 3 Nov 2018 23:02:46 +0800 Subject: [PATCH 4/4] php cs --- lib/functions.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/functions.php b/lib/functions.php index 22137405..cd7819ef 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -95,7 +95,6 @@ function asyncCall(callable $callback, ...$args) namespace Amp\Promise { - use function Amp\call; use Amp\Deferred; use Amp\Failure; use Amp\Loop; @@ -104,9 +103,8 @@ function asyncCall(callable $callback, ...$args) use Amp\Success; use Amp\TimeoutException; use React\Promise\PromiseInterface as ReactPromise; + use function Amp\call; use function Amp\Internal\createTypeError; - use RuntimeException; - use TypeError; /** * Registers a callback that will forward the failure reason to the event loop's error handler if the promise fails. @@ -249,7 +247,7 @@ function timeoutWithDefault($promise, int $timeout, $default = null): Promise return call(function () use ($promise, $default) { try { return yield $promise; - } catch (TimeoutException $excetpion) { + } catch (TimeoutException $exception) { return $default; } });