diff --git a/src/Promise.php b/src/Promise.php index 0261eb30..889926df 100644 --- a/src/Promise.php +++ b/src/Promise.php @@ -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) { @@ -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(); + } }); } @@ -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) @@ -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 = []; @@ -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; } diff --git a/tests/PromiseTest/CancelTestTrait.php b/tests/PromiseTest/CancelTestTrait.php index d722d756..0e2b8883 100644 --- a/tests/PromiseTest/CancelTestTrait.php +++ b/tests/PromiseTest/CancelTestTrait.php @@ -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(); + } }