Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new sleep() function and deprecate resolve() and reject() functions #51

Merged
merged 2 commits into from
Dec 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
}
}