# 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