Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/reactphp/async

Async utilities and fibers for ReactPHP.
https://github.com/reactphp/async

Last synced: about 2 months ago
JSON representation

Async utilities and fibers for ReactPHP.

Awesome Lists containing this project

README

        

# Async Utilities

[![CI status](https://github.com/reactphp/async/workflows/CI/badge.svg)](https://github.com/reactphp/async/actions)
[![installs on Packagist](https://img.shields.io/packagist/dt/react/async?color=blue&label=installs%20on%20Packagist)](https://packagist.org/packages/react/async)

Async utilities and fibers for [ReactPHP](https://reactphp.org/).

This library allows you to manage async control flow. It provides a number of
combinators for [Promise](https://github.com/reactphp/promise)-based APIs.
Instead of nesting or chaining promise callbacks, you can declare them as a
list, which is resolved sequentially in an async manner.
React/Async will not automagically change blocking code to be async. You need
to have an actual event loop and non-blocking libraries interacting with that
event loop for it to work. As long as you have a Promise-based API that runs in
an event loop, it can be used with this library.

**Table of Contents**

* [Usage](#usage)
* [async()](#async)
* [await()](#await)
* [coroutine()](#coroutine)
* [delay()](#delay)
* [parallel()](#parallel)
* [series()](#series)
* [waterfall()](#waterfall)
* [Todo](#todo)
* [Install](#install)
* [Tests](#tests)
* [License](#license)

## Usage

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

The below examples refer to all functions with their fully-qualified names like this:

```php
React\Async\await(…);
```

As of PHP 5.6+ you can also import each required function into your code like this:

```php
use function React\Async\await;

await(…);
```

Alternatively, you can also use an import statement similar to this:

```php
use React\Async;

Async\await(…);
```

### async()

The `async(callable():(PromiseInterface|T) $function): (callable():PromiseInterface)` function can be used to
return an async function for a function that uses [`await()`](#await) internally.

This function is specifically designed to complement the [`await()` function](#await).
The [`await()` function](#await) can be considered *blocking* from the
perspective of the calling code. You can avoid this blocking behavior by
wrapping it in an `async()` function call. Everything inside this function
will still be blocked, but everything outside this function can be executed
asynchronously without blocking:

```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
React\Async\await(React\Promise\Timer\sleep(1.0));
echo 'c';
}));

Loop::addTimer(1.0, function () {
echo 'b';
});

// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```

See also the [`await()` function](#await) for more details.

Note that this function only works in tandem with the [`await()` function](#await).
In particular, this function does not "magically" make any blocking function
non-blocking:

```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
sleep(1); // broken: using PHP's blocking sleep() for demonstration purposes
echo 'c';
}));

Loop::addTimer(1.0, function () {
echo 'b';
});

// prints "a" at t=0.5s
// prints "c" at t=1.5s: Correct timing, but wrong order
// prints "b" at t=1.5s: Triggered too late because it was blocked
```

As an alternative, you should always make sure to use this function in tandem
with the [`await()` function](#await) and an async API returning a promise
as shown in the previous example.

The `async()` function is specifically designed for cases where it is used
as a callback (such as an event loop timer, event listener, or promise
callback). For this reason, it returns a new function wrapping the given
`$function` instead of directly invoking it and returning its value.

```php
use function React\Async\async;

Loop::addTimer(1.0, async(function () { … }));
$connection->on('close', async(function () { … }));
$stream->on('data', async(function ($data) { … }));
$promise->then(async(function (int $result) { … }));
```

You can invoke this wrapping function to invoke the given `$function` with
any arguments given as-is. The function will always return a Promise which
will be fulfilled with whatever your `$function` returns. Likewise, it will
return a promise that will be rejected if you throw an `Exception` or
`Throwable` from your `$function`. This allows you to easily create
Promise-based functions:

```php
$promise = React\Async\async(function (): int {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];

$bytes = 0;
foreach ($urls as $url) {
$response = React\Async\await($browser->get($url));
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
})();

$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

The previous example uses [`await()`](#await) inside a loop to highlight how
this vastly simplifies consuming asynchronous operations. At the same time,
this naive example does not leverage concurrent execution, as it will
essentially "await" between each operation. In order to take advantage of
concurrent execution within the given `$function`, you can "await" multiple
promises by using a single [`await()`](#await) together with Promise-based
primitives like this:

```php
$promise = React\Async\async(function (): int {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];

$promises = [];
foreach ($urls as $url) {
$promises[] = $browser->get($url);
}

try {
$responses = React\Async\await(React\Promise\all($promises));
} catch (Exception $e) {
foreach ($promises as $promise) {
$promise->cancel();
}
throw $e;
}

$bytes = 0;
foreach ($responses as $response) {
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
})();

$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

The returned promise is implemented in such a way that it can be cancelled
when it is still pending. Cancelling a pending promise will cancel any awaited
promises inside that fiber or any nested fibers. As such, the following example
will only output `ab` and cancel the pending [`delay()`](#delay).
The [`await()`](#await) calls in this example would throw a `RuntimeException`
from the cancelled [`delay()`](#delay) call that bubbles up through the fibers.

```php
$promise = async(static function (): int {
echo 'a';
await(async(static function (): void {
echo 'b';
delay(2);
echo 'c';
})());
echo 'd';

return time();
})();

$promise->cancel();
await($promise);
```

### await()

The `await(PromiseInterface $promise): T` function can be used to
block waiting for the given `$promise` to be fulfilled.

```php
$result = React\Async\await($promise);
```

This function will only return after the given `$promise` has settled, i.e.
either fulfilled or rejected. While the promise is pending, this function
can be considered *blocking* from the perspective of the calling code.
You can avoid this blocking behavior by wrapping it in an [`async()` function](#async)
call. Everything inside this function will still be blocked, but everything
outside this function can be executed asynchronously without blocking:

```php
Loop::addTimer(0.5, React\Async\async(function () {
echo 'a';
React\Async\await(React\Promise\Timer\sleep(1.0));
echo 'c';
}));

Loop::addTimer(1.0, function () {
echo 'b';
});

// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```

See also the [`async()` function](#async) for more details.

Once the promise is fulfilled, this function will return whatever the promise
resolved to.

Once the promise is rejected, this will throw whatever the promise rejected
with. If the promise did not reject with an `Exception` or `Throwable`, then
this function will throw an `UnexpectedValueException` instead.

```php
try {
$result = React\Async\await($promise);
// promise successfully fulfilled with $result
echo 'Result: ' . $result;
} catch (Throwable $e) {
// promise rejected with $e
echo 'Error: ' . $e->getMessage();
}
```

### coroutine()

The `coroutine(callable(mixed ...$args):(\Generator|PromiseInterface|T) $function, mixed ...$args): PromiseInterface` function can be used to
execute a Generator-based coroutine to "await" promises.

```php
React\Async\coroutine(function () {
$browser = new React\Http\Browser();

try {
$response = yield $browser->get('https://example.com/');
assert($response instanceof Psr\Http\Message\ResponseInterface);
echo $response->getBody();
} catch (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
}
});
```

Using Generator-based coroutines is an alternative to directly using the
underlying promise APIs. For many use cases, this makes using promise-based
APIs much simpler, as it resembles a synchronous code flow more closely.
The above example performs the equivalent of directly using the promise APIs:

```php
$browser = new React\Http\Browser();

$browser->get('https://example.com/')->then(function (Psr\Http\Message\ResponseInterface $response) {
echo $response->getBody();
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

The `yield` keyword can be used to "await" a promise resolution. Internally,
it will turn the entire given `$function` into a [`Generator`](https://www.php.net/manual/en/class.generator.php).
This allows the execution to be interrupted and resumed at the same place
when the promise is fulfilled. The `yield` statement returns whatever the
promise is fulfilled with. If the promise is rejected, it will throw an
`Exception` or `Throwable`.

The `coroutine()` function will always return a Promise which will be
fulfilled with whatever your `$function` returns. Likewise, it will return
a promise that will be rejected if you throw an `Exception` or `Throwable`
from your `$function`. This allows you to easily create Promise-based
functions:

```php
$promise = React\Async\coroutine(function () {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];

$bytes = 0;
foreach ($urls as $url) {
$response = yield $browser->get($url);
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
});

$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

The previous example uses a `yield` statement inside a loop to highlight how
this vastly simplifies consuming asynchronous operations. At the same time,
this naive example does not leverage concurrent execution, as it will
essentially "await" between each operation. In order to take advantage of
concurrent execution within the given `$function`, you can "await" multiple
promises by using a single `yield` together with Promise-based primitives
like this:

```php
$promise = React\Async\coroutine(function () {
$browser = new React\Http\Browser();
$urls = [
'https://example.com/alice',
'https://example.com/bob'
];

$promises = [];
foreach ($urls as $url) {
$promises[] = $browser->get($url);
}

try {
$responses = yield React\Promise\all($promises);
} catch (Exception $e) {
foreach ($promises as $promise) {
$promise->cancel();
}
throw $e;
}

$bytes = 0;
foreach ($responses as $response) {
assert($response instanceof Psr\Http\Message\ResponseInterface);
$bytes += $response->getBody()->getSize();
}
return $bytes;
});

$promise->then(function (int $bytes) {
echo 'Total size: ' . $bytes . PHP_EOL;
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

### delay()

The `delay(float $seconds): void` function can be used to
delay program execution for duration given in `$seconds`.

```php
React\Async\delay($seconds);
```

This function will only return after the given number of `$seconds` have
elapsed. If there are no other events attached to this loop, it will behave
similar to PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php).

```php
echo 'a';
React\Async\delay(1.0);
echo 'b';

// prints "a" at t=0.0s
// prints "b" at t=1.0s
```

Unlike PHP's [`sleep()` function](https://www.php.net/manual/en/function.sleep.php),
this function may not necessarily halt execution of the entire process thread.
Instead, it allows the event loop to run any other events attached to the
same loop until the delay returns:

```php
echo 'a';
Loop::addTimer(1.0, function (): void {
echo 'b';
});
React\Async\delay(3.0);
echo 'c';

// prints "a" at t=0.0s
// prints "b" at t=1.0s
// prints "c" at t=3.0s
```

This behavior is especially useful if you want to delay the program execution
of a particular routine, such as when building a simple polling or retry
mechanism:

```php
try {
something();
} catch (Throwable) {
// in case of error, retry after a short delay
React\Async\delay(1.0);
something();
}
```

Because this function only returns after some time has passed, it can be
considered *blocking* from the perspective of the calling code. You can avoid
this blocking behavior by wrapping it in an [`async()` function](#async) call.
Everything inside this function will still be blocked, but everything outside
this function can be executed asynchronously without blocking:

```php
Loop::addTimer(0.5, React\Async\async(function (): void {
echo 'a';
React\Async\delay(1.0);
echo 'c';
}));

Loop::addTimer(1.0, function (): void {
echo 'b';
});

// prints "a" at t=0.5s
// prints "b" at t=1.0s
// prints "c" at t=1.5s
```

See also the [`async()` function](#async) for more details.

Internally, the `$seconds` argument will be used as a timer for the loop so that
it keeps running until this timer 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.

The function is implemented in such a way that it can be cancelled when it is
running inside an [`async()` function](#async). Cancelling the resulting
promise will clean up any pending timers and throw a `RuntimeException` from
the pending delay which in turn would reject the resulting promise.

```php
$promise = async(function (): void {
echo 'a';
delay(3.0);
echo 'b';
})();

Loop::addTimer(2.0, function () use ($promise): void {
$promise->cancel();
});

// prints "a" at t=0.0s
// rejects $promise at t=2.0
// never prints "b"
```

### parallel()

The `parallel(iterable> $tasks): PromiseInterface>` function can be used
like this:

```php
then(function (array $results) {
foreach ($results as $result) {
var_dump($result);
}
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

### series()

The `series(iterable> $tasks): PromiseInterface>` function can be used
like this:

```php
then(function (array $results) {
foreach ($results as $result) {
var_dump($result);
}
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

### waterfall()

The `waterfall(iterable> $tasks): PromiseInterface` function can be used
like this:

```php
then(function ($prev) {
echo "Final result is $prev\n";
}, function (Exception $e) {
echo 'Error: ' . $e->getMessage() . PHP_EOL;
});
```

## Todo

* Implement queue()

## Install

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

This project follows [SemVer](https://semver.org/).
This will install the latest supported version from this branch:

```bash
composer require react/async:^4.3
```

See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.

This project aims to run on any platform and thus does not require any PHP
extensions and supports running on PHP 8.1+.
It's *highly recommended to use the latest supported PHP version* for this project.

We're committed to providing long-term support (LTS) options and to provide a
smooth upgrade path. If you're using an older PHP version, you may use the
[`3.x` branch](https://github.com/reactphp/async/tree/3.x) (PHP 7.1+) or
[`2.x` branch](https://github.com/reactphp/async/tree/2.x) (PHP 5.3+) which both
provide a compatible API but do not take advantage of newer language features.
You may target multiple versions at the same time to support a wider range of
PHP versions like this:

```bash
composer require "react/async:^4 || ^3 || ^2"
```

## Tests

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

```bash
composer install
```

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

```bash
vendor/bin/phpunit
```

On top of this, we use PHPStan on max level to ensure type safety across the project:

```bash
vendor/bin/phpstan
```

## License

MIT, see [LICENSE file](LICENSE).

This project is heavily influenced by [async.js](https://github.com/caolan/async).