Skip to content

Commit d360822

Browse files
committed
Merge pull request #48 from clue-labs/cancellable
Backport promise cancellation support to v1 Promise API
2 parents 44027fb + e8401c3 commit d360822

14 files changed

+369
-13
lines changed

README.md

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ Table of Contents
3737
* [When::reject()](#whenreject)
3838
* [When::lazy()](#whenlazy)
3939
* [Promisor](#promisor)
40+
* [CancellablePromiseInterface](#cancellablepromiseinterface)
41+
* [CancellablePromiseInterface::cancel()](#cancellablepromiseinterfacecancel)
4042
4. [Examples](#examples)
4143
* [How to use Deferred](#how-to-use-deferred)
4244
* [How Promise forwarding works](#how-promise-forwarding-works)
@@ -111,6 +113,18 @@ $deferred->reject(mixed $reason = null);
111113
$deferred->progress(mixed $update = null);
112114
```
113115

116+
The constructor of the `Deferred` accepts an optional `$canceller` argument.
117+
See [Promise](#promise-1) for more information.
118+
119+
120+
``` php
121+
$deferred = new React\Promise\Deferred(function ($resolve, $reject, $progress) {
122+
throw new \Exception('Promise cancelled');
123+
});
124+
125+
$deferred->cancel();
126+
```
127+
114128
### Promise
115129

116130
The Promise represents the eventual outcome, which is either fulfillment
@@ -133,7 +147,13 @@ $resolver = function (callable $resolve, callable $reject, callable $notify) {
133147
// or $notify($progressNotification);
134148
};
135149

136-
$promise = new React\Promise\Promise($resolver);
150+
$canceller = function (callable $resolve, callable $reject, callable $progress) {
151+
// Cancel/abort any running operations like network connections, streams etc.
152+
153+
$reject(new \Exception('Promise cancelled'));
154+
};
155+
156+
$promise = new React\Promise\Promise($resolver, $canceller);
137157
```
138158

139159
The promise constructor receives a resolver function which will be called
@@ -150,6 +170,9 @@ immediately with 3 arguments:
150170
If the resolver throws an exception, the promise will be rejected with that
151171
thrown exception as the rejection reason.
152172

173+
The resolver function will be called immediately, the canceller function only
174+
once all consumers called the `cancel()` method of the promise.
175+
153176
A Promise has a single method `then()` which registers new fulfilled, error and
154177
progress handlers with this Promise (all parameters are optional):
155178

@@ -484,6 +507,32 @@ The `React\Promise\PromisorInterface` provides a common interface for objects
484507
that provide a promise. `React\Promise\Deferred` implements it, but since it
485508
is part of the public API anyone can implement it.
486509

510+
### CancellablePromiseInterface
511+
512+
A cancellable promise provides a mechanism for consumers to notify the creator
513+
of the promise that they are not longer interested in the result of an
514+
operation.
515+
516+
#### CancellablePromiseInterface::cancel()
517+
518+
``` php
519+
$promise->cancel();
520+
```
521+
522+
The `cancel()` method notifies the creator of the promise that there is no
523+
further interest in the results of the operation.
524+
525+
Once a promise is settled (either resolved or rejected), calling `cancel()` on
526+
a promise has no effect.
527+
528+
#### Implementations
529+
530+
* [Deferred](#deferred-1)
531+
* [Promise](#promise-1)
532+
* [FulfilledPromise](#fulfilledpromise)
533+
* [RejectedPromise](#rejectedpromise)
534+
* [LazyPromise](#lazypromise)
535+
487536
Examples
488537
--------
489538

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace React\Promise;
4+
5+
interface CancellablePromiseInterface extends PromiseInterface
6+
{
7+
/**
8+
* @return void
9+
*/
10+
public function cancel();
11+
}

src/React/Promise/Deferred.php

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,56 @@
22

33
namespace React\Promise;
44

5-
class Deferred implements PromiseInterface, ResolverInterface, PromisorInterface
5+
class Deferred implements PromiseInterface, ResolverInterface, PromisorInterface, CancellablePromiseInterface
66
{
77
private $completed;
88
private $promise;
99
private $resolver;
1010
private $handlers = array();
1111
private $progressHandlers = array();
12+
private $canceller;
13+
14+
private $requiredCancelRequests = 0;
15+
private $cancelRequests = 0;
16+
17+
public function __construct($canceller = null)
18+
{
19+
if ($canceller !== null && !is_callable($canceller)) {
20+
throw new \InvalidArgumentException(
21+
sprintf(
22+
'The canceller argument must be null or of type callable, %s given.',
23+
gettype($canceller)
24+
)
25+
);
26+
}
27+
28+
$this->canceller = $canceller;
29+
}
1230

1331
public function then($fulfilledHandler = null, $errorHandler = null, $progressHandler = null)
1432
{
1533
if (null !== $this->completed) {
1634
return $this->completed->then($fulfilledHandler, $errorHandler, $progressHandler);
1735
}
1836

19-
$deferred = new static();
37+
$canceller = null;
38+
if ($this->canceller !== null) {
39+
$this->requiredCancelRequests++;
40+
41+
$that = $this;
42+
$current =& $this->cancelRequests;
43+
$required =& $this->requiredCancelRequests;
44+
45+
$canceller = function () use ($that, &$current, &$required) {
46+
if (++$current < $required) {
47+
return;
48+
}
49+
50+
$that->cancel();
51+
};
52+
}
53+
54+
$deferred = new static($canceller);
2055

2156
if (is_callable($progressHandler)) {
2257
$progHandler = function ($update) use ($deferred, $progressHandler) {
@@ -96,6 +131,35 @@ public function resolver()
96131
return $this->resolver;
97132
}
98133

134+
public function cancel()
135+
{
136+
if (null === $this->canceller || null !== $this->completed) {
137+
return;
138+
}
139+
140+
$canceller = $this->canceller;
141+
$this->canceller = null;
142+
143+
try {
144+
$that = $this;
145+
146+
call_user_func(
147+
$canceller,
148+
function ($value = null) use ($that) {
149+
$that->resolve($value);
150+
},
151+
function ($reason = null) use ($that) {
152+
$that->reject($reason);
153+
},
154+
function ($update = null) use ($that) {
155+
$that->progress($update);
156+
}
157+
);
158+
} catch (\Exception $e) {
159+
$this->reject($e);
160+
}
161+
}
162+
99163
protected function processQueue($queue, $value)
100164
{
101165
foreach ($queue as $handler) {

src/React/Promise/DeferredPromise.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,9 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
1515
{
1616
return $this->deferred->then($fulfilledHandler, $errorHandler, $progressHandler);
1717
}
18+
19+
public function cancel()
20+
{
21+
$this->deferred->cancel();
22+
}
1823
}

src/React/Promise/FulfilledPromise.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace React\Promise;
44

5-
class FulfilledPromise implements PromiseInterface
5+
class FulfilledPromise implements PromiseInterface, CancellablePromiseInterface
66
{
77
private $result;
88

@@ -27,4 +27,8 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
2727
return new RejectedPromise($exception);
2828
}
2929
}
30+
31+
public function cancel()
32+
{
33+
}
3034
}

src/React/Promise/LazyPromise.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace React\Promise;
44

5-
class LazyPromise implements PromiseInterface
5+
class LazyPromise implements PromiseInterface, CancellablePromiseInterface
66
{
77
private $factory;
88
private $promise;
@@ -13,6 +13,19 @@ public function __construct($factory)
1313
}
1414

1515
public function then($fulfilledHandler = null, $errorHandler = null, $progressHandler = null)
16+
{
17+
return $this->promise()->then($fulfilledHandler, $errorHandler, $progressHandler);
18+
}
19+
20+
public function cancel()
21+
{
22+
$promise = $this->promise();
23+
if ($promise instanceof CancellablePromiseInterface) {
24+
$promise->cancel();
25+
}
26+
}
27+
28+
private function promise()
1629
{
1730
if (null === $this->promise) {
1831
try {
@@ -21,7 +34,6 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
2134
$this->promise = new RejectedPromise($exception);
2235
}
2336
}
24-
25-
return $this->promise->then($fulfilledHandler, $errorHandler, $progressHandler);
37+
return $this->promise;
2638
}
2739
}

src/React/Promise/Promise.php

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
namespace React\Promise;
44

5-
class Promise implements PromiseInterface
5+
class Promise implements PromiseInterface, CancellablePromiseInterface
66
{
77
private $deferred;
88

9-
public function __construct($resolver)
9+
public function __construct($resolver, $canceller = null)
1010
{
1111
if (!is_callable($resolver)) {
1212
throw new \InvalidArgumentException(
@@ -17,7 +17,7 @@ public function __construct($resolver)
1717
);
1818
}
1919

20-
$this->deferred = new Deferred();
20+
$this->deferred = new Deferred($canceller);
2121
$this->call($resolver);
2222
}
2323

@@ -26,6 +26,11 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
2626
return $this->deferred->then($fulfilledHandler, $errorHandler, $progressHandler);
2727
}
2828

29+
public function cancel()
30+
{
31+
$this->deferred->cancel();
32+
}
33+
2934
private function call($callback)
3035
{
3136
$deferred = $this->deferred;

src/React/Promise/RejectedPromise.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace React\Promise;
44

5-
class RejectedPromise implements PromiseInterface
5+
class RejectedPromise implements PromiseInterface, CancellablePromiseInterface
66
{
77
private $reason;
88

@@ -27,4 +27,8 @@ public function then($fulfilledHandler = null, $errorHandler = null, $progressHa
2727
return new RejectedPromise($exception);
2828
}
2929
}
30+
31+
public function cancel()
32+
{
33+
}
3034
}

tests/React/Promise/DeferredPromiseTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,16 @@ public function shouldForwardToDeferred()
2020
$p = new DeferredPromise($mock);
2121
$p->then(1, 2, 3);
2222
}
23+
24+
/** @test */
25+
public function shouldForwardCancelToDeferred()
26+
{
27+
$mock = $this->getMock('React\\Promise\\Deferred');
28+
$mock
29+
->expects($this->once())
30+
->method('cancel');
31+
32+
$p = new DeferredPromise($mock);
33+
$p->cancel();
34+
}
2335
}

0 commit comments

Comments
 (0)