293 lines
10 KiB
PHP
293 lines
10 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Coroutine (modified version of AMP Coroutine).
|
|
*
|
|
* The MIT License (MIT)
|
|
*
|
|
* 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.
|
|
*
|
|
* @copyright 2015-2018 amphp
|
|
* @copyright 2016 PHP Asynchronous Interoperability Group
|
|
* @license https://opensource.org/licenses/MIT MIT
|
|
*/
|
|
|
|
namespace danog\MadelineProto;
|
|
|
|
use Amp\Failure;
|
|
use Amp\Internal;
|
|
use Amp\Promise;
|
|
use Amp\Success;
|
|
use JsonSerializable;
|
|
use ReflectionGenerator;
|
|
|
|
/**
|
|
* Creates a promise from a generator function yielding promises.
|
|
*
|
|
* When a promise is yielded, execution of the generator is interrupted until the promise is resolved. A success
|
|
* value is sent into the generator, while a failure reason is thrown into the generator. Using a coroutine,
|
|
* asynchronous code can be written without callbacks and be structured like synchronous code.
|
|
*/
|
|
final class Coroutine implements Promise, \ArrayAccess, JsonSerializable
|
|
{
|
|
use Internal\Placeholder {
|
|
fail as internalFail;
|
|
}
|
|
/** @var \Generator */
|
|
private $generator;
|
|
/** @var callable(\Throwable|null $exception, mixed $value): void */
|
|
private $onResolve;
|
|
/** @var bool Used to control iterative coroutine continuation. */
|
|
private $immediate = true;
|
|
/** @var \Throwable|null Promise failure reason when executing next coroutine step, null at all other times. */
|
|
private $exception;
|
|
/** @var mixed Promise success value when executing next coroutine step, null at all other times. */
|
|
private $value;
|
|
/** @var ?self Reference to coroutine that started this coroutine */
|
|
private $parentCoroutine;
|
|
/**
|
|
* @param \Generator $generator
|
|
*/
|
|
public function __construct(\Generator $generator)
|
|
{
|
|
$this->generator = $generator;
|
|
try {
|
|
$yielded = $this->generator->current();
|
|
while (!$yielded instanceof Promise) {
|
|
if ($yielded instanceof \YieldReturnValue) {
|
|
$this->resolve($yielded->getReturn());
|
|
$this->generator->next();
|
|
return;
|
|
}
|
|
if (!$this->generator->valid()) {
|
|
if (PHP_MAJOR_VERSION >= 7) {
|
|
$this->resolve($this->generator->getReturn());
|
|
} else {
|
|
$this->resolve(null);
|
|
}
|
|
return;
|
|
}
|
|
if ($yielded instanceof \Generator) {
|
|
$yielded = new self($yielded);
|
|
} else {
|
|
$yielded = $this->generator->send($yielded);
|
|
}
|
|
}
|
|
if ($yielded instanceof self) {
|
|
$yielded->parentCoroutine = $this;
|
|
}
|
|
} catch (\Throwable $exception) {
|
|
$this->fail($exception);
|
|
return;
|
|
}
|
|
/*
|
|
* @param \Throwable|null $exception Exception to be thrown into the generator.
|
|
* @param mixed $value Value to be sent into the generator.
|
|
*/
|
|
$this->onResolve = function ($exception, $value): void {
|
|
$this->exception = $exception;
|
|
$this->value = $value;
|
|
if (!$this->immediate) {
|
|
$this->immediate = true;
|
|
return;
|
|
}
|
|
try {
|
|
do {
|
|
if ($this->exception) {
|
|
// Throw exception at current execution point.
|
|
$yielded = $this->throw($this->exception);
|
|
} else {
|
|
// Send the new value and execute to next yield statement.
|
|
$yielded = $this->generator->send($this->value);
|
|
}
|
|
while (!$yielded instanceof Promise) {
|
|
if ($yielded instanceof \YieldReturnValue) {
|
|
$this->resolve($yielded->getReturn());
|
|
$this->onResolve = null;
|
|
$this->generator->next();
|
|
return;
|
|
}
|
|
if (!$this->generator->valid()) {
|
|
if (PHP_MAJOR_VERSION >= 7) {
|
|
$this->resolve($this->generator->getReturn());
|
|
} else {
|
|
$this->resolve(null);
|
|
}
|
|
$this->onResolve = null;
|
|
return;
|
|
}
|
|
if ($yielded instanceof \Generator) {
|
|
$yielded = new self($yielded);
|
|
} else {
|
|
$yielded = $this->generator->send($yielded);
|
|
}
|
|
}
|
|
if ($yielded instanceof self) {
|
|
$yielded->parentCoroutine = $this;
|
|
}
|
|
$this->immediate = false;
|
|
$yielded->onResolve($this->onResolve);
|
|
} while ($this->immediate);
|
|
$this->immediate = true;
|
|
} catch (\Throwable $exception) {
|
|
$this->fail($exception);
|
|
$this->onResolve = null;
|
|
} finally {
|
|
$this->exception = null;
|
|
$this->value = null;
|
|
}
|
|
};
|
|
$yielded->onResolve($this->onResolve);
|
|
}
|
|
/**
|
|
* Throw exception into the generator.
|
|
*
|
|
* @param \Throwable $reason Exception
|
|
*
|
|
* @internal
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function throw(\Throwable $reason)
|
|
{
|
|
if (!isset($reason->yieldedFrames)) {
|
|
if (\method_exists($reason, 'updateTLTrace')) {
|
|
$reason->updateTLTrace($this->getTrace());
|
|
} else {
|
|
$reason->yieldedFrames = $this->getTrace();
|
|
}
|
|
}
|
|
return $this->generator->throw($reason);
|
|
}
|
|
/**
|
|
* @param \Throwable $reason Failure reason.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function fail(\Throwable $reason): void
|
|
{
|
|
$this->resolve(new Failure($reason));
|
|
}
|
|
public function offsetExists($offset): bool
|
|
{
|
|
throw new Exception('Not supported!');
|
|
}
|
|
/**
|
|
* Get data at an array offset asynchronously.
|
|
*
|
|
* @param mixed $offset Offset
|
|
*
|
|
* @return Promise
|
|
*/
|
|
public function offsetGet($offset)
|
|
{
|
|
return Tools::call((function () use ($offset): \Generator {
|
|
$result = yield $this;
|
|
return $result[$offset];
|
|
})());
|
|
}
|
|
public function offsetSet($offset, $value): Promise
|
|
{
|
|
return Tools::call((function () use ($offset, $value): \Generator {
|
|
$result = yield $this;
|
|
if ($offset === null) {
|
|
return $result[] = $value;
|
|
}
|
|
return $result[$offset] = $value;
|
|
})());
|
|
}
|
|
public function offsetUnset($offset): Promise
|
|
{
|
|
return Tools::call((function () use ($offset): \Generator {
|
|
$result = yield $this;
|
|
unset($result[$offset]);
|
|
})());
|
|
}
|
|
/**
|
|
* Get an attribute asynchronously.
|
|
*
|
|
* @param string $offset Offset
|
|
*
|
|
* @return Promise
|
|
*/
|
|
public function __get(string $offset)
|
|
{
|
|
return Tools::call((function () use ($offset): \Generator {
|
|
$result = yield $this;
|
|
return $result->{$offset};
|
|
})());
|
|
}
|
|
public function __call(string $name, array $arguments)
|
|
{
|
|
return Tools::call((function () use ($name, $arguments): \Generator {
|
|
$result = yield $this;
|
|
return $result->{$name}(...$arguments);
|
|
})());
|
|
}
|
|
/**
|
|
* Get current stack trace for running coroutine.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getTrace(): array
|
|
{
|
|
$frames = [];
|
|
try {
|
|
$reflector = new ReflectionGenerator($this->generator);
|
|
$frames = $reflector->getTrace();
|
|
$frames[] = \array_merge($this->parentCoroutine ? $this->parentCoroutine->getFrame() : [], ['function' => $reflector->getFunction()->getName(), 'args' => []]);
|
|
} catch (\Throwable $e) {
|
|
}
|
|
if ($this->parentCoroutine) {
|
|
$frames = \array_merge($frames, $this->parentCoroutine->getTrace());
|
|
}
|
|
return $frames;
|
|
}
|
|
/**
|
|
* Get current execution frame.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getFrame(): array
|
|
{
|
|
try {
|
|
$reflector = new ReflectionGenerator($this->generator);
|
|
return ['file' => $reflector->getExecutingFile(), 'line' => $reflector->getExecutingLine()];
|
|
} catch (\Throwable $e) {
|
|
}
|
|
return [];
|
|
}
|
|
|
|
private const WARNING = 'To obtain a result from a Coroutine object, yield the result or disable async (not recommended). See https://docs.madelineproto.xyz/docs/ASYNC.html for more information on async.';
|
|
public function __debugInfo()
|
|
{
|
|
return [self::WARNING];
|
|
}
|
|
|
|
/**
|
|
* Obtain.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function jsonSerialize(): string
|
|
{
|
|
return self::WARNING;
|
|
}
|
|
}
|