1
0
mirror of https://github.com/danog/amp.git synced 2025-01-23 05:41:25 +01:00
amp/lib/Internal/Producer.php
Niklas Keller c45cd5a3a9 Fix backpressure release
Previously, backpressure was only released once advance() was called
again. This means the last backpressure item was never free'd in case
the caller knows there are no more items to consume and didn't call
advance() the last time.
2019-09-21 18:30:14 +02:00

206 lines
5.8 KiB
PHP

<?php
namespace Amp\Internal;
use Amp\Deferred;
use Amp\Failure;
use Amp\Promise;
use Amp\Success;
use React\Promise\PromiseInterface as ReactPromise;
/**
* Trait used by Iterator implementations. Do not use this trait in your code, instead compose your class from one of
* the available classes implementing \Amp\Iterator.
* Note that it is the responsibility of the user of this trait to ensure that listeners have a chance to listen first
* before emitting values.
*
* @internal
*/
trait Producer
{
/** @var \Amp\Promise|null */
private $complete;
/** @var mixed[] */
private $values = [];
/** @var \Amp\Deferred[] */
private $backPressure = [];
/** @var int */
private $position = -1;
/** @var \Amp\Deferred|null */
private $waiting;
/** @var null|array */
private $resolutionTrace;
/**
* {@inheritdoc}
*/
public function advance(): Promise
{
if ($this->waiting !== null) {
throw new \Error("The prior promise returned must resolve before invoking this method again");
}
if (isset($this->backPressure[$this->position])) {
$future = $this->backPressure[$this->position];
unset($this->values[$this->position], $this->backPressure[$this->position]);
$future->resolve();
}
++$this->position;
if (\array_key_exists($this->position, $this->values)) {
if (isset($this->backPressure[$this->position])) {
$future = $this->backPressure[$this->position];
unset($this->backPressure[$this->position]);
$future->resolve();
}
return new Success(true);
}
if ($this->complete) {
return $this->complete;
}
$this->waiting = new Deferred;
$this->waiting->promise()->onResolve(function () {
if (isset($this->backPressure[$this->position])) {
$future = $this->backPressure[$this->position];
unset($this->backPressure[$this->position]);
$future->resolve();
}
});
return $this->waiting->promise();
}
/**
* {@inheritdoc}
*/
public function getCurrent()
{
if (empty($this->values) && $this->complete) {
throw new \Error("The iterator has completed");
}
if (!\array_key_exists($this->position, $this->values)) {
throw new \Error("Promise returned from advance() must resolve before calling this method");
}
return $this->values[$this->position];
}
/**
* Emits a value from the iterator. The returned promise is resolved with the emitted value once all listeners
* have been invoked.
*
* @param mixed $value
*
* @return \Amp\Promise
*
* @throws \Error If the iterator has completed.
*/
private function emit($value): Promise
{
if ($this->complete) {
throw new \Error("Iterators cannot emit values after calling complete");
}
if ($value instanceof ReactPromise) {
$value = Promise\adapt($value);
}
if ($value instanceof Promise) {
$deferred = new Deferred;
$value->onResolve(function ($e, $v) use ($deferred) {
if ($this->complete) {
$deferred->fail(
new \Error("The iterator was completed before the promise result could be emitted")
);
return;
}
if ($e) {
$this->fail($e);
$deferred->fail($e);
return;
}
$deferred->resolve($this->emit($v));
});
return $deferred->promise();
}
$this->values[] = $value;
$this->backPressure[] = $pressure = new Deferred;
if ($this->waiting !== null) {
$waiting = $this->waiting;
$this->waiting = null;
$waiting->resolve(true);
}
return $pressure->promise();
}
/**
* Completes the iterator.
*
* @throws \Error If the iterator has already been completed.
*/
private function complete()
{
if ($this->complete) {
$message = "Iterator has already been completed";
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);
}
\assert((function () {
$env = \getenv("AMP_DEBUG") ?: "0";
if (($env !== "0" && $env !== "false") || (\defined("AMP_DEBUG") && \AMP_DEBUG)) {
$trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS);
\array_shift($trace); // remove current closure
$this->resolutionTrace = $trace;
}
return true;
})());
$this->complete = new Success(false);
if ($this->waiting !== null) {
$waiting = $this->waiting;
$this->waiting = null;
$waiting->resolve($this->complete);
}
}
private function fail(\Throwable $exception)
{
$this->complete = new Failure($exception);
if ($this->waiting !== null) {
$waiting = $this->waiting;
$this->waiting = null;
$waiting->resolve($this->complete);
}
}
}