Skip to content

Commit

Permalink
Merge pull request #20 from reactphp/cancellable-promise
Browse files Browse the repository at this point in the history
Cancellable promise
  • Loading branch information
cboden committed Oct 15, 2014
2 parents f1e1063 + 1bced89 commit 41fdcef
Show file tree
Hide file tree
Showing 22 changed files with 428 additions and 44 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

* 2.1.x (xxxx-xx-xx)

* Introduce new CancellablePromiseInterface implemented by all promises
* Add new .cancel() method (part of the CancellablePromiseInterface)

* 2.0.0 (2013-12-10)

New major release. The goal was to streamline the API and to make it more
Expand Down
48 changes: 43 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ Table of Contents
* [Deferred::progress()](#deferredprogress)
* [PromiseInterface](#promiseinterface)
* [PromiseInterface::then()](#promiseinterfacethen)
* [CancellablePromiseInterface](#cancellablepromiseinterface)
* [CancellablePromiseInterface::cancel()](#cancellablepromiseinterfacecancel)
* [Promise](#promise-1)
* [FulfilledPromise](#fulfilledpromise)
* [RejectedPromise](#rejectedpromise)
Expand Down Expand Up @@ -96,6 +98,9 @@ The `resolve` and `reject` methods control the state of the deferred.

The `progress` method is for progress notification.

The constructor of the `Deferred` accepts an optional `$canceller` argument.
See [Promise](#promise-1) for more information.

#### Deferred::promise()

``` php
Expand Down Expand Up @@ -197,6 +202,31 @@ the same call to `then()`:
* [resolve()](#resolve) - Creating a resolved promise
* [reject()](#reject) - Creating a rejected promise

### CancellablePromiseInterface

A cancellable promise provides a mechanism for consumers to notify the creator
of the promise that they are not longer interested in the result of an
operation.

#### CancellablePromiseInterface::cancel()

``` php
$promise->cancel();
```

The `cancel()` method notifies the creator of the promise that there is no
further interest in the results of the operation.

Once a promise is settled (either fulfilled or rejected), calling `cancel()` on
a promise has no effect.

#### Implementations

* [Promise](#promise-1)
* [FulfilledPromise](#fulfilledpromise)
* [RejectedPromise](#rejectedpromise)
* [LazyPromise](#lazypromise)

### Promise

Creates a promise whose state is controlled by the functions passed to
Expand All @@ -214,11 +244,17 @@ $resolver = function (callable $resolve, callable $reject, callable $progress) {
// or $progress($progressNotification);
};

$promise = new React\Promise\Promise($resolver);
$canceller = function (callable $resolve, callable $reject, callable $progress) {
// Cancel/abort any running operations like network connections, streams etc.

$reject(new \Exception('Promise cancelled'));
};

$promise = new React\Promise\Promise($resolver, $canceller);
```

The promise constructor receives a resolver function which will be called
with 3 arguments:
The promise constructor receives a resolver function and an optional canceller
function which both will be called with 3 arguments:

* `$resolve($value)` - Primary function that seals the fate of the
returned promise. Accepts either a non-promise value, or another promise.
Expand All @@ -228,9 +264,11 @@ with 3 arguments:
* `$reject($reason)` - Function that rejects the promise.
* `$progress($update)` - Function that issues progress events for the promise.

If the resolver throws an exception, the promise will be rejected with that
thrown exception as the rejection reason.
If the resolver or canceller throw an exception, the promise will be rejected
with that thrown exception as the rejection reason.

The resolver function will be called immediately, the canceller function only
once all consumers called the `cancel()` method of the promise.

### FulfilledPromise

Expand Down
11 changes: 11 additions & 0 deletions src/CancellablePromiseInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace React\Promise;

interface CancellablePromiseInterface extends PromiseInterface
{
/**
* @return void
*/
public function cancel();
}
8 changes: 7 additions & 1 deletion src/Deferred.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ class Deferred implements PromisorInterface
private $resolveCallback;
private $rejectCallback;
private $progressCallback;
private $canceller;

public function __construct(callable $canceller = null)
{
$this->canceller = $canceller;
}

public function promise()
{
Expand All @@ -16,7 +22,7 @@ public function promise()
$this->resolveCallback = $resolve;
$this->rejectCallback = $reject;
$this->progressCallback = $progress;
});
}, $this->canceller);
}

return $this->promise;
Expand Down
6 changes: 5 additions & 1 deletion src/FulfilledPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace React\Promise;

class FulfilledPromise implements PromiseInterface
class FulfilledPromise implements CancellablePromiseInterface
{
private $value;

Expand All @@ -29,4 +29,8 @@ public function then(callable $onFulfilled = null, callable $onRejected = null,
return new RejectedPromise($exception);
}
}

public function cancel()
{
}
}
14 changes: 12 additions & 2 deletions src/LazyPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace React\Promise;

class LazyPromise implements PromiseInterface
class LazyPromise implements CancellablePromiseInterface
{
private $factory;
private $promise;
Expand All @@ -13,6 +13,16 @@ public function __construct(callable $factory)
}

public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
{
return $this->promise()->then($onFulfilled, $onRejected, $onProgress);
}

public function cancel()
{
return $this->promise()->cancel();
}

private function promise()
{
if (null === $this->promise) {
try {
Expand All @@ -22,6 +32,6 @@ public function then(callable $onFulfilled = null, callable $onRejected = null,
}
}

return $this->promise->then($onFulfilled, $onRejected, $onProgress);
return $this->promise;
}
}
67 changes: 49 additions & 18 deletions src/Promise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,21 @@

namespace React\Promise;

class Promise implements PromiseInterface
class Promise implements CancellablePromiseInterface
{
private $canceller;
private $result;

private $handlers = [];
private $progressHandlers = [];

public function __construct(callable $resolver)
private $requiredCancelRequests = 0;
private $cancelRequests = 0;

public function __construct(callable $resolver, callable $canceller = null)
{
try {
$resolver(
function ($value = null) {
$this->resolve($value);
},
function ($reason = null) {
$this->reject($reason);
},
function ($update = null) {
$this->progress($update);
}
);
} catch (\Exception $e) {
$this->reject($e);
}
$this->canceller = $canceller;
$this->call($resolver);
}

public function then(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
Expand All @@ -34,7 +25,28 @@ public function then(callable $onFulfilled = null, callable $onRejected = null,
return $this->result->then($onFulfilled, $onRejected, $onProgress);
}

return new static($this->resolver($onFulfilled, $onRejected, $onProgress));
if (null === $this->canceller) {
return new static($this->resolver($onFulfilled, $onRejected, $onProgress));
}

$this->requiredCancelRequests++;

return new static($this->resolver($onFulfilled, $onRejected, $onProgress), function ($resolve, $reject, $progress) {
if (++$this->cancelRequests < $this->requiredCancelRequests) {
return;
}

$this->cancel();
});
}

public function cancel()
{
if (null === $this->canceller || null !== $this->result) {
return;
}

$this->call($this->canceller);
}

private function resolver(callable $onFulfilled = null, callable $onRejected = null, callable $onProgress = null)
Expand Down Expand Up @@ -101,4 +113,23 @@ private function settle(PromiseInterface $result)

$this->result = $result;
}

private function call(callable $callback)
{
try {
$callback(
function ($value = null) {
$this->resolve($value);
},
function ($reason = null) {
$this->reject($reason);
},
function ($update = null) {
$this->progress($update);
}
);
} catch (\Exception $e) {
$this->reject($e);
}
}
}
6 changes: 5 additions & 1 deletion src/RejectedPromise.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace React\Promise;

class RejectedPromise implements PromiseInterface
class RejectedPromise implements CancellablePromiseInterface
{
private $reason;

Expand All @@ -27,4 +27,8 @@ public function then(callable $onFulfilled = null, callable $onRejected = null,
return new RejectedPromise($exception);
}
}

public function cancel()
{
}
}
4 changes: 2 additions & 2 deletions tests/DeferredTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ class DeferredTest extends TestCase
{
use PromiseTest\FullTestTrait;

public function getPromiseTestAdapter()
public function getPromiseTestAdapter(callable $canceller = null)
{
$d = new Deferred();
$d = new Deferred($canceller);

return new CallbackPromiseAdapter([
'promise' => [$d, 'promise'],
Expand Down
2 changes: 1 addition & 1 deletion tests/FulfilledPromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class FulfilledPromiseTest extends TestCase
use PromiseTest\PromiseSettledTestTrait,
PromiseTest\PromiseFulfilledTestTrait;

public function getPromiseTestAdapter()
public function getPromiseTestAdapter(callable $canceller = null)
{
$promise = null;

Expand Down
4 changes: 2 additions & 2 deletions tests/LazyPromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ class LazyPromiseTest extends TestCase
{
use PromiseTest\FullTestTrait;

public function getPromiseTestAdapter()
public function getPromiseTestAdapter(callable $canceller = null)
{
$d = new Deferred();
$d = new Deferred($canceller);

$factory = function () use ($d) {
return $d->promise();
Expand Down
4 changes: 2 additions & 2 deletions tests/PromiseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ class PromiseTest extends TestCase
{
use PromiseTest\FullTestTrait;

public function getPromiseTestAdapter()
public function getPromiseTestAdapter(callable $canceller = null)
{
$resolveCallback = $rejectCallback = $progressCallback = null;

$promise = new Promise(function ($resolve, $reject, $progress) use (&$resolveCallback, &$rejectCallback, &$progressCallback) {
$resolveCallback = $resolve;
$rejectCallback = $reject;
$progressCallback = $progress;
});
}, $canceller);

return new CallbackPromiseAdapter([
'promise' => function () use ($promise) {
Expand Down
Loading

0 comments on commit 41fdcef

Please sign in to comment.