Skip to content

Commit 9fd4bd8

Browse files
committed
Implement follower cancellation propagation
1 parent 5d58c47 commit 9fd4bd8

1 file changed

Lines changed: 47 additions & 18 deletions

File tree

src/Promise.php

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ final class Promise implements PromiseInterface
99

1010
private $handlers = [];
1111

12-
private $remainingCancelRequests = 0;
12+
private $requiredCancelRequests = 0;
1313

1414
public function __construct(callable $resolver, callable $canceller = null)
1515
{
@@ -27,14 +27,14 @@ public function then(callable $onFulfilled = null, callable $onRejected = null)
2727
return new static($this->resolver($onFulfilled, $onRejected));
2828
}
2929

30-
$this->remainingCancelRequests++;
30+
$this->requiredCancelRequests++;
3131

3232
return new static($this->resolver($onFulfilled, $onRejected), function () {
33-
if (--$this->remainingCancelRequests > 0) {
34-
return;
35-
}
33+
$this->requiredCancelRequests--;
3634

37-
$this->cancel();
35+
if ($this->requiredCancelRequests <= 0) {
36+
$this->cancel();
37+
}
3838
});
3939
}
4040

@@ -76,14 +76,37 @@ public function always(callable $onFulfilledOrRejected)
7676

7777
public function cancel()
7878
{
79-
if (null === $this->canceller) {
80-
return;
81-
}
82-
8379
$canceller = $this->canceller;
8480
$this->canceller = null;
8581

86-
$this->call($canceller);
82+
$parentCanceller = null;
83+
84+
if (null !== $this->result) {
85+
// Go up the promise chain and reach the top most promise which is
86+
// itself not following another promise
87+
$root = $this->unwrap($this->result);
88+
89+
// Return if the root promise is already resolved or a
90+
// FulfilledPromise or RejectedPromise
91+
if (!$root instanceof self || null !== $root->result) {
92+
return;
93+
}
94+
95+
$root->requiredCancelRequests--;
96+
97+
if ($root->requiredCancelRequests <= 0) {
98+
$parentCanceller = [$root, 'cancel'];
99+
}
100+
}
101+
102+
if (null !== $canceller) {
103+
$this->call($canceller);
104+
}
105+
106+
// For BC, we call the parent canceller after our own canceller
107+
if ($parentCanceller) {
108+
$parentCanceller();
109+
}
87110
}
88111

89112
private function resolver(callable $onFulfilled = null, callable $onRejected = null)
@@ -119,10 +142,22 @@ private function settle(PromiseInterface $result)
119142
{
120143
$result = $this->unwrap($result);
121144

145+
if ($result === $this) {
146+
$result = new RejectedPromise(
147+
new \LogicException('Cannot resolve a promise with itself.')
148+
);
149+
}
150+
151+
if ($result instanceof self) {
152+
$result->requiredCancelRequests++;
153+
} else {
154+
// Unset canceller only when not following a pending promise
155+
$this->canceller = null;
156+
}
157+
122158
$handlers = $this->handlers;
123159

124160
$this->handlers = [];
125-
$this->canceller = null;
126161
$this->result = $result;
127162

128163
foreach ($handlers as $handler) {
@@ -136,12 +171,6 @@ private function unwrap($promise)
136171
$promise = $promise->result;
137172
}
138173

139-
if ($promise === $this) {
140-
return new RejectedPromise(
141-
new \LogicException('Cannot resolve a promise with itself.')
142-
);
143-
}
144-
145174
return $promise;
146175
}
147176

0 commit comments

Comments
 (0)