Skip to content

Commit

Permalink
Merge pull request #54 from clue-labs/cancellation-v3
Browse files Browse the repository at this point in the history
[3.x] Consistent cancellation semantics for `coroutine()`
  • Loading branch information
WyriHaximus authored Jun 30, 2022
2 parents 87eabc0 + 7058eb2 commit 5bc3782
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 23 deletions.
8 changes: 3 additions & 5 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,10 @@ function coroutine(callable $function, ...$args): PromiseInterface

$promise = null;
$deferred = new Deferred(function () use (&$promise) {
// cancel pending promise(s) as long as generator function keeps yielding
while ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$temp = $promise;
$promise = null;
$temp->cancel();
if ($promise instanceof PromiseInterface && \method_exists($promise, 'cancel')) {
$promise->cancel();
}
$promise = null;
});

/** @var callable $next */
Expand Down
47 changes: 29 additions & 18 deletions tests/CoroutineTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,42 +106,53 @@ public function testCoroutineReturnsRejectedPromiseIfFunctionYieldsInvalidValue(
$promise->then(null, $this->expectCallableOnceWith(new \UnexpectedValueException('Expected coroutine to yield React\Promise\PromiseInterface, but got integer')));
}


public function testCoroutineWillCancelPendingPromiseWhenCallingCancelOnResultingPromise()
public function testCancelCoroutineWillReturnRejectedPromiseWhenCancellingPendingPromiseRejects()
{
$cancelled = 0;
$promise = coroutine(function () use (&$cancelled) {
yield new Promise(function () use (&$cancelled) {
++$cancelled;
$promise = coroutine(function () {
yield new Promise(function () { }, function () {
throw new \RuntimeException('Operation cancelled');
});
});

$promise->cancel();

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

public function testCoroutineWillCancelAllPendingPromisesWhenFunctionContinuesToYieldWhenCallingCancelOnResultingPromise()
public function testCancelCoroutineWillReturnFulfilledPromiseWhenCancellingPendingPromiseRejectsInsideCatchThatReturnsValue()
{
$promise = coroutine(function () {
$promise = new Promise(function () { }, function () {
throw new \RuntimeException('Frist operation cancelled', 21);
});

try {
yield $promise;
yield new Promise(function () { }, function () {
throw new \RuntimeException('Operation cancelled');
});
} catch (\RuntimeException $e) {
// ignore exception and continue
return 42;
}
});

yield new Promise(function () { }, function () {
throw new \RuntimeException('Second operation cancelled', 42);
});
$promise->cancel();

$promise->then($this->expectCallableOnceWith(42));
}

public function testCancelCoroutineWillReturnPendigPromiseWhenCancellingFirstPromiseRejectsInsideCatchThatYieldsSecondPromise()
{
$promise = coroutine(function () {
try {
yield new Promise(function () { }, function () {
throw new \RuntimeException('First operation cancelled');
});
} catch (\RuntimeException $e) {
yield new Promise(function () { }, function () {
throw new \RuntimeException('Second operation never cancelled');
});
}
});

$promise->cancel();

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

public function testCoroutineShouldNotCreateAnyGarbageReferencesWhenGeneratorReturns()
Expand Down

0 comments on commit 5bc3782

Please sign in to comment.