Skip to content

Commit

Permalink
Merge pull request #51 from clue-labs/sleep
Browse files Browse the repository at this point in the history
Add new `sleep()` function and deprecate `resolve()` and `reject()` functions
  • Loading branch information
WyriHaximus authored Dec 5, 2021
2 parents 9ccdc9b + b9469d3 commit 997c10e
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 14 deletions.
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<void, RuntimeException>` 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<float, RuntimeException>` function can be used to
create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value.
Expand Down Expand Up @@ -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<void, TimeoutException|RuntimeException>` function can be used to
create a new promise which rejects in `$time` seconds with a `TimeoutException`.
Expand Down
65 changes: 55 additions & 10 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
* });
* ```
*
Expand All @@ -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<float, \RuntimeException>
* @return PromiseInterface<void, \RuntimeException>
*/
function resolve($time, LoopInterface $loop = null)
function sleep($time, LoopInterface $loop = null)
{
if ($loop === null) {
$loop = Loop::get();
Expand All @@ -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
Expand All @@ -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<float, \RuntimeException>
* @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) {
Expand Down Expand Up @@ -281,10 +324,12 @@ function resolve($time, LoopInterface $loop = null)
* @param float $time
* @param LoopInterface $loop
* @return PromiseInterface<void, TimeoutException|\RuntimeException>
* @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');
});
}
102 changes: 102 additions & 0 deletions tests/FunctionSleepTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

namespace React\Tests\Promise\Timer;

use React\EventLoop\Loop;
use React\Promise\Timer;

class FunctionSleepTest extends TestCase
{
public function testPromiseIsPendingWithoutRunningLoop()
{
$promise = Timer\sleep(0.01);

$this->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());
}
}

0 comments on commit 997c10e

Please sign in to comment.