MAJ Librarys

This commit is contained in:
EoleDev 2016-03-09 16:03:46 +01:00
parent 1d3ed3af6d
commit 03ef74d0cf
17 changed files with 347 additions and 194 deletions

View file

@ -1,5 +1,11 @@
# CHANGELOG
## 1.1.0 - 2016-03-07
* Update EachPromise to prevent recurring on a iterator when advancing, as this
could trigger fatal generator errors.
* Update Promise to allow recursive waiting without unwrapping exceptions.
## 1.0.3 - 2015-10-15
* Update EachPromise to immediately resolve when the underlying promise iterator

View file

@ -24,6 +24,9 @@ class EachPromise implements PromisorInterface
/** @var Promise */
private $aggregate;
/** @var bool */
private $mutex;
/**
* Configuration hash can include the following key value pairs:
*
@ -81,6 +84,7 @@ class EachPromise implements PromisorInterface
private function createPromise()
{
$this->mutex = false;
$this->aggregate = new Promise(function () {
reset($this->pending);
if (empty($this->pending) && !$this->iterable->valid()) {
@ -169,11 +173,21 @@ class EachPromise implements PromisorInterface
private function advanceIterator()
{
// Place a lock on the iterator so that we ensure to not recurse,
// preventing fatal generator errors.
if ($this->mutex) {
return false;
}
$this->mutex = true;
try {
$this->iterable->next();
$this->mutex = false;
return true;
} catch (\Exception $e) {
$this->aggregate->reject($e);
$this->mutex = false;
return false;
}
}
@ -186,9 +200,11 @@ class EachPromise implements PromisorInterface
}
unset($this->pending[$idx]);
$this->advanceIterator();
if (!$this->checkIfFinished()) {
// Only refill pending promises if we are not locked, preventing the
// EachPromise to recursively invoke the provided iterator, which
// cause a fatal error: "Cannot resume an already running generator"
if ($this->advanceIterator() && !$this->checkIfFinished()) {
// Add more pending promises if possible.
$this->refillPending();
}

View file

@ -61,17 +61,19 @@ class Promise implements PromiseInterface
{
$this->waitIfPending();
if (!$unwrap) {
return null;
}
$inner = $this->result instanceof PromiseInterface
? $this->result->wait($unwrap)
: $this->result;
if ($this->result instanceof PromiseInterface) {
return $this->result->wait($unwrap);
} elseif ($this->state === self::FULFILLED) {
return $this->result;
} else {
// It's rejected so "unwrap" and throw an exception.
throw exception_for($this->result);
if ($unwrap) {
if ($this->result instanceof PromiseInterface
|| $this->state === self::FULFILLED
) {
return $inner;
} else {
// It's rejected so "unwrap" and throw an exception.
throw exception_for($inner);
}
}
}
@ -257,11 +259,10 @@ class Promise implements PromiseInterface
$this->waitList = null;
foreach ($waitList as $result) {
descend:
$result->waitIfPending();
if ($result->result instanceof Promise) {
while ($result->result instanceof Promise) {
$result = $result->result;
goto descend;
$result->waitIfPending();
}
}
}

View file

@ -56,6 +56,7 @@ class TaskQueue
*/
public function run()
{
/** @var callable $task */
while ($task = array_shift($this->queue)) {
$task();
}

View file

@ -146,9 +146,9 @@ function inspect(PromiseInterface $promise)
'value' => $promise->wait()
];
} catch (RejectionException $e) {
return ['state' => 'rejected', 'reason' => $e->getReason()];
return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
} catch (\Exception $e) {
return ['state' => 'rejected', 'reason' => $e];
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
}
}
@ -304,10 +304,10 @@ function settle($promises)
return each(
$promises,
function ($value, $idx) use (&$results) {
$results[$idx] = ['state' => 'fulfilled', 'value' => $value];
$results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value];
},
function ($reason, $idx) use (&$results) {
$results[$idx] = ['state' => 'rejected', 'reason' => $reason];
$results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason];
}
)->then(function () use (&$results) {
ksort($results);

View file

@ -39,8 +39,8 @@ class EachPromiseTest extends \PHPUnit_Framework_TestCase
public function testIsWaitable()
{
$a = new Promise(function () use (&$a) { $a->resolve('a'); });
$b = new Promise(function () use (&$b) { $b->resolve('b'); });
$a = $this->createSelfResolvingPromise('a');
$b = $this->createSelfResolvingPromise('b');
$called = [];
$each = new EachPromise([$a, $b], [
'fulfilled' => function ($value) use (&$called) { $called[] = $value; }
@ -54,7 +54,7 @@ class EachPromiseTest extends \PHPUnit_Framework_TestCase
public function testCanResolveBeforeConsumingAll()
{
$called = 0;
$a = new Promise(function () use (&$a) { $a->resolve('a'); });
$a = $this->createSelfResolvingPromise('a');
$b = new Promise(function () { $this->fail(); });
$each = new EachPromise([$a, $b], [
'fulfilled' => function ($value, $idx, Promise $aggregate) use (&$called) {
@ -291,4 +291,46 @@ class EachPromiseTest extends \PHPUnit_Framework_TestCase
}
$this->assertEquals(range(0, 9), $results);
}
private function createSelfResolvingPromise($value)
{
$p = new Promise(function () use (&$p, $value) {
$p->resolve($value);
});
return $p;
}
public function testMutexPreventsGeneratorRecursion()
{
$results = $promises = [];
for ($i = 0; $i < 20; $i++) {
$p = $this->createSelfResolvingPromise($i);
$pending[] = $p;
$promises[] = $p;
}
$iter = function () use (&$promises, &$pending) {
foreach ($promises as $promise) {
// Resolve a promises, which will trigger the then() function,
// which would cause the EachPromise to try to add more
// promises to the queue. Without a lock, this would trigger
// a "Cannot resume an already running generator" fatal error.
if ($p = array_pop($pending)) {
$p->wait();
}
yield $promise;
}
};
$each = new EachPromise($iter(), [
'concurrency' => 5,
'fulfilled' => function ($r) use (&$results, &$pending) {
$results[] = $r;
}
]);
$each->promise()->wait();
$this->assertCount(20, $results);
}
}

View file

@ -172,6 +172,18 @@ class PromiseTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('Whoop', $p->wait());
}
public function testWaitsOnAPromiseChainEvenWhenNotUnwrapped()
{
$p2 = new Promise(function () use (&$p2) {
$p2->reject('Fail');
});
$p = new Promise(function () use ($p2, &$p) {
$p->resolve($p2);
});
$p->wait(false);
$this->assertSame(Promise::REJECTED, $p2->getState());
}
public function testCannotCancelNonPending()
{
$p = new Promise();