Skip to content

Commit 179520e

Browse files
authored
Merge pull request #99 from jsor-labs/follower-cancellation-propagation
Follower cancellation propagation
2 parents 84afb3d + c03ea25 commit 179520e

File tree

2 files changed

+110
-16
lines changed

2 files changed

+110
-16
lines changed

src/Promise.php

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ class Promise implements ExtendedPromiseInterface, CancellablePromiseInterface
1111
private $progressHandlers = [];
1212

1313
private $requiredCancelRequests = 0;
14-
private $cancelRequests = 0;
1514

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

3433
return new static($this->resolver($onFulfilled, $onRejected, $onProgress), function () {
35-
if (++$this->cancelRequests < $this->requiredCancelRequests) {
36-
return;
37-
}
34+
$this->requiredCancelRequests--;
3835

39-
$this->cancel();
36+
if ($this->requiredCancelRequests <= 0) {
37+
$this->cancel();
38+
}
4039
});
4140
}
4241

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

8887
public function cancel()
8988
{
90-
if (null === $this->canceller || null !== $this->result) {
91-
return;
92-
}
93-
9489
$canceller = $this->canceller;
9590
$this->canceller = null;
9691

97-
$this->call($canceller);
92+
$parentCanceller = null;
93+
94+
if (null !== $this->result) {
95+
// Go up the promise chain and reach the top most promise which is
96+
// itself not following another promise
97+
$root = $this->unwrap($this->result);
98+
99+
// Return if the root promise is already resolved or a
100+
// FulfilledPromise or RejectedPromise
101+
if (!$root instanceof self || null !== $root->result) {
102+
return;
103+
}
104+
105+
$root->requiredCancelRequests--;
106+
107+
if ($root->requiredCancelRequests <= 0) {
108+
$parentCanceller = [$root, 'cancel'];
109+
}
110+
}
111+
112+
if (null !== $canceller) {
113+
$this->call($canceller);
114+
}
115+
116+
// For BC, we call the parent canceller after our own canceller
117+
if ($parentCanceller) {
118+
$parentCanceller();
119+
}
98120
}
99121

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

182+
if ($promise === $this) {
183+
$promise = new RejectedPromise(
184+
new \LogicException('Cannot resolve a promise with itself.')
185+
);
186+
}
187+
188+
if ($promise instanceof self) {
189+
$promise->requiredCancelRequests++;
190+
}
191+
160192
$handlers = $this->handlers;
161193

162194
$this->progressHandlers = $this->handlers = [];
@@ -184,12 +216,6 @@ private function extract($promise)
184216
$promise = $promise->promise();
185217
}
186218

187-
if ($promise === $this) {
188-
return new RejectedPromise(
189-
new \LogicException('Cannot resolve a promise with itself.')
190-
);
191-
}
192-
193219
return $promise;
194220
}
195221

tests/PromiseTest/CancelTestTrait.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,4 +228,72 @@ public function cancelShouldAlwaysTriggerCancellerWhenCalledOnRootPromise()
228228

229229
$adapter->promise()->cancel();
230230
}
231+
232+
/** @test */
233+
public function cancelShouldTriggerCancellerWhenFollowerCancels()
234+
{
235+
$adapter1 = $this->getPromiseTestAdapter($this->expectCallableOnce());
236+
237+
$root = $adapter1->promise();
238+
239+
$adapter2 = $this->getPromiseTestAdapter($this->expectCallableOnce());
240+
241+
$follower = $adapter2->promise();
242+
$adapter2->resolve($root);
243+
244+
$follower->cancel();
245+
}
246+
247+
/** @test */
248+
public function cancelShouldNotTriggerCancellerWhenCancellingOnlyOneFollower()
249+
{
250+
$adapter1 = $this->getPromiseTestAdapter($this->expectCallableNever());
251+
252+
$root = $adapter1->promise();
253+
254+
$adapter2 = $this->getPromiseTestAdapter($this->expectCallableOnce());
255+
256+
$follower1 = $adapter2->promise();
257+
$adapter2->resolve($root);
258+
259+
$adapter3 = $this->getPromiseTestAdapter($this->expectCallableNever());
260+
$adapter3->resolve($root);
261+
262+
$follower1->cancel();
263+
}
264+
265+
/** @test */
266+
public function cancelCalledOnFollowerShouldOnlyCancelWhenAllChildrenAndFollowerCancelled()
267+
{
268+
$adapter1 = $this->getPromiseTestAdapter($this->expectCallableOnce());
269+
270+
$root = $adapter1->promise();
271+
272+
$child = $root->then();
273+
274+
$adapter2 = $this->getPromiseTestAdapter($this->expectCallableOnce());
275+
276+
$follower = $adapter2->promise();
277+
$adapter2->resolve($root);
278+
279+
$follower->cancel();
280+
$child->cancel();
281+
}
282+
283+
/** @test */
284+
public function cancelShouldNotTriggerCancellerWhenCancellingFollowerButNotChildren()
285+
{
286+
$adapter1 = $this->getPromiseTestAdapter($this->expectCallableNever());
287+
288+
$root = $adapter1->promise();
289+
290+
$root->then();
291+
292+
$adapter2 = $this->getPromiseTestAdapter($this->expectCallableOnce());
293+
294+
$follower = $adapter2->promise();
295+
$adapter2->resolve($root);
296+
297+
$follower->cancel();
298+
}
231299
}

0 commit comments

Comments
 (0)