From bc185b8e9c17636daefc98bcf727583e7fd70365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 28 Nov 2021 19:56:01 +0100 Subject: [PATCH 1/2] Add new `sleep()` function --- README.md | 33 ++++++++++++ src/functions.php | 59 +++++++++++++++++---- tests/FunctionSleepTest.php | 102 ++++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 9 deletions(-) create mode 100644 tests/FunctionSleepTest.php diff --git a/README.md b/README.md index 75f154e..46f45b3 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A trivial implementation of timeouts for `Promise`s, built on top of [ReactPHP]( * [Usage](#usage) * [timeout()](#timeout) + * [sleep()](#sleep) * [resolve()](#resolve) * [reject()](#reject) * [TimeoutException](#timeoutexception) @@ -171,6 +172,38 @@ The applies to all promise collection primitives alike, i.e. `all()`, For more details on the promise primitives, please refer to the [Promise documentation](https://github.com/reactphp/promise#functions). +### sleep() + +The `sleep(float $time, ?LoopInterface $loop = null): PromiseInterface` function can be used to +create a new promise that resolves in `$time` seconds. + +```php +React\Promise\Timer\sleep(1.5)->then(function () { + echo 'Thanks for waiting!' . PHP_EOL; +}); +``` + +Internally, the given `$time` value will be used to start a timer that will +resolve the promise once it triggers. This implies that if you pass a really +small (or negative) value, it will still start a timer and will thus trigger +at the earliest possible time in the future. + +This function takes an optional `LoopInterface|null $loop` parameter that can be used to +pass the event loop instance to use. You can use a `null` value here in order to +use the [default loop](https://github.com/reactphp/event-loop#loop). This value +SHOULD NOT be given unless you're sure you want to explicitly use a given event +loop instance. + +The returned promise is implemented in such a way that it can be cancelled +when it is still pending. Cancelling a pending promise will reject its value +with a `RuntimeException` and clean up any pending timers. + +```php +$timer = React\Promise\Timer\sleep(2.0); + +$timer->cancel(); +``` + ### resolve() The `resolve(float $time, ?LoopInterface $loop = null): PromiseInterface` function can be used to diff --git a/src/functions.php b/src/functions.php index 4bbd03a..0d46805 100644 --- a/src/functions.php +++ b/src/functions.php @@ -193,11 +193,11 @@ function timeout(PromiseInterface $promise, $time, LoopInterface $loop = null) } /** - * Create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value. + * Create a new promise that resolves in `$time` seconds. * * ```php - * React\Promise\Timer\resolve(1.5)->then(function ($time) { - * echo 'Thanks for waiting ' . $time . ' seconds' . PHP_EOL; + * React\Promise\Timer\sleep(1.5)->then(function () { + * echo 'Thanks for waiting!' . PHP_EOL; * }); * ``` * @@ -217,16 +217,16 @@ function timeout(PromiseInterface $promise, $time, LoopInterface $loop = null) * with a `RuntimeException` and clean up any pending timers. * * ```php - * $timer = React\Promise\Timer\resolve(2.0); + * $timer = React\Promise\Timer\sleep(2.0); * * $timer->cancel(); * ``` * * @param float $time * @param ?LoopInterface $loop - * @return PromiseInterface + * @return PromiseInterface */ -function resolve($time, LoopInterface $loop = null) +function sleep($time, LoopInterface $loop = null) { if ($loop === null) { $loop = Loop::get(); @@ -235,8 +235,8 @@ function resolve($time, LoopInterface $loop = null) $timer = null; return new Promise(function ($resolve) use ($loop, $time, &$timer) { // resolve the promise when the timer fires in $time seconds - $timer = $loop->addTimer($time, function () use ($time, $resolve) { - $resolve($time); + $timer = $loop->addTimer($time, function () use ($resolve) { + $resolve(); }); }, function () use (&$timer, $loop) { // cancelling this promise will cancel the timer, clean the reference @@ -248,6 +248,47 @@ function resolve($time, LoopInterface $loop = null) }); } +/** + * Create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value. + * + * ```php + * React\Promise\Timer\resolve(1.5)->then(function ($time) { + * echo 'Thanks for waiting ' . $time . ' seconds' . PHP_EOL; + * }); + * ``` + * + * Internally, the given `$time` value will be used to start a timer that will + * resolve the promise once it triggers. This implies that if you pass a really + * small (or negative) value, it will still start a timer and will thus trigger + * at the earliest possible time in the future. + * + * This function takes an optional `LoopInterface|null $loop` parameter that can be used to + * pass the event loop instance to use. You can use a `null` value here in order to + * use the [default loop](https://github.com/reactphp/event-loop#loop). This value + * SHOULD NOT be given unless you're sure you want to explicitly use a given event + * loop instance. + * + * The returned promise is implemented in such a way that it can be cancelled + * when it is still pending. Cancelling a pending promise will reject its value + * with a `RuntimeException` and clean up any pending timers. + * + * ```php + * $timer = React\Promise\Timer\resolve(2.0); + * + * $timer->cancel(); + * ``` + * + * @param float $time + * @param ?LoopInterface $loop + * @return PromiseInterface + */ +function resolve($time, LoopInterface $loop = null) +{ + return sleep($time, $loop)->then(function() use ($time) { + return $time; + }); +} + /** * Create a new promise which rejects in `$time` seconds with a `TimeoutException`. * @@ -284,7 +325,7 @@ function resolve($time, LoopInterface $loop = null) */ function reject($time, LoopInterface $loop = null) { - return resolve($time, $loop)->then(function ($time) { + return sleep($time, $loop)->then(function () use ($time) { throw new TimeoutException($time, 'Timer expired after ' . $time . ' seconds'); }); } diff --git a/tests/FunctionSleepTest.php b/tests/FunctionSleepTest.php new file mode 100644 index 0000000..d39c95a --- /dev/null +++ b/tests/FunctionSleepTest.php @@ -0,0 +1,102 @@ +expectPromisePending($promise); + } + + public function testPromiseExpiredIsPendingWithoutRunningLoop() + { + $promise = Timer\sleep(-1); + + $this->expectPromisePending($promise); + } + + public function testPromiseWillBeResolvedOnTimeout() + { + $promise = Timer\sleep(0.01); + + Loop::run(); + + $this->expectPromiseResolved($promise); + } + + public function testPromiseExpiredWillBeResolvedOnTimeout() + { + $promise = Timer\sleep(-1); + + Loop::run(); + + $this->expectPromiseResolved($promise); + } + + public function testWillStartLoopTimer() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->with($this->equalTo(0.01)); + + Timer\sleep(0.01, $loop); + } + + public function testCancellingPromiseWillCancelLoopTimer() + { + $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); + + $timer = $this->getMockBuilder(interface_exists('React\EventLoop\TimerInterface') ? 'React\EventLoop\TimerInterface' : 'React\EventLoop\Timer\TimerInterface')->getMock(); + $loop->expects($this->once())->method('addTimer')->will($this->returnValue($timer)); + + $promise = Timer\sleep(0.01, $loop); + + $loop->expects($this->once())->method('cancelTimer')->with($this->equalTo($timer)); + + $promise->cancel(); + } + + public function testCancellingPromiseWillRejectTimer() + { + $promise = Timer\sleep(0.01); + + $promise->cancel(); + + $this->expectPromiseRejected($promise); + } + + public function testWaitingForPromiseToResolveDoesNotLeaveGarbageCycles() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + gc_collect_cycles(); + + $promise = Timer\sleep(0.01); + Loop::run(); + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } + + public function testCancellingPromiseDoesNotLeaveGarbageCycles() + { + if (class_exists('React\Promise\When')) { + $this->markTestSkipped('Not supported on legacy Promise v1 API'); + } + + gc_collect_cycles(); + + $promise = Timer\sleep(0.01); + $promise->cancel(); + unset($promise); + + $this->assertEquals(0, gc_collect_cycles()); + } +} From b9469d350bbd8f49987b516801914556b85ff4c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20L=C3=BCck?= Date: Sun, 5 Dec 2021 12:13:38 +0100 Subject: [PATCH 2/2] Deprecate `resolve()` and `reject()` functions in favor of `sleep()` --- README.md | 12 ++++++++---- src/functions.php | 8 ++++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 46f45b3..c894d5b 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ A trivial implementation of timeouts for `Promise`s, built on top of [ReactPHP]( * [Usage](#usage) * [timeout()](#timeout) * [sleep()](#sleep) - * [resolve()](#resolve) - * [reject()](#reject) + * [~~resolve()~~](#resolve) + * [~~reject()~~](#reject) * [TimeoutException](#timeoutexception) * [getTimeout()](#gettimeout) * [Install](#install) @@ -204,7 +204,9 @@ $timer = React\Promise\Timer\sleep(2.0); $timer->cancel(); ``` -### resolve() +### ~~resolve()~~ + +> Deprecated since v1.8.0, see [`sleep()`](#sleep) instead. The `resolve(float $time, ?LoopInterface $loop = null): PromiseInterface` function can be used to create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value. @@ -236,7 +238,9 @@ $timer = React\Promise\Timer\resolve(2.0); $timer->cancel(); ``` -### reject() +### ~~reject()~~ + +> Deprecated since v1.8.0, see [`sleep()`](#sleep) instead. The `reject(float $time, ?LoopInterface $loop = null): PromiseInterface` function can be used to create a new promise which rejects in `$time` seconds with a `TimeoutException`. diff --git a/src/functions.php b/src/functions.php index 0d46805..43665b2 100644 --- a/src/functions.php +++ b/src/functions.php @@ -249,7 +249,7 @@ function sleep($time, LoopInterface $loop = null) } /** - * Create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value. + * [Deprecated] Create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value. * * ```php * React\Promise\Timer\resolve(1.5)->then(function ($time) { @@ -281,6 +281,8 @@ function sleep($time, LoopInterface $loop = null) * @param float $time * @param ?LoopInterface $loop * @return PromiseInterface + * @deprecated 1.8.0 See `sleep()` instead + * @see sleep() */ function resolve($time, LoopInterface $loop = null) { @@ -290,7 +292,7 @@ function resolve($time, LoopInterface $loop = null) } /** - * Create a new promise which rejects in `$time` seconds with a `TimeoutException`. + * [Deprecated] Create a new promise which rejects in `$time` seconds with a `TimeoutException`. * * ```php * React\Promise\Timer\reject(2.0)->then(null, function (React\Promise\Timer\TimeoutException $e) { @@ -322,6 +324,8 @@ function resolve($time, LoopInterface $loop = null) * @param float $time * @param LoopInterface $loop * @return PromiseInterface + * @deprecated 1.8.0 See `sleep()` instead + * @see sleep() */ function reject($time, LoopInterface $loop = null) {