Skip to content

Commit bc185b8

Browse files
committed
Add new sleep() function
1 parent fe0cfcd commit bc185b8

File tree

3 files changed

+185
-9
lines changed

3 files changed

+185
-9
lines changed

README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ A trivial implementation of timeouts for `Promise`s, built on top of [ReactPHP](
88

99
* [Usage](#usage)
1010
* [timeout()](#timeout)
11+
* [sleep()](#sleep)
1112
* [resolve()](#resolve)
1213
* [reject()](#reject)
1314
* [TimeoutException](#timeoutexception)
@@ -171,6 +172,38 @@ The applies to all promise collection primitives alike, i.e. `all()`,
171172
For more details on the promise primitives, please refer to the
172173
[Promise documentation](https://github.com/reactphp/promise#functions).
173174

175+
### sleep()
176+
177+
The `sleep(float $time, ?LoopInterface $loop = null): PromiseInterface<void, RuntimeException>` function can be used to
178+
create a new promise that resolves in `$time` seconds.
179+
180+
```php
181+
React\Promise\Timer\sleep(1.5)->then(function () {
182+
echo 'Thanks for waiting!' . PHP_EOL;
183+
});
184+
```
185+
186+
Internally, the given `$time` value will be used to start a timer that will
187+
resolve the promise once it triggers. This implies that if you pass a really
188+
small (or negative) value, it will still start a timer and will thus trigger
189+
at the earliest possible time in the future.
190+
191+
This function takes an optional `LoopInterface|null $loop` parameter that can be used to
192+
pass the event loop instance to use. You can use a `null` value here in order to
193+
use the [default loop](https://github.com/reactphp/event-loop#loop). This value
194+
SHOULD NOT be given unless you're sure you want to explicitly use a given event
195+
loop instance.
196+
197+
The returned promise is implemented in such a way that it can be cancelled
198+
when it is still pending. Cancelling a pending promise will reject its value
199+
with a `RuntimeException` and clean up any pending timers.
200+
201+
```php
202+
$timer = React\Promise\Timer\sleep(2.0);
203+
204+
$timer->cancel();
205+
```
206+
174207
### resolve()
175208

176209
The `resolve(float $time, ?LoopInterface $loop = null): PromiseInterface<float, RuntimeException>` function can be used to

src/functions.php

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -193,11 +193,11 @@ function timeout(PromiseInterface $promise, $time, LoopInterface $loop = null)
193193
}
194194

195195
/**
196-
* Create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value.
196+
* Create a new promise that resolves in `$time` seconds.
197197
*
198198
* ```php
199-
* React\Promise\Timer\resolve(1.5)->then(function ($time) {
200-
* echo 'Thanks for waiting ' . $time . ' seconds' . PHP_EOL;
199+
* React\Promise\Timer\sleep(1.5)->then(function () {
200+
* echo 'Thanks for waiting!' . PHP_EOL;
201201
* });
202202
* ```
203203
*
@@ -217,16 +217,16 @@ function timeout(PromiseInterface $promise, $time, LoopInterface $loop = null)
217217
* with a `RuntimeException` and clean up any pending timers.
218218
*
219219
* ```php
220-
* $timer = React\Promise\Timer\resolve(2.0);
220+
* $timer = React\Promise\Timer\sleep(2.0);
221221
*
222222
* $timer->cancel();
223223
* ```
224224
*
225225
* @param float $time
226226
* @param ?LoopInterface $loop
227-
* @return PromiseInterface<float, \RuntimeException>
227+
* @return PromiseInterface<void, \RuntimeException>
228228
*/
229-
function resolve($time, LoopInterface $loop = null)
229+
function sleep($time, LoopInterface $loop = null)
230230
{
231231
if ($loop === null) {
232232
$loop = Loop::get();
@@ -235,8 +235,8 @@ function resolve($time, LoopInterface $loop = null)
235235
$timer = null;
236236
return new Promise(function ($resolve) use ($loop, $time, &$timer) {
237237
// resolve the promise when the timer fires in $time seconds
238-
$timer = $loop->addTimer($time, function () use ($time, $resolve) {
239-
$resolve($time);
238+
$timer = $loop->addTimer($time, function () use ($resolve) {
239+
$resolve();
240240
});
241241
}, function () use (&$timer, $loop) {
242242
// cancelling this promise will cancel the timer, clean the reference
@@ -248,6 +248,47 @@ function resolve($time, LoopInterface $loop = null)
248248
});
249249
}
250250

251+
/**
252+
* Create a new promise that resolves in `$time` seconds with the `$time` as the fulfillment value.
253+
*
254+
* ```php
255+
* React\Promise\Timer\resolve(1.5)->then(function ($time) {
256+
* echo 'Thanks for waiting ' . $time . ' seconds' . PHP_EOL;
257+
* });
258+
* ```
259+
*
260+
* Internally, the given `$time` value will be used to start a timer that will
261+
* resolve the promise once it triggers. This implies that if you pass a really
262+
* small (or negative) value, it will still start a timer and will thus trigger
263+
* at the earliest possible time in the future.
264+
*
265+
* This function takes an optional `LoopInterface|null $loop` parameter that can be used to
266+
* pass the event loop instance to use. You can use a `null` value here in order to
267+
* use the [default loop](https://github.com/reactphp/event-loop#loop). This value
268+
* SHOULD NOT be given unless you're sure you want to explicitly use a given event
269+
* loop instance.
270+
*
271+
* The returned promise is implemented in such a way that it can be cancelled
272+
* when it is still pending. Cancelling a pending promise will reject its value
273+
* with a `RuntimeException` and clean up any pending timers.
274+
*
275+
* ```php
276+
* $timer = React\Promise\Timer\resolve(2.0);
277+
*
278+
* $timer->cancel();
279+
* ```
280+
*
281+
* @param float $time
282+
* @param ?LoopInterface $loop
283+
* @return PromiseInterface<float, \RuntimeException>
284+
*/
285+
function resolve($time, LoopInterface $loop = null)
286+
{
287+
return sleep($time, $loop)->then(function() use ($time) {
288+
return $time;
289+
});
290+
}
291+
251292
/**
252293
* Create a new promise which rejects in `$time` seconds with a `TimeoutException`.
253294
*
@@ -284,7 +325,7 @@ function resolve($time, LoopInterface $loop = null)
284325
*/
285326
function reject($time, LoopInterface $loop = null)
286327
{
287-
return resolve($time, $loop)->then(function ($time) {
328+
return sleep($time, $loop)->then(function () use ($time) {
288329
throw new TimeoutException($time, 'Timer expired after ' . $time . ' seconds');
289330
});
290331
}

tests/FunctionSleepTest.php

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace React\Tests\Promise\Timer;
4+
5+
use React\EventLoop\Loop;
6+
use React\Promise\Timer;
7+
8+
class FunctionSleepTest extends TestCase
9+
{
10+
public function testPromiseIsPendingWithoutRunningLoop()
11+
{
12+
$promise = Timer\sleep(0.01);
13+
14+
$this->expectPromisePending($promise);
15+
}
16+
17+
public function testPromiseExpiredIsPendingWithoutRunningLoop()
18+
{
19+
$promise = Timer\sleep(-1);
20+
21+
$this->expectPromisePending($promise);
22+
}
23+
24+
public function testPromiseWillBeResolvedOnTimeout()
25+
{
26+
$promise = Timer\sleep(0.01);
27+
28+
Loop::run();
29+
30+
$this->expectPromiseResolved($promise);
31+
}
32+
33+
public function testPromiseExpiredWillBeResolvedOnTimeout()
34+
{
35+
$promise = Timer\sleep(-1);
36+
37+
Loop::run();
38+
39+
$this->expectPromiseResolved($promise);
40+
}
41+
42+
public function testWillStartLoopTimer()
43+
{
44+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
45+
$loop->expects($this->once())->method('addTimer')->with($this->equalTo(0.01));
46+
47+
Timer\sleep(0.01, $loop);
48+
}
49+
50+
public function testCancellingPromiseWillCancelLoopTimer()
51+
{
52+
$loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
53+
54+
$timer = $this->getMockBuilder(interface_exists('React\EventLoop\TimerInterface') ? 'React\EventLoop\TimerInterface' : 'React\EventLoop\Timer\TimerInterface')->getMock();
55+
$loop->expects($this->once())->method('addTimer')->will($this->returnValue($timer));
56+
57+
$promise = Timer\sleep(0.01, $loop);
58+
59+
$loop->expects($this->once())->method('cancelTimer')->with($this->equalTo($timer));
60+
61+
$promise->cancel();
62+
}
63+
64+
public function testCancellingPromiseWillRejectTimer()
65+
{
66+
$promise = Timer\sleep(0.01);
67+
68+
$promise->cancel();
69+
70+
$this->expectPromiseRejected($promise);
71+
}
72+
73+
public function testWaitingForPromiseToResolveDoesNotLeaveGarbageCycles()
74+
{
75+
if (class_exists('React\Promise\When')) {
76+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
77+
}
78+
79+
gc_collect_cycles();
80+
81+
$promise = Timer\sleep(0.01);
82+
Loop::run();
83+
unset($promise);
84+
85+
$this->assertEquals(0, gc_collect_cycles());
86+
}
87+
88+
public function testCancellingPromiseDoesNotLeaveGarbageCycles()
89+
{
90+
if (class_exists('React\Promise\When')) {
91+
$this->markTestSkipped('Not supported on legacy Promise v1 API');
92+
}
93+
94+
gc_collect_cycles();
95+
96+
$promise = Timer\sleep(0.01);
97+
$promise->cancel();
98+
unset($promise);
99+
100+
$this->assertEquals(0, gc_collect_cycles());
101+
}
102+
}

0 commit comments

Comments
 (0)