2022-04-05 13:48:51 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace Appwrite\GraphQL;
|
|
|
|
|
|
|
|
|
|
use Swoole\Coroutine\Channel;
|
2022-04-06 23:23:20 +00:00
|
|
|
use function Co\go;
|
2022-04-05 13:48:51 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Inspired by https://github.com/streamcommon/promise/blob/master/lib/ExtSwoolePromise.php
|
|
|
|
|
*
|
|
|
|
|
* @package Appwrite\GraphQL
|
|
|
|
|
*/
|
2022-04-08 07:04:15 +00:00
|
|
|
class CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
{
|
|
|
|
|
const STATE_PENDING = 1;
|
|
|
|
|
const STATE_FULFILLED = 0;
|
|
|
|
|
const STATE_REJECTED = -1;
|
|
|
|
|
|
|
|
|
|
protected int $state = self::STATE_PENDING;
|
|
|
|
|
|
|
|
|
|
private mixed $result;
|
|
|
|
|
|
|
|
|
|
public function __construct(?callable $executor = null)
|
|
|
|
|
{
|
|
|
|
|
if (\is_null($executor)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!\extension_loaded('swoole')) {
|
|
|
|
|
throw new \RuntimeException('Swoole ext missing!');
|
|
|
|
|
}
|
|
|
|
|
$resolve = function ($value) {
|
|
|
|
|
$this->setResult($value);
|
|
|
|
|
$this->setState(self::STATE_FULFILLED);
|
|
|
|
|
};
|
|
|
|
|
$reject = function ($value) {
|
|
|
|
|
if ($this->isPending()) {
|
|
|
|
|
$this->setResult($value);
|
|
|
|
|
$this->setState(self::STATE_REJECTED);
|
|
|
|
|
}
|
|
|
|
|
};
|
2022-04-06 23:23:20 +00:00
|
|
|
|
|
|
|
|
go(function (callable $executor, callable $resolve, callable $reject) {
|
2022-04-05 13:48:51 +00:00
|
|
|
try {
|
|
|
|
|
$executor($resolve, $reject);
|
|
|
|
|
} catch (\Throwable $exception) {
|
|
|
|
|
$reject($exception);
|
|
|
|
|
}
|
|
|
|
|
}, $executor, $resolve, $reject);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new promise from the given callable.
|
|
|
|
|
*
|
|
|
|
|
* @param callable $promise
|
2022-04-08 07:04:15 +00:00
|
|
|
* @return CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
*/
|
2022-04-08 07:04:15 +00:00
|
|
|
final public static function create(callable $promise): CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
{
|
|
|
|
|
return new static($promise);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Resolve promise with given value.
|
|
|
|
|
*
|
|
|
|
|
* @param mixed $value
|
2022-04-08 07:04:15 +00:00
|
|
|
* @return CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
*/
|
2022-04-08 07:04:15 +00:00
|
|
|
final public static function resolve(mixed $value): CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
{
|
|
|
|
|
return new static(function (callable $resolve) use ($value) {
|
|
|
|
|
$resolve($value);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Rejects the promise with the given reason.
|
|
|
|
|
*
|
|
|
|
|
* @param mixed $value
|
2022-04-08 07:04:15 +00:00
|
|
|
* @return CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
*/
|
2022-04-08 07:04:15 +00:00
|
|
|
final public static function reject(mixed $value): CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
{
|
|
|
|
|
return new static(function (callable $resolve, callable $reject) use ($value) {
|
|
|
|
|
$reject($value);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Catch any exception thrown by the executor.
|
|
|
|
|
*
|
|
|
|
|
* @param callable $onRejected
|
2022-04-08 07:04:15 +00:00
|
|
|
* @return CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
*/
|
2022-04-08 07:04:15 +00:00
|
|
|
final public function catch(callable $onRejected): CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
{
|
|
|
|
|
return $this->then(null, $onRejected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Execute the promise.
|
|
|
|
|
*
|
|
|
|
|
* @param callable|null $onFulfilled
|
|
|
|
|
* @param callable|null $onRejected
|
2022-04-08 07:04:15 +00:00
|
|
|
* @return CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
*/
|
2022-04-08 07:04:15 +00:00
|
|
|
public function then(?callable $onFulfilled = null, ?callable $onRejected = null): CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
{
|
|
|
|
|
return self::create(function (callable $resolve, callable $reject) use ($onFulfilled, $onRejected) {
|
|
|
|
|
while ($this->isPending()) {
|
|
|
|
|
usleep(25000);
|
|
|
|
|
}
|
|
|
|
|
$callable = $this->isFulfilled() ? $onFulfilled : $onRejected;
|
|
|
|
|
if (!is_callable($callable)) {
|
|
|
|
|
$resolve($this->result);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
$resolve($callable($this->result));
|
|
|
|
|
} catch (\Throwable $error) {
|
|
|
|
|
$reject($error);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns a promise that completes when all passed in promises complete.
|
|
|
|
|
*
|
2022-04-08 07:04:15 +00:00
|
|
|
* @param iterable|CoroutinePromise[] $promises
|
|
|
|
|
* @return CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
*/
|
2022-04-08 07:04:15 +00:00
|
|
|
public static function all(iterable $promises): CoroutinePromise
|
2022-04-05 13:48:51 +00:00
|
|
|
{
|
|
|
|
|
return self::create(function (callable $resolve, callable $reject) use ($promises) {
|
|
|
|
|
$ticks = count($promises);
|
|
|
|
|
|
|
|
|
|
$firstError = null;
|
|
|
|
|
$channel = new Channel($ticks);
|
|
|
|
|
$result = [];
|
|
|
|
|
$key = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($promises as $promise) {
|
2022-04-08 07:04:15 +00:00
|
|
|
if (!$promise instanceof CoroutinePromise) {
|
2022-04-05 13:48:51 +00:00
|
|
|
$channel->close();
|
|
|
|
|
throw new \RuntimeException(
|
|
|
|
|
'Supported only Appwrite\GraphQL\SwoolePromise instance'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
$promise->then(function ($value) use ($key, $result, $channel) {
|
|
|
|
|
$result[$key] = $value;
|
|
|
|
|
$channel->push(true);
|
|
|
|
|
return $value;
|
|
|
|
|
}, function ($error) use ($channel, &$firstError) {
|
|
|
|
|
$channel->push(true);
|
|
|
|
|
if ($firstError === null) {
|
|
|
|
|
$firstError = $error;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
$key++;
|
|
|
|
|
}
|
|
|
|
|
while ($ticks--) {
|
|
|
|
|
$channel->pop();
|
|
|
|
|
}
|
|
|
|
|
$channel->close();
|
|
|
|
|
|
|
|
|
|
if ($firstError !== null) {
|
|
|
|
|
$reject($firstError);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$resolve($result);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set resolved result
|
|
|
|
|
*
|
|
|
|
|
* @param mixed $value
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
private function setResult(mixed $value): void
|
|
|
|
|
{
|
2022-04-08 07:04:15 +00:00
|
|
|
if (!$value instanceof CoroutinePromise) {
|
2022-04-07 12:23:45 +00:00
|
|
|
$this->result = $value;
|
|
|
|
|
return;
|
2022-04-05 13:48:51 +00:00
|
|
|
}
|
2022-04-07 12:23:45 +00:00
|
|
|
|
2022-04-05 13:48:51 +00:00
|
|
|
$resolved = false;
|
|
|
|
|
$callable = function ($value) use (&$resolved) {
|
|
|
|
|
$this->setResult($value);
|
|
|
|
|
$resolved = true;
|
|
|
|
|
};
|
|
|
|
|
$value->then($callable, $callable);
|
|
|
|
|
|
|
|
|
|
while (!$resolved) {
|
|
|
|
|
usleep(25000);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Change promise state
|
|
|
|
|
*
|
|
|
|
|
* @param integer $state
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
final protected function setState(int $state): void
|
|
|
|
|
{
|
|
|
|
|
$this->state = $state;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Promise is pending
|
|
|
|
|
*
|
|
|
|
|
* @return boolean
|
|
|
|
|
*/
|
|
|
|
|
final protected function isPending(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->state == self::STATE_PENDING;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Promise is fulfilled
|
|
|
|
|
*
|
|
|
|
|
* @return boolean
|
|
|
|
|
*/
|
|
|
|
|
final protected function isFulfilled(): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->state == self::STATE_FULFILLED;
|
|
|
|
|
}
|
|
|
|
|
}
|