Skip to content

Commit

Permalink
Merge pull request #99 from jsor-labs/follower-cancellation-propagation
Browse files Browse the repository at this point in the history
Follower cancellation propagation
  • Loading branch information
jsor committed Sep 19, 2017
2 parents 84afb3d + c03ea25 commit 179520e
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 16 deletions.
58 changes: 42 additions & 16 deletions src/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class Promise implements ExtendedPromiseInterface, CancellablePromiseInterface
private $progressHandlers = [];

private $requiredCancelRequests = 0;
private $cancelRequests = 0;

public function __construct(callable $resolver, callable $canceller = null)
{
Expand All @@ -32,11 +31,11 @@ public function then(callable $onFulfilled = null, callable $onRejected = null,
$this->requiredCancelRequests++;

return new static($this->resolver($onFulfilled, $onRejected, $onProgress), function () {
if (++$this->cancelRequests < $this->requiredCancelRequests) {
return;
}
$this->requiredCancelRequests--;

$this->cancel();
if ($this->requiredCancelRequests <= 0) {
$this->cancel();
}
});
}

Expand Down Expand Up @@ -87,14 +86,37 @@ public function progress(callable $onProgress)

public function cancel()
{
if (null === $this->canceller || null !== $this->result) {
return;
}

$canceller = $this->canceller;
$this->canceller = null;

$this->call($canceller);
$parentCanceller = null;

if (null !== $this->result) {
// Go up the promise chain and reach the top most promise which is
// itself not following another promise
$root = $this->unwrap($this->result);

// Return if the root promise is already resolved or a
// FulfilledPromise or RejectedPromise
if (!$root instanceof self || null !== $root->result) {
return;
}

$root->requiredCancelRequests--;

if ($root->requiredCancelRequests <= 0) {
$parentCanceller = [$root, 'cancel'];
}
}

if (null !== $canceller) {
$this->call($canceller);
}

// For BC, we call the parent canceller after our own canceller
if ($parentCanceller) {
$parentCanceller();
}
}

private function resolver(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
Expand Down Expand Up @@ -157,6 +179,16 @@ private function settle(ExtendedPromiseInterface $promise)
{
$promise = $this->unwrap($promise);

if ($promise === $this) {
$promise = new RejectedPromise(
new \LogicException('Cannot resolve a promise with itself.')
);
}

if ($promise instanceof self) {
$promise->requiredCancelRequests++;
}

$handlers = $this->handlers;

$this->progressHandlers = $this->handlers = [];
Expand Down Expand Up @@ -184,12 +216,6 @@ private function extract($promise)
$promise = $promise->promise();
}

if ($promise === $this) {
return new RejectedPromise(
new \LogicException('Cannot resolve a promise with itself.')
);
}

return $promise;
}

Expand Down
68 changes: 68 additions & 0 deletions tests/PromiseTest/CancelTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,4 +228,72 @@ public function cancelShouldAlwaysTriggerCancellerWhenCalledOnRootPromise()

$adapter->promise()->cancel();
}

/** @test */
public function cancelShouldTriggerCancellerWhenFollowerCancels()
{
$adapter1 = $this->getPromiseTestAdapter($this->expectCallableOnce());

$root = $adapter1->promise();

$adapter2 = $this->getPromiseTestAdapter($this->expectCallableOnce());

$follower = $adapter2->promise();
$adapter2->resolve($root);

$follower->cancel();
}

/** @test */
public function cancelShouldNotTriggerCancellerWhenCancellingOnlyOneFollower()
{
$adapter1 = $this->getPromiseTestAdapter($this->expectCallableNever());

$root = $adapter1->promise();

$adapter2 = $this->getPromiseTestAdapter($this->expectCallableOnce());

$follower1 = $adapter2->promise();
$adapter2->resolve($root);

$adapter3 = $this->getPromiseTestAdapter($this->expectCallableNever());
$adapter3->resolve($root);

$follower1->cancel();
}

/** @test */
public function cancelCalledOnFollowerShouldOnlyCancelWhenAllChildrenAndFollowerCancelled()
{
$adapter1 = $this->getPromiseTestAdapter($this->expectCallableOnce());

$root = $adapter1->promise();

$child = $root->then();

$adapter2 = $this->getPromiseTestAdapter($this->expectCallableOnce());

$follower = $adapter2->promise();
$adapter2->resolve($root);

$follower->cancel();
$child->cancel();
}

/** @test */
public function cancelShouldNotTriggerCancellerWhenCancellingFollowerButNotChildren()
{
$adapter1 = $this->getPromiseTestAdapter($this->expectCallableNever());

$root = $adapter1->promise();

$root->then();

$adapter2 = $this->getPromiseTestAdapter($this->expectCallableOnce());

$follower = $adapter2->promise();
$adapter2->resolve($root);

$follower->cancel();
}
}

0 comments on commit 179520e

Please sign in to comment.