Skip to content

Commit 5bc3782

Browse files
authored
Merge pull request #54 from clue-labs/cancellation-v3
[3.x] Consistent cancellation semantics for `coroutine()`
2 parents 87eabc0 + 7058eb2 commit 5bc3782

File tree

2 files changed

+32
-23
lines changed

2 files changed

+32
-23
lines changed

src/functions.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,10 @@ function coroutine(callable $function, ...$args): PromiseInterface
233233

234234
$promise = null;
235235
$deferred = new Deferred(function () use (&$promise) {
236-
// cancel pending promise(s) as long as generator function keeps yielding
237-
while ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
238-
$temp = $promise;
239-
$promise = null;
240-
$temp->cancel();
236+
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
237+
$promise->cancel();
241238
}
239+
$promise = null;
242240
});
243241

244242
/** @var callable $next */

tests/CoroutineTest.php

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -106,42 +106,53 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue(
106106
$promise->then(null, $this->expectCallableOnceWith(new \UnexpectedValueException('Expected coroutine to yield React\Promise\PromiseInterface, but got integer')));
107107
}
108108

109-
110-
public function testCoroutineWillCancelPendingPromiseWhenCallingCancelOnResultingPromise()
109+
public function testCancelCoroutineWillReturnRejectedPromiseWhenCancellingPendingPromiseRejects()
111110
{
112-
$cancelled = 0;
113-
$promise = coroutine(function () use (&$cancelled) {
114-
yield new Promise(function () use (&$cancelled) {
115-
++$cancelled;
111+
$promise = coroutine(function () {
112+
yield new Promise(function () { }, function () {
113+
throw new \RuntimeException('Operation cancelled');
116114
});
117115
});
118116

119117
$promise->cancel();
120118

121-
$this->assertEquals(1, $cancelled);
119+
$promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('Operation cancelled')));
122120
}
123121

124-
public function testCoroutineWillCancelAllPendingPromisesWhenFunctionContinuesToYieldWhenCallingCancelOnResultingPromise()
122+
public function testCancelCoroutineWillReturnFulfilledPromiseWhenCancellingPendingPromiseRejectsInsideCatchThatReturnsValue()
125123
{
126124
$promise = coroutine(function () {
127-
$promise = new Promise(function () { }, function () {
128-
throw new \RuntimeException('Frist operation cancelled', 21);
129-
});
130-
131125
try {
132-
yield $promise;
126+
yield new Promise(function () { }, function () {
127+
throw new \RuntimeException('Operation cancelled');
128+
});
133129
} catch (\RuntimeException $e) {
134-
// ignore exception and continue
130+
return 42;
135131
}
132+
});
136133

137-
yield new Promise(function () { }, function () {
138-
throw new \RuntimeException('Second operation cancelled', 42);
139-
});
134+
$promise->cancel();
135+
136+
$promise->then($this->expectCallableOnceWith(42));
137+
}
138+
139+
public function testCancelCoroutineWillReturnPendigPromiseWhenCancellingFirstPromiseRejectsInsideCatchThatYieldsSecondPromise()
140+
{
141+
$promise = coroutine(function () {
142+
try {
143+
yield new Promise(function () { }, function () {
144+
throw new \RuntimeException('First operation cancelled');
145+
});
146+
} catch (\RuntimeException $e) {
147+
yield new Promise(function () { }, function () {
148+
throw new \RuntimeException('Second operation never cancelled');
149+
});
150+
}
140151
});
141152

142153
$promise->cancel();
143154

144-
$promise->then(null, $this->expectCallableOnceWith(new \RuntimeException('Second operation cancelled', 42)));
155+
$promise->then($this->expectCallableNever(), $this->expectCallableNever());
145156
}
146157

147158
public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorReturns()

0 commit comments

Comments
 (0)