From c7f64ce2c014e8cd6e38dda6386395fcad72ec02 Mon Sep 17 00:00:00 2001 From: Aaron Piotrowski Date: Sat, 21 May 2016 09:44:52 -0500 Subject: [PATCH] Initial commit --- .gitattributes | 6 + .gitignore | 4 + LICENSE | 22 + composer.json | 37 ++ lib/Coroutine.php | 97 +++++ lib/Deferred.php | 78 ++++ lib/Exception/MultiReasonException.php | 29 ++ lib/Exception/TimeoutException.php | 5 + lib/Failure.php | 44 ++ lib/Future.php | 15 + lib/Internal/Lazy.php | 51 +++ lib/Internal/Placeholder.php | 104 +++++ lib/Internal/WhenQueue.php | 58 +++ lib/Promise.php | 46 +++ lib/Success.php | 44 ++ lib/functions.php | 540 +++++++++++++++++++++++++ 16 files changed, 1180 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 composer.json create mode 100644 lib/Coroutine.php create mode 100644 lib/Deferred.php create mode 100644 lib/Exception/MultiReasonException.php create mode 100644 lib/Exception/TimeoutException.php create mode 100644 lib/Failure.php create mode 100644 lib/Future.php create mode 100644 lib/Internal/Lazy.php create mode 100644 lib/Internal/Placeholder.php create mode 100644 lib/Internal/WhenQueue.php create mode 100644 lib/Promise.php create mode 100644 lib/Success.php create mode 100644 lib/functions.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..29a850a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +example export-ignore +test export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +phpunit.xml.dist export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d92a4ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +build +composer.lock +phpunit.xml +vendor diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c9bb310 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2016 amphp + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..cbbf786 --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "name": "amphp/awaitable", + "description": "", + "keywords": [ + "asynchronous", + "async", + "awaitable", + "coroutine", + "promise", + "future", + "delayed" + ], + "homepage": "http://amphp.org", + "license": "MIT", + "require": { + "async-interop/awaitable": "dev-master", + "async-interop/event-loop": "dev-master", + "async-interop/event-loop-implementation": "dev-master" + }, + "require-dev": { + "amphp/loop": "dev-master" + }, + "minimum-stability": "dev", + "autoload": { + "psr-4": { + "Amp\\Awaitable\\": "lib" + }, + "files": [ + "lib/functions.php" + ] + }, + "autoload-dev": { + "psr-4": { + "Amp\\Test\\Awaitable\\": "test" + } + } +} diff --git a/lib/Coroutine.php b/lib/Coroutine.php new file mode 100644 index 0000000..f693c0d --- /dev/null +++ b/lib/Coroutine.php @@ -0,0 +1,97 @@ +generator = $generator; + + /** + * @param \Throwable|\Exception|null $exception Exception to be thrown into the generator. + * @param mixed $value The value to send to the generator. + */ + $this->when = function ($exception = null, $value = null) { + if (self::MAX_RECURSION_DEPTH < $this->depth) { // Defer continuation to avoid blowing up call stack. + Loop::defer(function () use ($exception, $value) { + $when = $this->when; + $when($exception, $value); + }); + return; + } + + try { + if ($exception) { + // Throw exception at current execution point. + $this->next($this->generator->throw($exception)); + return; + } + + // Send the new value and execute to next yield statement. + $this->next($this->generator->send($value), $value); + } catch (\Throwable $exception) { + $this->fail($exception); + } catch (\Exception $exception) { + $this->fail($exception); + } + }; + + try { + $this->next($this->generator->current()); + } catch (\Throwable $exception) { + $this->fail($exception); + } catch (\Exception $exception) { + $this->fail($exception); + } + } + + /** + * Examines the value yielded from the generator and prepares the next step in iteration. + * + * @param mixed $yielded Value yielded from generator. + * @param mixed $last Prior resolved value. No longer needed when PHP 5.x support is dropped. + */ + private function next($yielded, $last = null) + { + if (!$this->generator->valid()) { + $this->resolve(PHP_MAJOR_VERSION >= 7 ? $this->generator->getReturn() : $last); + return; + } + + ++$this->depth; + + if ($yielded instanceof Awaitable) { + $yielded->when($this->when); + } else { + $this->resolve($yielded); + } + + --$this->depth; + } +} \ No newline at end of file diff --git a/lib/Deferred.php b/lib/Deferred.php new file mode 100644 index 0000000..a4b0709 --- /dev/null +++ b/lib/Deferred.php @@ -0,0 +1,78 @@ +awaitable = new Promise(function (callable $resolve, callable $fail) { + $this->resolve = $resolve; + $this->fail = $fail; + }); + } + + /** + * @return \Interop\Async\Awaitable + */ + public function getAwaitable() { + return $this->awaitable; + } + + /** + * Fulfill the awaitable with the given value. + * + * @param mixed $value + */ + public function resolve($value = null) { + $resolve = $this->resolve; + $resolve($value); + } + + /** + * Fails the awaitable the the given reason. + * + * @param \Throwable|\Exception $reason + */ + public function fail($reason) { + $fail = $this->fail; + $fail($reason); + } + } + } +} catch (\AssertionError $exception) { + goto development; // zend.assertions=1 and assert.exception=1, use development definition. +} diff --git a/lib/Exception/MultiReasonException.php b/lib/Exception/MultiReasonException.php new file mode 100644 index 0000000..5b928ca --- /dev/null +++ b/lib/Exception/MultiReasonException.php @@ -0,0 +1,29 @@ +reasons = $reasons; + } + + /** + * @return \Throwable[]|\Exception[] + */ + public function getReasons() + { + return $this->reasons; + } +} diff --git a/lib/Exception/TimeoutException.php b/lib/Exception/TimeoutException.php new file mode 100644 index 0000000..d06d422 --- /dev/null +++ b/lib/Exception/TimeoutException.php @@ -0,0 +1,5 @@ +exception = $exception; + } + + /** + * {@inheritdoc} + */ + public function when(callable $onResolved) { + try { + $onResolved($this->exception, null); + } catch (\Throwable $exception) { + Loop::defer(static function ($watcher, $exception) { + throw $exception; + }, $exception); + } catch (\Exception $exception) { + Loop::defer(static function ($watcher, $exception) { + throw $exception; + }, $exception); + } + } +} \ No newline at end of file diff --git a/lib/Future.php b/lib/Future.php new file mode 100644 index 0000000..d65944e --- /dev/null +++ b/lib/Future.php @@ -0,0 +1,15 @@ +provider = $provider; + } + + /** + * @return \Interop\Async\Awaitable + */ + protected function getAwaitable() { + if (null === $this->awaitable) { + $provider = $this->provider; + $this->provider = null; + + try { + $this->awaitable = Awaitable\resolve($provider()); + } catch (\Throwable $exception) { + $this->awaitable = Awaitable\fail($exception); + } catch (\Exception $exception) { + $this->awaitable = Awaitable\fail($exception); + } + } + + return $this->awaitable; + } + + /** + * {@inheritdoc} + */ + public function when(callable $onResolved) { + $this->getAwaitable()->when($onResolved); + } +} \ No newline at end of file diff --git a/lib/Internal/Placeholder.php b/lib/Internal/Placeholder.php new file mode 100644 index 0000000..8fdeca9 --- /dev/null +++ b/lib/Internal/Placeholder.php @@ -0,0 +1,104 @@ +resolved) { + if ($this->result instanceof Awaitable) { + $this->result->when($onResolved); + } else { + $this->execute($onResolved); + } + return; + } + + if (null === $this->onResolved) { + $this->onResolved = $onResolved; + } elseif (!$this->onResolved instanceof WhenQueue) { + $this->onResolved = new WhenQueue($this->onResolved); + $this->onResolved->push($onResolved); + } else { + $this->onResolved->push($onResolved); + } + } + + /** + * @param mixed $value + */ + protected function resolve($value = null) { + if ($this->resolved) { + return; + } + + $this->resolved = true; + + if ($value instanceof Awaitable) { + if ($this === $value) { + $value = new Failure( + new \InvalidArgumentException('Cannot resolve an awaitable with itself') + ); + } + + $this->result = $value; + + if (null !== $this->onResolved) { + $this->result->when($this->onResolved); + } + } else { + $this->result = $value; + + if (null !== $this->onResolved) { + $this->execute($this->onResolved); + } + } + + $this->onResolved = null; + } + + /** + * @param \Throwable|\Exception $reason + */ + protected function fail($reason) { + $this->resolve(new Failure($reason)); + } + + /** + * @param callable $onResolved + */ + private function execute(callable $onResolved) { + try { + $onResolved(null, $this->result); + } catch (\Throwable $exception) { + Loop::defer(static function ($watcher, $exception) { + throw $exception; + }, $exception); + } catch (\Exception $exception) { + Loop::defer(static function ($watcher, $exception) { + throw $exception; + }, $exception); + } + } +} diff --git a/lib/Internal/WhenQueue.php b/lib/Internal/WhenQueue.php new file mode 100644 index 0000000..9bde3e8 --- /dev/null +++ b/lib/Internal/WhenQueue.php @@ -0,0 +1,58 @@ +push($callback); + } + } + + /** + * Calls each callback in the queue, passing the provided values to the function. + * + * @param \Throwable|\Exception|null $exception + * @param mixed $value + */ + public function __invoke($exception = null, $value = null) { + foreach ($this->queue as $callback) { + try { + $callback($exception, $value); + } catch (\Throwable $exception) { + Loop::defer(static function ($watcher, $exception) { + throw $exception; + }, $exception); + } catch (\Exception $exception) { + Loop::defer(static function ($watcher, $exception) { + throw $exception; + }, $exception); + } + } + } + + /** + * Unrolls instances of self to avoid blowing up the call stack on resolution. + * + * @param callable $callback + */ + public function push(callable $callback) + { + if ($callback instanceof self) { + $this->queue = \array_merge($this->queue, $callback->queue); + return; + } + + $this->queue[] = $callback; + } +} diff --git a/lib/Promise.php b/lib/Promise.php new file mode 100644 index 0000000..e1d52f3 --- /dev/null +++ b/lib/Promise.php @@ -0,0 +1,46 @@ +resolve($value); + }; + + /** + * Fails the promise with the given exception. + * + * @param \Exception $reason + */ + $fail = function ($reason) { + $this->fail($reason); + }; + + try { + $resolver($resolve, $fail); + } catch (\Throwable $exception) { + $this->fail($exception); + } catch (\Exception $exception) { + $this->fail($exception); + } + } +} diff --git a/lib/Success.php b/lib/Success.php new file mode 100644 index 0000000..15ac1f0 --- /dev/null +++ b/lib/Success.php @@ -0,0 +1,44 @@ +value = $value; + } + + /** + * {@inheritdoc} + */ + public function when(callable $onResolved) { + try { + $onResolved(null, $this->value); + } catch (\Throwable $exception) { + Loop::defer(static function ($watcher, $exception) { + throw $exception; + }, $exception); + } catch (\Exception $exception) { + Loop::defer(static function ($watcher, $exception) { + throw $exception; + }, $exception); + } + } +} \ No newline at end of file diff --git a/lib/functions.php b/lib/functions.php new file mode 100644 index 0000000..48ef6dd --- /dev/null +++ b/lib/functions.php @@ -0,0 +1,540 @@ +when(function ($exception = null, $value = null) { + if ($exception) { + /** @var \Throwable|\Exception $exception */ + throw $exception; + } + }); + } + + /** + * Runs the event loop until the awaitable is resolved. Should not be called within a running event loop. + * + * @param \Interop\Async\Awaitable $awaitable + * @param \Interop\Async\LoopDriver|null $driver + * + * @return mixed Awaitable success value. + * + * @throws \Throwable|\Exception Awaitable failure reason. + */ + function wait(Awaitable $awaitable, LoopDriver $driver = null) { + Loop::execute(function () use (&$value, &$exception, $awaitable) { + $awaitable->when(function ($e = null, $v = null) use (&$value, &$exception) { + Loop::stop(); + $exception = $e; + $value = $v; + }); + }, $driver); + + if ($exception) { + throw $exception; + } + + return $value; + } + + /** + * Pipe the promised value through the specified functor once it resolves. + * + * @param \Interop\Async\Awaitable $awaitable + * @param callable(mixed $value): mixed $functor + * + * @return \Interop\Async\Awaitable + */ + function pipe(Awaitable $awaitable, callable $functor) { + $deferred = new Deferred(); + + $awaitable->when(function ($exception = null, $value = null) use ($deferred, $functor) { + if ($exception) { + $deferred->fail($exception); + return; + } + + try { + $deferred->resolve($functor($value)); + } catch (\Throwable $exception) { + $deferred->fail($exception); + } catch (\Exception $exception) { + $deferred->fail($exception); + } + }); + + return $deferred->getAwaitable(); + } + + /** + * @param \Interop\Async\Awaitable $awaitable + * @param callable(\Throwable|\Exception $exception): mixed $functor + * + * @return \Interop\Async\Awaitable + */ + function capture(Awaitable $awaitable, callable $functor) { + $deferred = new Deferred(); + + $awaitable->when(function ($exception = null, $value = null) use ($deferred, $functor) { + if (!$exception) { + $deferred->resolve($value); + return; + } + + try { + $deferred->resolve($functor($exception)); + } catch (\Throwable $exception) { + $deferred->fail($exception); + } catch (\Exception $exception) { + $deferred->fail($exception); + } + }); + + return $deferred->getAwaitable(); + } + + /** + * Create an artificial timeout for any Awaitable. + * + * If the timeout expires before the awaitable is resolved, the returned awaitable fails with an instance of + * \Amp\Awaitable\Exception\TimeoutException. + * + * @param \Interop\Async\Awaitable $awaitable + * @param int $timeout Timeout in milliseconds. + * + * @return \Interop\Async\Awaitable + */ + function timeout(Awaitable $awaitable, $timeout) { + $deferred = new Deferred(); + + $watcher = Loop::delay(function () use ($deferred) { + $deferred->fail(new Exception\TimeoutException()); + }, $timeout); + + $onResolved = function () use ($awaitable, $deferred, $watcher) { + Loop::cancel($watcher); + $deferred->resolve($awaitable); + }; + + $awaitable->when($onResolved); + + return $deferred->getAwaitable(); + } + + /** + * Artificially delays the success of an awaitable $delay milliseconds after the awaitable succeeds. If the + * awaitable fails, the returned awaitable fails immediately. + * + * @param \Interop\Async\Awaitable $awaitable + * @param int $delay Delay in milliseconds. + * + * @return \Interop\Async\Awaitable + */ + function delay(Awaitable $awaitable, $delay) { + $deferred = new Deferred(); + + $onResolved = function ($exception = null) use ($awaitable, $deferred, $delay) { + if ($exception) { + $deferred->fail($exception); + return; + } + + Loop::delay(function () use ($awaitable, $deferred) { + $deferred->resolve($awaitable); + }, $delay); + }; + + $awaitable->when($onResolved); + + return $deferred->getAwaitable(); + } + + /** + * Returns a awaitable that calls $promisor only when the result of the awaitable is requested (e.g., then() or + * done() is called on the returned awaitable). $promisor can return a awaitable or any value. If $promisor throws + * an exception, the returned awaitable is rejected with that exception. + * + * @param callable $promisor + * @param mixed ...$args + * + * @return \Interop\Async\Awaitable + */ + function lazy(callable $promisor /* ...$args */) + { + $args = \array_slice(\func_get_args(), 1); + + if (empty($args)) { + return new Internal\Lazy($promisor); + } + + return new Internal\Lazy(function () use ($promisor, $args) { + return \call_user_func_array($promisor, $args); + }); + } + + /** + * Transforms a function that takes a callback into a function that returns a awaitable. The awaitable is fulfilled + * with an array of the parameters that would have been passed to the callback function. + * + * @param callable $worker Function that normally accepts a callback. + * @param int $index Position of callback in $worker argument list (0-indexed). + * + * @return callable + */ + function promisify(callable $worker, $index = 0) + { + return function (/* ...$args */) use ($worker, $index) { + $args = \func_get_args(); + + $deferred = new Deferred(); + + $callback = function (/* ...$args */) use ($deferred) { + $deferred->resolve(\func_get_args()); + }; + + if (\count($args) < $index) { + throw new \InvalidArgumentException('Too few arguments given to function'); + } + + \array_splice($args, $index, 0, [$callback]); + + \call_user_func_array($worker, $args); + + return $deferred->getAwaitable(); + }; + } + + /** + * Adapts any object with a then(callable $onFulfilled, callable $onRejected) method to a awaitable usable by + * components depending on placeholders implementing Awaitable. + * + * @param object $thenable Object with a then() method. + * + * @return \Interop\Async\Awaitable Awaitable resolved by the $thenable object. + */ + function adapt($thenable) + { + if (!\is_object($thenable) || !\method_exists($thenable, 'then')) { + return fail(new \InvalidArgumentException('Must provide an object with a then() method')); + } + + return new Promise(function (callable $resolve, callable $fail) use ($thenable) { + $thenable->then($resolve, $fail); + }); + } + + /** + * Wraps the given callable $worker in a awaitable aware function that has the same number of arguments as $worker, + * but those arguments may be awaitables for the future argument value or just values. The returned function will + * return a awaitable for the return value of $worker and will never throw. The $worker function will not be called + * until each awaitable given as an argument is fulfilled. If any awaitable provided as an argument fails, the + * awaitable returned by the returned function will be failed for the same reason. The awaitable succeeds with + * the return value of $worker or failed if $worker throws. + * + * @param callable $worker + * + * @return callable + */ + function lift(callable $worker) + { + /** + * @param mixed ...$args Awaitables or values. + * + * @return \Interop\Async\Awaitable + */ + return function (/* ...$args */) use ($worker) { + $args = \func_get_args(); + + if (1 === \count($args)) { + return pipe(resolve($args[0]), $worker); + } + + return pipe(all($args), function (array $args) use ($worker) { + return \call_user_func_array($worker, $args); + }); + }; + } + + /** + * Returns a awaitable that is resolved when all awaitables are resolved. The returned awaitable will not reject by + * itself (only if cancelled). Returned awaitable succeeds with an array of resolved awaitables, with keys + * identical and corresponding to the original given array. + * + * @param mixed[] $awaitables Awaitables or values (passed through resolve() to create awaitables). + * + * @return \Interop\Async\Awaitable + */ + function settle(array $awaitables) + { + if (empty($awaitables)) { + return resolve([]); + } + + $deferred = new Deferred(); + + $pending = \count($awaitables); + + $onResolved = function () use (&$awaitables, &$pending, $deferred) { + if (0 === --$pending) { + $deferred->resolve($awaitables); + } + }; + + foreach ($awaitables as &$awaitable) { + $awaitable = resolve($awaitable); + $awaitable->when($onResolved); + } + + return $deferred->getAwaitable(); + } + + /** + * Returns a awaitable that succeeds when all awaitables succeed, and fails if any awaitable fails. Returned + * awaitable succeeds with an array of values used to succeed each contained awaitable, with keys corresponding to + * the array of awaitables. + * + * @param mixed[] $awaitables Awaitables or values (passed through resolve() to create awaitables). + * + * @return \Interop\Async\Awaitable + */ + function all(array $awaitables) + { + if (empty($awaitables)) { + return resolve([]); + } + + $deferred = new Deferred(); + + $pending = \count($awaitables); + $values = []; + + foreach ($awaitables as $key => $awaitable) { + $onResolved = function ($exception = null, $value = null) use ($key, &$values, &$pending, $deferred) { + if ($exception) { + $deferred->fail($exception); + return; + } + + $values[$key] = $value; + if (0 === --$pending) { + $deferred->resolve($values); + } + }; + + resolve($awaitable)->when($onResolved); + } + + return $deferred->getAwaitable(); + } + + /** + * Returns a awaitable that succeeds when any awaitable succeeds, and fails only if all awaitables fail. + * + * @param mixed[] $awaitables Awaitables or values (passed through resolve() to create awaitables). + * + * @return \Interop\Async\Awaitable + */ + function any(array $awaitables) + { + if (empty($awaitables)) { + return fail(new \InvalidArgumentException('No awaitables provided')); + } + + $deferred = new Deferred(); + + $pending = \count($awaitables); + $exceptions = []; + + foreach ($awaitables as $key => $awaitable) { + $onResolved = function ($exception = null, $value = null) use ($key, &$exceptions, &$pending, $deferred) { + if (!$exception) { + $deferred->resolve($value); + return; + } + + $exceptions[$key] = $exception; + if (0 === --$pending) { + $deferred->fail(new Exception\MultiReasonException($exceptions)); + } + }; + + resolve($awaitable)->when($onResolved); + } + + return $deferred->getAwaitable(); + } + + /** + * Returns a awaitable that succeeds when $required number of awaitables succeed. The awaitable fails if $required + * number of awaitables can no longer succeed. + * + * @param mixed[] $awaitables Awaitables or values (passed through resolve() to create awaitables). + * @param int $required Number of awaitables that must succeed to succeed the returned awaitable. + * + * @return \Interop\Async\Awaitable + */ + function some(array $awaitables, $required) + { + $required = (int) $required; + + if (0 >= $required) { + return resolve([]); + } + + $pending = \count($awaitables); + + if ($required > $pending) { + return fail(new \InvalidArgumentException('Too few awaitables provided')); + } + + $deferred = new Deferred(); + + $required = \min($pending, $required); + $values = []; + $exceptions = []; + + foreach ($awaitables as $key => $awaitable) { + $onResolved = function ($exception = null, $value = null) use ( + &$key, &$values, &$exceptions, &$pending, &$required, $deferred + ) { + if ($exception) { + $exceptions[$key] = $exception; + if ($required > --$pending) { + $deferred->fail(new Exception\MultiReasonException($exceptions)); + } + return; + } + + $values[$key] = $value; + --$pending; + if (0 === --$required) { + $deferred->resolve($values); + } + }; + + resolve($awaitable)->when($onResolved); + } + + return $deferred->getAwaitable(); + } + + /** + * Returns a awaitable that succeeds or fails when the first awaitable succeeds or fails. + * + * @param mixed[] $awaitables Awaitables or values (passed through resolve() to create awaitables). + * + * @return \Interop\Async\Awaitable + */ + function choose(array $awaitables) + { + if (empty($awaitables)) { + return fail(new \InvalidArgumentException('No awaitables provided')); + } + + $deferred = new Deferred(); + + foreach ($awaitables as $awaitable) { + resolve($awaitable)->when(function ($exception = null, $value = null) use ($deferred) { + if ($exception) { + $deferred->fail($exception); + return; + } + + $deferred->resolve($value); + }); + } + + return $deferred->getAwaitable(); + } + + /** + * Maps the callback to each awaitable as it succeeds. Returns an array of awaitables resolved by the return + * callback value of the callback function. The callback may return awaitables or throw exceptions to fail + * awaitables in the array. If a awaitable in the passed array fails, the callback will not be called and the + * awaitable in the array fails for the same reason. Tip: Use all() or settle() to determine when all + * awaitables in the array have been resolved. + * + * @param callable(mixed $value): mixed $callback + * @param mixed[] ...$awaitables Awaitables or values (passed through resolve() to create awaitables). + * + * @return \Interop\Async\Awaitable[] Array of awaitables resolved with the result of the mapped function. + */ + function map(callable $callback /* array ...$awaitables */) + { + $args = \func_get_args(); + $args[0] = lift($args[0]); + + return \call_user_func_array('array_map', $args); + } +}