1
0
mirror of https://github.com/danog/amp.git synced 2024-12-11 08:59:46 +01:00
amp/lib/Internal/EmitSource.php

392 lines
10 KiB
PHP
Raw Normal View History

2020-05-13 17:15:21 +02:00
<?php
namespace Amp\Internal;
use Amp\Deferred;
use Amp\DisposedException;
2020-07-29 17:29:57 +02:00
use Amp\Loop;
2020-08-23 16:18:28 +02:00
use Amp\Pipeline;
2020-05-13 17:15:21 +02:00
use Amp\Promise;
use React\Promise\PromiseInterface as ReactPromise;
2020-09-25 05:14:58 +02:00
use function Amp\await;
2020-05-13 17:15:21 +02:00
/**
2020-08-23 16:18:28 +02:00
* Class used internally by {@see Pipeline} implementations. Do not use this class in your code, instead compose your
* class from one of the available classes implementing {@see Pipeline}.
2020-05-13 17:15:21 +02:00
*
* @internal
*
* @template TValue
* @template TSend
*/
2020-05-28 19:59:55 +02:00
final class EmitSource
2020-05-13 17:15:21 +02:00
{
2020-09-25 05:14:58 +02:00
private ?Promise $result = null;
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private bool $completed = false;
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private array $emittedValues = [];
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private array $sendValues = [];
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private array $backPressure = [];
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private array $waiting = [];
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private int $consumePosition = 0;
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private int $emitPosition = 0;
2020-05-13 17:15:21 +02:00
2020-09-25 05:14:58 +02:00
private ?array $resolutionTrace = null;
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private bool $disposed = false;
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private bool $used = false;
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
private ?array $onDisposal = [];
2020-07-29 17:29:57 +02:00
2020-05-13 17:15:21 +02:00
/**
2020-09-25 05:14:58 +02:00
* @psalm-return TValue
2020-05-13 17:15:21 +02:00
*/
2020-09-25 05:14:58 +02:00
public function continue(): mixed
2020-05-13 17:15:21 +02:00
{
return $this->next(Promise\succeed());
2020-05-13 17:15:21 +02:00
}
/**
* @psalm-param TSend $value
*
2020-05-21 17:11:22 +02:00
* @psalm-return Promise<TValue|null>
2020-05-13 17:15:21 +02:00
*/
2020-09-25 05:14:58 +02:00
public function send(mixed $value): mixed
2020-05-13 17:15:21 +02:00
{
if ($this->consumePosition === 0) {
throw new \Error("Must initialize async generator by calling continue() first");
}
return $this->next(Promise\succeed($value));
2020-05-13 17:15:21 +02:00
}
/**
2020-09-25 05:14:58 +02:00
* @psalm-return mixed
2020-05-13 17:15:21 +02:00
*/
2020-09-25 05:14:58 +02:00
public function throw(\Throwable $exception): mixed
2020-05-13 17:15:21 +02:00
{
if ($this->consumePosition === 0) {
throw new \Error("Must initialize async generator by calling continue() first");
}
return $this->next(Promise\fail($exception));
2020-05-13 17:15:21 +02:00
}
/**
2020-05-18 20:49:56 +02:00
* @psalm-param Promise<TSend|null> $promise
2020-05-13 17:15:21 +02:00
*
2020-09-25 05:14:58 +02:00
* @psalm-return TValue
2020-05-13 17:15:21 +02:00
*/
2020-09-25 05:14:58 +02:00
private function next(Promise $promise): mixed
2020-05-13 17:15:21 +02:00
{
$position = $this->consumePosition++;
if (isset($this->backPressure[$position - 1])) {
$deferred = $this->backPressure[$position - 1];
unset($this->backPressure[$position - 1]);
$deferred->resolve($promise);
} elseif ($position > 0) {
// Send-values are indexed as $this->consumePosition - 1.
$this->sendValues[$position - 1] = $promise;
}
2020-05-28 19:59:55 +02:00
if (\array_key_exists($position, $this->emittedValues)) {
$value = $this->emittedValues[$position];
unset($this->emittedValues[$position]);
2020-05-13 17:15:21 +02:00
2020-09-25 05:14:58 +02:00
return await(Promise\succeed($value));
2020-05-13 17:15:21 +02:00
}
if ($this->result) {
2020-09-25 05:14:58 +02:00
return await($this->result);
2020-05-13 17:15:21 +02:00
}
$this->waiting[$position] = $deferred = new Deferred;
2020-09-25 05:14:58 +02:00
return await($deferred->promise());
2020-05-13 17:15:21 +02:00
}
2020-08-23 16:18:28 +02:00
public function pipe(): Pipeline
2020-05-13 17:15:21 +02:00
{
if ($this->used) {
2020-08-23 16:18:28 +02:00
throw new \Error("A pipeline may be started only once");
2020-05-13 17:15:21 +02:00
}
$this->used = true;
2020-08-23 16:18:28 +02:00
return new AutoDisposingPipeline($this);
2020-05-13 17:15:21 +02:00
}
/**
* @return void
*
2020-08-23 16:18:28 +02:00
* @see Pipeline::dispose()
*/
2020-09-24 18:52:22 +02:00
public function dispose(): void
2020-05-13 17:15:21 +02:00
{
$this->cancel(true);
}
2020-05-13 17:15:21 +02:00
2020-09-24 18:52:22 +02:00
public function destroy(): void
{
$this->cancel(false);
}
2020-09-24 18:52:22 +02:00
private function cancel(bool $cancelPending): void
{
try {
if ($this->result) {
return; // Pipeline already completed or failed.
}
$this->finalize(Promise\fail(new DisposedException), true);
} finally {
if ($this->disposed && $cancelPending) {
$this->triggerDisposal();
}
}
2020-05-13 17:15:21 +02:00
}
2020-07-29 17:29:57 +02:00
/**
* @param callable():void $onDispose
*
* @return void
*
2020-08-23 16:18:28 +02:00
* @see Pipeline::onDisposal()
2020-07-29 17:29:57 +02:00
*/
2020-09-24 18:52:22 +02:00
public function onDisposal(callable $onDisposal): void
2020-07-29 17:29:57 +02:00
{
if ($this->disposed) {
try {
$onDisposal();
} catch (\Throwable $e) {
Loop::defer(static function () use ($e) {
throw $e;
});
2020-07-29 17:29:57 +02:00
}
return;
}
if ($this->result) {
return;
}
2020-07-29 17:29:57 +02:00
$this->onDisposal[] = $onDisposal;
}
2020-05-13 17:15:21 +02:00
/**
2020-08-23 16:18:28 +02:00
* Emits a value from the pipeline. The returned promise is resolved once the emitted value has been consumed or
* if the pipeline is completed, failed, or disposed.
2020-05-13 17:15:21 +02:00
*
* @param mixed $value
*
* @psalm-param TValue $value
*
2020-05-18 20:49:56 +02:00
* @return Promise<mixed> Resolves with the sent value once the value has been consumed. Fails with the failure
2020-08-23 16:18:28 +02:00
* reason if the {@see fail()} is called, or with {@see DisposedException} if the pipeline
2020-05-18 20:49:56 +02:00
* is destroyed.
2020-05-13 17:15:21 +02:00
*
2020-05-18 20:49:56 +02:00
* @psalm-return Promise<TSend|null>
2020-05-13 17:15:21 +02:00
*
2020-08-23 16:18:28 +02:00
* @throws \Error If the pipeline has completed.
2020-05-13 17:15:21 +02:00
*/
2020-09-24 18:52:22 +02:00
public function emit(mixed $value): Promise
2020-05-13 17:15:21 +02:00
{
2020-05-21 17:11:22 +02:00
if ($value === null) {
2020-08-23 16:18:28 +02:00
throw new \TypeError("Pipelines cannot emit NULL");
}
2020-05-13 17:15:21 +02:00
if ($value instanceof Promise || $value instanceof ReactPromise) {
2020-08-23 16:18:28 +02:00
throw new \TypeError("Pipelines cannot emit promises");
2020-05-13 17:15:21 +02:00
}
2020-05-28 19:59:55 +02:00
$position = $this->emitPosition++;
2020-05-13 17:15:21 +02:00
if (isset($this->waiting[$position])) {
$deferred = $this->waiting[$position];
unset($this->waiting[$position]);
2020-05-21 17:11:22 +02:00
$deferred->resolve($value);
2020-05-13 17:15:21 +02:00
// Send-values are indexed as $this->consumePosition - 1, so use $position for the next value.
if (isset($this->sendValues[$position])) {
$promise = $this->sendValues[$position];
unset($this->sendValues[$position]);
return $promise;
}
} elseif ($this->result) {
if ($this->completed) {
throw new \Error("Pipelines cannot emit values after calling complete");
}
return $this->result;
2020-05-13 17:15:21 +02:00
} else {
2020-05-28 19:59:55 +02:00
$this->emittedValues[$position] = $value;
2020-05-13 17:15:21 +02:00
}
if ($this->disposed) {
if (empty($this->waiting)) {
$this->triggerDisposal();
}
return Promise\succeed();
}
2020-05-13 17:15:21 +02:00
$this->backPressure[$position] = $deferred = new Deferred;
return $deferred->promise();
}
/**
2020-08-23 16:18:28 +02:00
* @return bool True if the pipeline has been completed or failed.
*/
public function isComplete(): bool
{
return $this->completed;
}
2020-07-17 18:22:13 +02:00
/**
2020-08-23 16:18:28 +02:00
* @return bool True if the pipeline was disposed.
2020-07-17 18:22:13 +02:00
*/
public function isDisposed(): bool
{
return $this->disposed && empty($this->waiting);
2020-07-17 18:22:13 +02:00
}
2020-05-13 17:15:21 +02:00
/**
2020-08-23 16:18:28 +02:00
* Completes the pipeline.
*
2020-05-13 17:15:21 +02:00
* @return void
*
* @throws \Error If the iterator has already been completed.
*/
2020-09-24 18:52:22 +02:00
public function complete(): void
2020-05-13 17:15:21 +02:00
{
$this->finalize(Promise\succeed());
2020-05-13 17:15:21 +02:00
}
/**
2020-08-23 16:18:28 +02:00
* Fails the pipeline.
2020-05-13 17:15:21 +02:00
*
* @param \Throwable $exception
*
* @return void
*/
2020-09-24 18:52:22 +02:00
public function fail(\Throwable $exception): void
2020-05-13 17:15:21 +02:00
{
if ($exception instanceof DisposedException) {
2020-08-23 16:18:28 +02:00
throw new \Error("Cannot fail a pipeline with an instance of " . DisposedException::class);
}
$this->finalize(Promise\fail($exception));
2020-05-13 17:15:21 +02:00
}
/**
* @param Promise $result
* @param bool $disposed Flag if the generator was disposed.
2020-05-13 17:15:21 +02:00
*
* @return void
*/
2020-09-24 18:52:22 +02:00
private function finalize(Promise $result, bool $disposed = false): void
2020-05-13 17:15:21 +02:00
{
if ($this->completed) {
2020-08-23 16:18:28 +02:00
$message = "Pipeline has already been completed";
2020-05-13 17:15:21 +02:00
if (isset($this->resolutionTrace)) {
$trace = formatStacktrace($this->resolutionTrace);
$message .= ". Previous completion trace:\n\n{$trace}\n\n";
} else {
// @codeCoverageIgnoreStart
$message .= ", define environment variable AMP_DEBUG or const AMP_DEBUG = true and enable assertions "
. "for a stacktrace of the previous resolution.";
// @codeCoverageIgnoreEnd
}
throw new \Error($message);
}
$this->completed = $this->completed ?: !$disposed; // $disposed is false if complete() or fail() invoked
2020-07-29 17:29:57 +02:00
$this->disposed = $this->disposed ?: $disposed; // Once disposed, do not change flag
2020-05-13 17:15:21 +02:00
if ($this->completed) { // Record stack trace when calling complete() or fail()
\assert((function () {
if (isDebugEnabled()) {
$trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
\array_shift($trace); // remove current closure
$this->resolutionTrace = $trace;
}
2020-05-13 17:15:21 +02:00
return true;
})());
}
2020-05-13 17:15:21 +02:00
if ($this->result) {
return;
}
2020-05-13 17:15:21 +02:00
$this->result = $result;
2020-05-13 17:15:21 +02:00
if ($this->disposed) {
if (empty($this->waiting)) {
$this->triggerDisposal();
}
} else {
$this->resolvePending();
}
}
/**
* Resolves all pending promises returned from {@see continue()} with the result promise.
*/
2020-09-24 18:52:22 +02:00
private function resolvePending(): void
{
$backPressure = $this->backPressure;
$this->backPressure = [];
$waiting = $this->waiting;
$this->waiting = [];
foreach ($backPressure as $deferred) {
$deferred->resolve($this->result);
}
foreach ($waiting as $deferred) {
$deferred->resolve($this->result);
}
}
2020-05-13 17:15:21 +02:00
/**
* Invokes all pending {@see onDisposal()} callbacks and fails pending {@see continue()} promises.
*/
2020-09-24 18:52:22 +02:00
private function triggerDisposal(): void
{
\assert($this->disposed, "Pipeline was not disposed on triggering disposal");
if ($this->onDisposal === null) {
return;
2020-05-13 17:15:21 +02:00
}
$onDisposal = $this->onDisposal;
$this->onDisposal = null;
$this->resolvePending();
2020-07-29 17:29:57 +02:00
/** @psalm-suppress PossiblyNullIterator $alreadyDisposed is a guard against $this->onDisposal being null */
foreach ($onDisposal as $callback) {
try {
$callback();
} catch (\Throwable $e) {
Loop::defer(static function () use ($e) {
throw $e;
});
2020-07-29 17:29:57 +02:00
}
2020-05-13 17:15:21 +02:00
}
}
}