diff --git a/README.md b/README.md index 75f154e..c894d5b 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ A trivial implementation of timeouts for `Promise`s, built on top of [ReactPHP]( * [Usage](#usage) * [timeout()](#timeout) - * [resolve()](#resolve) - * [reject()](#reject) + * [sleep()](#sleep) + * [~~resolve()~~](#resolve) + * [~~reject()~~](#reject) * [TimeoutException](#timeoutexception) * [getTimeout()](#gettimeout) * [Install](#install) @@ -171,7 +172,41 @@ 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). -### resolve() +### 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()~~ + +> 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. @@ -203,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 4bbd03a..43665b2 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 @@ -249,7 +249,50 @@ function resolve($time, LoopInterface $loop = null) } /** - * Create a new promise which rejects in `$time` seconds with a `TimeoutException`. + * [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) { + * 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 + * @deprecated 1.8.0 See `sleep()` instead + * @see sleep() + */ +function resolve($time, LoopInterface $loop = null) +{ + return sleep($time, $loop)->then(function() use ($time) { + return $time; + }); +} + +/** + * [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) { @@ -281,10 +324,12 @@ 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) { - 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()); + } +}