# react/promise-timer [![Build Status](https://travis-ci.org/reactphp/promise-timer.svg?branch=master)](https://travis-ci.org/reactphp/promise-timer)

A trivial implementation of timeouts for `Promise`s, built on top of [React PHP](http://reactphp.org/).

**Table of contents**

* [Usage](#usage)
  * [timeout()](#timeout)
    * [Timeout cancellation](#timeout-cancellation)
    * [Cancellation handler](#cancellation-handler)
    * [Input cancellation](#input-cancellation)
    * [Output cancellation](#output-cancellation)
    * [Collections](#collections)
  * [resolve()](#resolve)
    * [Resolve cancellation](#resolve-cancellation)
  * [reject()](#reject)
    * [Reject cancellation](#reject-cancellation)
  * [TimeoutException](#timeoutexception)
* [Install](#install)
* [Tests](#tests)
* [License](#license)

## Usage

This lightweight library consists only of a few simple functions.
All functions reside under the `React\Promise\Timer` namespace.

The below examples assume you use an import statement similar to this:

```php
use React\Promise\Timer;

Timer\timeout(…);
```

Alternatively, you can also refer to them with their fully-qualified name:

```php
\React\Promise\Timer\timeout(…);
``` 

### timeout()

The `timeout(PromiseInterface $promise, $time, LoopInterface $loop)` function
can be used to *cancel* operations that take *too long*.
You need to pass in an input `$promise` that represents a pending operation and timeout parameters.
It returns a new `Promise` with the following resolution behavior:

* If the input `$promise` resolves before `$time` seconds, resolve the resulting promise with its fulfillment value.
* If the input `$promise` rejects before `$time` seconds, reject the resulting promise with its rejection value.
* If the input `$promise` does not settle before `$time` seconds, *cancel* the operation and reject the resulting promise with a [`TimeoutException`](#timeoutexception).

Internally, the given `$time` value will be used to start a timer that will
*cancel* the pending operation 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.

If the input `$promise` is already settled, then the resulting promise will
resolve or reject immediately without starting a timer at all.

A common use case for handling only resolved values looks like this:

```php
$promise = accessSomeRemoteResource();
Timer\timeout($promise, 10.0, $loop)->then(function ($value) {
    // the operation finished within 10.0 seconds
});
```

A more complete example could look like this:

```php
$promise = accessSomeRemoteResource();
Timer\timeout($promise, 10.0, $loop)->then(
    function ($value) {
        // the operation finished within 10.0 seconds
    },
    function ($error) {
        if ($error instanceof Timer\TimeoutException) {
            // the operation has failed due to a timeout
        } else {
            // the input operation has failed due to some other error
        }
    }
);
```

Or if you're using [react/promise v2.2.0](https://github.com/reactphp/promise) or up:

```php
Timer\timeout($promise, 10.0, $loop)
    ->then(function ($value) {
        // the operation finished within 10.0 seconds
    })
    ->otherwise(function (Timer\TimeoutException $error) {
        // the operation has failed due to a timeout
    })
    ->otherwise(function ($error) {
        // the input operation has failed due to some other error
    })
;
```

#### Timeout cancellation

As discussed above, the [`timeout()`](#timeout) function will *cancel* the
underlying operation if it takes *too long*.
This means that you can be sure the resulting promise will then be rejected
with a [`TimeoutException`](#timeoutexception).

However, what happens to the underlying input `$promise` is a bit more tricky:
Once the timer fires, we will try to call
[`$promise->cancel()`](https://github.com/reactphp/promise#cancellablepromiseinterfacecancel)
on the input `$promise` which in turn invokes its [cancellation handler](#cancellation-handler).

This means that it's actually up the input `$promise` to handle
[cancellation support](https://github.com/reactphp/promise#cancellablepromiseinterface).

* A common use case involves cleaning up any resources like open network sockets or
  file handles or terminating external processes or timers.

* If the given input `$promise` does not support cancellation, then this is a NO-OP.
  This means that while the resulting promise will still be rejected, the underlying
  input `$promise` may still be pending and can hence continue consuming resources.

See the following chapter for more details on the cancellation handler.

#### Cancellation handler

For example, an implementation for the above operation could look like this:

```php
function accessSomeRemoteResource()
{
    return new Promise(
        function ($resolve, $reject) use (&$socket) {
            // this will be called once the promise is created
            // a common use case involves opening any resources and eventually resolving
            $socket = createSocket();
            $socket->on('data', function ($data) use ($resolve) {
                $resolve($data);
            });
        },
        function ($resolve, $reject) use (&$socket) {
            // this will be called once calling `cancel()` on this promise
            // a common use case involves cleaning any resources and then rejecting
            $socket->close();
            $reject(new \RuntimeException('Operation cancelled'));
        }
    );
}
```

In this example, calling `$promise->cancel()` will invoke the registered cancellation
handler which then closes the network socket and rejects the `Promise` instance.

If no cancellation handler is passed to the `Promise` constructor, then invoking
its `cancel()` method it is effectively a NO-OP.
This means that it may still be pending and can hence continue consuming resources.

For more details on the promise cancellation, please refer to the
[Promise documentation](https://github.com/reactphp/promise#cancellablepromiseinterface).

#### Input cancellation

Irrespective of the timout handling, you can also explicitly `cancel()` the
input `$promise` at any time.
This means that the `timeout()` handling does not affect cancellation of the
input `$promise`, as demonstrated in the following example:

```php
$promise = accessSomeRemoteResource();
$timeout = Timer\timeout($promise, 10.0, $loop);

$promise->cancel();
```

The registered [cancellation handler](#cancellation-handler) is responsible for
handling the `cancel()` call:

* A described above, a common use involves resource cleanup and will then *reject*
  the `Promise`.
  If the input `$promise` is being rejected, then the timeout will be aborted
  and the resulting promise will also be rejected.
* If the input `$promise` is still pending, then the timout will continue
  running until the timer expires.
  The same happens if the input `$promise` does not register a
  [cancellation handler](#cancellation-handler). 

#### Output cancellation

Similarily, you can also explicitly `cancel()` the resulting promise like this:

```php
$promise = accessSomeRemoteResource();
$timeout = Timer\timeout($promise, 10.0, $loop);

$timeout->cancel();
```

Note how this looks very similar to the above [input cancellation](#input-cancellation)
example. Accordingly, it also behaves very similar.

Calling `cancel()` on the resulting promise will merely try
to `cancel()` the input `$promise`.
This means that we do not take over responsibility of the outcome and it's
entirely up to the input `$promise` to handle cancellation support.

The registered [cancellation handler](#cancellation-handler) is responsible for
handling the `cancel()` call:

* As described above, a common use involves resource cleanup and will then *reject*
  the `Promise`.
  If the input `$promise` is being rejected, then the timeout will be aborted
  and the resulting promise will also be rejected.
* If the input `$promise` is still pending, then the timout will continue
  running until the timer expires.
  The same happens if the input `$promise` does not register a
  [cancellation handler](#cancellation-handler). 

To re-iterate, note that calling `cancel()` on the resulting promise will merely
try to cancel the input `$promise` only.
It is then up to the cancellation handler of the input promise to settle the promise.
If the input promise is still pending when the timeout occurs, then the normal
[timeout cancellation](#timeout-cancellation) handling will trigger, effectively rejecting
the output promise with a [`TimeoutException`](#timeoutexception).

This is done for consistency with the [timeout cancellation](#timeout-cancellation)
handling and also because it is assumed this is often used like this:

```php
$timeout = Timer\timeout(accessSomeRemoteResource(), 10.0, $loop);

$timeout->cancel();
```

As described above, this example works as expected and cleans up any resources
allocated for the input `$promise`.

Note that if the given input `$promise` does not support cancellation, then this
is a NO-OP.
This means that while the resulting promise will still be rejected after the
timeout, the underlying input `$promise` may still be pending and can hence
continue consuming resources.

#### Collections

If you want to wait for multiple promises to resolve, you can use the normal promise primitives like this:

```php
$promises = array(
    accessSomeRemoteResource(),
    accessSomeRemoteResource(),
    accessSomeRemoteResource()
);

$promise = \React\Promise\all($promises);

Timer\timeout($promise, 10, $loop)->then(function ($values) {
    // *all* promises resolved
});
```

The applies to all promise collection primitives alike, i.e. `all()`, `race()`, `any()`, `some()` etc.

For more details on the promise primitives, please refer to the
[Promise documentation](https://github.com/reactphp/promise#functions).

### resolve()

The `resolve($time, LoopInterface $loop)` function can be used to create a new Promise that
resolves in `$time` seconds with the `$time` as the fulfillment value.

```php
Timer\resolve(1.5, $loop)->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.

#### Resolve cancellation

You can explicitly `cancel()` the resulting timer promise at any time:

```php
$timer = Timer\resolve(2.0, $loop);

$timer->cancel();
```

This will abort the timer and *reject* with a `RuntimeException`.

### reject()

The `reject($time, LoopInterface $loop)` function can be used to create a new Promise
which rejects in `$time` seconds with a `TimeoutException`.

```php
Timer\reject(2.0, $loop)->then(null, function (TimeoutException $e) {
    echo 'Rejected after ' . $e->getTimeout() . ' seconds ' . PHP_EOL;
});
```

Internally, the given `$time` value will be used to start a timer that will
reject 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 complements the [`resolve()`](#resolve) function
and can be used as a basic building block for higher-level promise consumers.

#### Reject cancellation

You can explicitly `cancel()` the resulting timer promise at any time:

```php
$timer = Timer\reject(2.0, $loop);

$timer->cancel();
```

This will abort the timer and *reject* with a `RuntimeException`.

### TimeoutException

The `TimeoutException` extends PHP's built-in `RuntimeException`.

The `getTimeout()` method can be used to get the timeout value in seconds.

## Install

The recommended way to install this library is [through Composer](http://getcomposer.org).
[New to Composer?](http://getcomposer.org/doc/00-intro.md)

This will install the latest supported version:

```bash
$ composer require react/promise-timer:^1.2
```

More details and upgrade guides can be found in the [CHANGELOG](CHANGELOG.md).

## Tests

To run the test suite, you first need to clone this repo and then install all
dependencies [through Composer](http://getcomposer.org):

```bash
$ composer install
```

To run the test suite, go to the project root and run:

```bash
$ php vendor/bin/phpunit
```

## License

MIT
