mirror of
https://github.com/danog/amp.git
synced 2025-01-22 13:21:16 +01:00
Add support for React promises
This commit is contained in:
parent
fe88413a17
commit
7ad10f5d7d
@ -36,6 +36,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/phpunit-util": "dev-master",
|
||||
"react/promise": "^2",
|
||||
"friendsofphp/php-cs-fixer": "~1.9",
|
||||
"phpunit/phpunit": "^6"
|
||||
},
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Amp;
|
||||
|
||||
use React\Promise\PromiseInterface as ReactPromise;
|
||||
|
||||
/**
|
||||
* Creates a promise from a generator function yielding promises.
|
||||
*
|
||||
@ -55,21 +57,29 @@ final class Coroutine implements Promise {
|
||||
$yielded = $this->generator->send($value);
|
||||
}
|
||||
|
||||
if ($yielded instanceof Promise) {
|
||||
++$this->depth;
|
||||
$yielded->when($this->when);
|
||||
--$this->depth;
|
||||
return;
|
||||
if (!$yielded instanceof Promise) {
|
||||
if (!$this->generator->valid()) {
|
||||
$this->resolve($this->generator->getReturn());
|
||||
return;
|
||||
}
|
||||
|
||||
if ($yielded instanceof ReactPromise) {
|
||||
$yielded = adapt($yielded);
|
||||
} else {
|
||||
throw new InvalidYieldError(
|
||||
$this->generator,
|
||||
\sprintf(
|
||||
"Unexpected yield; Expected an instance of %s or %s",
|
||||
Promise::class,
|
||||
ReactPromise::class
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->generator->valid()) {
|
||||
throw new InvalidYieldError(
|
||||
$this->generator,
|
||||
\sprintf("Unexpected yield; Expected an instance of %s", Promise::class)
|
||||
);
|
||||
}
|
||||
|
||||
$this->resolve($this->generator->getReturn());
|
||||
++$this->depth;
|
||||
$yielded->when($this->when);
|
||||
--$this->depth;
|
||||
} catch (\Throwable $exception) {
|
||||
$this->dispose($exception);
|
||||
}
|
||||
@ -78,21 +88,29 @@ final class Coroutine implements Promise {
|
||||
try {
|
||||
$yielded = $this->generator->current();
|
||||
|
||||
if ($yielded instanceof Promise) {
|
||||
++$this->depth;
|
||||
$yielded->when($this->when);
|
||||
--$this->depth;
|
||||
return;
|
||||
if (!$yielded instanceof Promise) {
|
||||
if (!$this->generator->valid()) {
|
||||
$this->resolve($this->generator->getReturn());
|
||||
return;
|
||||
}
|
||||
|
||||
if ($yielded instanceof ReactPromise) {
|
||||
$yielded = adapt($yielded);
|
||||
} else {
|
||||
throw new InvalidYieldError(
|
||||
$this->generator,
|
||||
\sprintf(
|
||||
"Unexpected yield; Expected an instance of %s or %s",
|
||||
Promise::class,
|
||||
ReactPromise::class
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->generator->valid()) {
|
||||
throw new InvalidYieldError(
|
||||
$this->generator,
|
||||
\sprintf("Unexpected yield; Expected an instance of %s", Promise::class)
|
||||
);
|
||||
}
|
||||
|
||||
$this->resolve($this->generator->getReturn());
|
||||
++$this->depth;
|
||||
$yielded->when($this->when);
|
||||
--$this->depth;
|
||||
} catch (\Throwable $exception) {
|
||||
$this->dispose($exception);
|
||||
}
|
||||
|
@ -5,6 +5,8 @@ namespace Amp\Internal;
|
||||
use Amp\Failure;
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use function Amp\adapt;
|
||||
use React\Promise\PromiseInterface as ReactPromise;
|
||||
|
||||
/**
|
||||
* Trait used by Promise implementations. Do not use this trait in your code, instead compose your class from one of
|
||||
@ -64,6 +66,10 @@ trait Placeholder {
|
||||
throw new \Error("Promise has already been resolved");
|
||||
}
|
||||
|
||||
if ($value instanceof ReactPromise) {
|
||||
$value = adapt($value);
|
||||
}
|
||||
|
||||
$this->resolved = true;
|
||||
$this->result = $value;
|
||||
|
||||
|
@ -6,6 +6,7 @@ use Amp\Deferred;
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use React\Promise\PromiseInterface as ReactPromise;
|
||||
|
||||
/**
|
||||
* Trait used by Stream implementations. Do not use this trait in your code, instead compose your class from one of
|
||||
@ -49,6 +50,10 @@ trait Producer {
|
||||
throw new \Error("Streams cannot emit values after calling resolve");
|
||||
}
|
||||
|
||||
if ($value instanceof ReactPromise) {
|
||||
$value = adapt($value);
|
||||
}
|
||||
|
||||
if ($value instanceof Promise) {
|
||||
$deferred = new Deferred;
|
||||
$value->when(function ($e, $v) use ($deferred) {
|
||||
@ -76,6 +81,9 @@ trait Producer {
|
||||
foreach ($this->listeners as $onNext) {
|
||||
try {
|
||||
$result = $onNext($value);
|
||||
if ($result instanceof ReactPromise) {
|
||||
$result = adapt($result);
|
||||
}
|
||||
if ($result instanceof Promise) {
|
||||
$promises[] = $result;
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Amp;
|
||||
|
||||
use React\Promise\PromiseInterface as ReactPromise;
|
||||
|
||||
/**
|
||||
* Creates a promise that calls $promisor only when the result of the promise is requested (i.e. when() is called on
|
||||
* the promise). $promisor can return a promise or any value. If $promisor throws an exception, the promise fails with
|
||||
@ -32,6 +34,10 @@ class LazyPromise implements Promise {
|
||||
try {
|
||||
$this->promise = $provider();
|
||||
|
||||
if ($this->promise instanceof ReactPromise) {
|
||||
$this->promise = adapt($this->promise);
|
||||
}
|
||||
|
||||
if (!$this->promise instanceof Promise) {
|
||||
$this->promise = new Success($this->promise);
|
||||
}
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Amp;
|
||||
|
||||
use React\Promise\PromiseInterface as ReactPromise;
|
||||
|
||||
/**
|
||||
* Creates a successful stream (which is also a promise) using the given value (which can be any value except another
|
||||
* object implementing \Amp\Promise).
|
||||
@ -16,7 +18,7 @@ final class Success implements Stream {
|
||||
* @throws \Error If a promise is given as the value.
|
||||
*/
|
||||
public function __construct($value = null) {
|
||||
if ($value instanceof Promise) {
|
||||
if ($value instanceof Promise || $value instanceof ReactPromise) {
|
||||
throw new \Error("Cannot use a promise as success value");
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
namespace Amp;
|
||||
|
||||
use React\Promise\PromiseInterface as ReactPromise;
|
||||
|
||||
/**
|
||||
* Wraps the callback in a promise/coroutine-aware function that automatically upgrades Generators to coroutines and
|
||||
* calls rethrow() on the returned promises (or the coroutine created).
|
||||
@ -18,6 +20,10 @@ function wrap(callable $callback): callable {
|
||||
$result = new Coroutine($result);
|
||||
}
|
||||
|
||||
if ($result instanceof ReactPromise) {
|
||||
$result = adapt($result);
|
||||
}
|
||||
|
||||
if ($result instanceof Promise) {
|
||||
rethrow($result);
|
||||
}
|
||||
@ -45,6 +51,10 @@ function coroutine(callable $worker): callable {
|
||||
return new Coroutine($result);
|
||||
}
|
||||
|
||||
if ($result instanceof ReactPromise) {
|
||||
$result = adapt($result);
|
||||
}
|
||||
|
||||
if (!$result instanceof Promise) {
|
||||
return new Success($result);
|
||||
}
|
||||
@ -60,7 +70,7 @@ function coroutine(callable $worker): callable {
|
||||
* @param callable(mixed ...$args): mixed $functor
|
||||
* @param array ...$args Arguments to pass to the function.
|
||||
*
|
||||
* @return \Amp\Promise
|
||||
* @return \AsyncInterop\Promise
|
||||
*/
|
||||
function call(callable $functor, ...$args): Promise {
|
||||
try {
|
||||
@ -73,6 +83,10 @@ function call(callable $functor, ...$args): Promise {
|
||||
return new Coroutine($result);
|
||||
}
|
||||
|
||||
if ($result instanceof ReactPromise) {
|
||||
$result = adapt($result);
|
||||
}
|
||||
|
||||
if (!$result instanceof Promise) {
|
||||
return new Success($result);
|
||||
}
|
||||
@ -221,19 +235,26 @@ function timeout(Promise $promise, int $timeout): Promise {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts any object with a then(callable $onFulfilled, callable $onRejected) method to a promise usable by
|
||||
* components depending on placeholders implementing Promise.
|
||||
* Adapts any object with a done(callable $onFulfilled, callable $onRejected) or then(callable $onFulfilled,
|
||||
* callable $onRejected) method to a promise usable by components depending on placeholders implementing
|
||||
* \AsyncInterop\Promise.
|
||||
*
|
||||
* @param object $thenable Object with a then() method.
|
||||
* @param object $promise Object with a done() or then() method.
|
||||
*
|
||||
* @return \Amp\Promise Promise resolved by the $thenable object.
|
||||
*
|
||||
* @throws \Error If the provided object does not have a then() method.
|
||||
*/
|
||||
function adapt($thenable): Promise {
|
||||
function adapt($promise): Promise {
|
||||
$deferred = new Deferred;
|
||||
|
||||
$thenable->then([$deferred, 'resolve'], [$deferred, 'fail']);
|
||||
if (\method_exists($promise, 'done')) {
|
||||
$promise->done([$deferred, 'resolve'], [$deferred, 'fail']);
|
||||
} elseif (\method_exists($promise, 'then')) {
|
||||
$promise->then([$deferred, 'resolve'], [$deferred, 'fail']);
|
||||
} else {
|
||||
throw new \Error("Object must have a 'then' or 'done' method");
|
||||
}
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
@ -259,7 +280,11 @@ function lift(callable $worker): callable {
|
||||
return function (...$args) use ($worker): Promise {
|
||||
foreach ($args as $key => $arg) {
|
||||
if (!$arg instanceof Promise) {
|
||||
$args[$key] = new Success($arg);
|
||||
if ($arg instanceof ReactPromise) {
|
||||
$args[$key] = adapt($arg);
|
||||
} else {
|
||||
$args[$key] = new Success($arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -300,7 +325,9 @@ function any(array $promises): Promise {
|
||||
$values = [];
|
||||
|
||||
foreach ($promises as $key => $promise) {
|
||||
if (!$promise instanceof Promise) {
|
||||
if ($promise instanceof ReactPromise) {
|
||||
$promise = adapt($promise);
|
||||
} elseif (!$promise instanceof Promise) {
|
||||
throw new \Error("Non-promise provided");
|
||||
}
|
||||
|
||||
@ -342,7 +369,9 @@ function all(array $promises): Promise {
|
||||
$values = [];
|
||||
|
||||
foreach ($promises as $key => $promise) {
|
||||
if (!$promise instanceof Promise) {
|
||||
if ($promise instanceof ReactPromise) {
|
||||
$promise = adapt($promise);
|
||||
} elseif (!$promise instanceof Promise) {
|
||||
throw new \Error("Non-promise provided");
|
||||
}
|
||||
|
||||
@ -388,7 +417,9 @@ function first(array $promises): Promise {
|
||||
$exceptions = [];
|
||||
|
||||
foreach ($promises as $key => $promise) {
|
||||
if (!$promise instanceof Promise) {
|
||||
if ($promise instanceof ReactPromise) {
|
||||
$promise = adapt($promise);
|
||||
} elseif (!$promise instanceof Promise) {
|
||||
throw new \Error("Non-promise provided");
|
||||
}
|
||||
|
||||
@ -434,7 +465,9 @@ function some(array $promises): Promise {
|
||||
$exceptions = [];
|
||||
|
||||
foreach ($promises as $key => $promise) {
|
||||
if (!$promise instanceof Promise) {
|
||||
if ($promise instanceof ReactPromise) {
|
||||
$promise = adapt($promise);
|
||||
} elseif (!$promise instanceof Promise) {
|
||||
throw new \Error("Non-promise provided");
|
||||
}
|
||||
|
||||
@ -474,7 +507,7 @@ function some(array $promises): Promise {
|
||||
function map(callable $callback, array ...$promises): array {
|
||||
foreach ($promises as $promiseSet) {
|
||||
foreach ($promiseSet as $promise) {
|
||||
if (!$promise instanceof Promise) {
|
||||
if (!$promise instanceof Promise && !$promise instanceof ReactPromise) {
|
||||
throw new \Error("Non-promise provided");
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ use Amp\Loop;
|
||||
use Amp\Pause;
|
||||
use Amp\Success;
|
||||
use Amp\Promise;
|
||||
use React\Promise\Promise as ReactPromise;
|
||||
|
||||
class CoroutineTest extends \PHPUnit\Framework\TestCase {
|
||||
const TIMEOUT = 100;
|
||||
@ -607,4 +608,134 @@ class CoroutineTest extends \PHPUnit\Framework\TestCase {
|
||||
$this->assertNull($reason);
|
||||
$this->assertSame($value, $result);
|
||||
}
|
||||
|
||||
public function testYieldingFulfilledReactPromise() {
|
||||
$value = 1;
|
||||
$promise = new ReactPromise(function ($resolve, $reject) use ($value) {
|
||||
$resolve($value);
|
||||
});
|
||||
|
||||
$generator = function () use ($promise) {
|
||||
return yield $promise;
|
||||
};
|
||||
|
||||
$coroutine = new Coroutine($generator());
|
||||
|
||||
$coroutine->when(function ($exception, $value) use (&$reason, &$result) {
|
||||
$reason = $exception;
|
||||
$result = $value;
|
||||
});
|
||||
|
||||
$this->assertNull($reason);
|
||||
$this->assertSame($value, $result);
|
||||
}
|
||||
|
||||
public function testYieldingFulfilledReactPromiseAfterInteropPromise() {
|
||||
$value = 1;
|
||||
$promise = new ReactPromise(function ($resolve, $reject) use ($value) {
|
||||
$resolve($value);
|
||||
});
|
||||
|
||||
$generator = function () use ($promise) {
|
||||
$value = yield new Success(-1);
|
||||
return yield $promise;
|
||||
};
|
||||
|
||||
$coroutine = new Coroutine($generator());
|
||||
|
||||
$coroutine->when(function ($exception, $value) use (&$reason, &$result) {
|
||||
$reason = $exception;
|
||||
$result = $value;
|
||||
});
|
||||
|
||||
$this->assertNull($reason);
|
||||
$this->assertSame($value, $result);
|
||||
}
|
||||
|
||||
public function testYieldingRejectedReactPromise() {
|
||||
$exception = new \Exception;
|
||||
$promise = new ReactPromise(function ($resolve, $reject) use ($exception) {
|
||||
$reject($exception);
|
||||
});
|
||||
|
||||
$generator = function () use ($promise) {
|
||||
return yield $promise;
|
||||
};
|
||||
|
||||
$coroutine = new Coroutine($generator());
|
||||
|
||||
$coroutine->when(function ($exception, $value) use (&$reason, &$result) {
|
||||
$reason = $exception;
|
||||
$result = $value;
|
||||
});
|
||||
|
||||
$this->assertSame($reason, $exception);
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testYieldingRejectedReactPromiseAfterInteropPromise() {
|
||||
$exception = new \Exception;
|
||||
$promise = new ReactPromise(function ($resolve, $reject) use ($exception) {
|
||||
$reject($exception);
|
||||
});
|
||||
|
||||
$generator = function () use ($promise) {
|
||||
$value = yield new Success(-1);
|
||||
return yield $promise;
|
||||
};
|
||||
|
||||
$coroutine = new Coroutine($generator());
|
||||
|
||||
$coroutine->when(function ($exception, $value) use (&$reason, &$result) {
|
||||
$reason = $exception;
|
||||
$result = $value;
|
||||
});
|
||||
|
||||
$this->assertSame($reason, $exception);
|
||||
$this->assertNull($result);
|
||||
}
|
||||
|
||||
public function testReturnFulfilledReactPromise() {
|
||||
$value = 1;
|
||||
$promise = new ReactPromise(function ($resolve, $reject) use ($value) {
|
||||
$resolve($value);
|
||||
});
|
||||
|
||||
$generator = function () use ($promise) {
|
||||
return $promise;
|
||||
yield; // Unreachable, but makes function a generator.
|
||||
};
|
||||
|
||||
$coroutine = new Coroutine($generator());
|
||||
|
||||
$coroutine->when(function ($exception, $value) use (&$reason, &$result) {
|
||||
$reason = $exception;
|
||||
$result = $value;
|
||||
});
|
||||
|
||||
$this->assertNull($reason);
|
||||
$this->assertSame($value, $result);
|
||||
}
|
||||
|
||||
public function testReturningRejectedReactPromise() {
|
||||
$exception = new \Exception;
|
||||
$promise = new ReactPromise(function ($resolve, $reject) use ($exception) {
|
||||
$reject($exception);
|
||||
});
|
||||
|
||||
$generator = function () use ($promise) {
|
||||
return $promise;
|
||||
yield; // Unreachable, but makes function a generator.
|
||||
};
|
||||
|
||||
$coroutine = new Coroutine($generator());
|
||||
|
||||
$coroutine->when(function ($exception, $value) use (&$reason, &$result) {
|
||||
$reason = $exception;
|
||||
$result = $value;
|
||||
});
|
||||
|
||||
$this->assertSame($reason, $exception);
|
||||
$this->assertNull($result);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user