Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancellable promise #20

Merged
merged 8 commits into from
Oct 15, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just remember to update this before tagging. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep 😎


* 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider adding a check to conditionally return this new behavior vs the old behavior based on if a canceller is associated with the promise? Creating a new closure when it isn't necessary would add overhead where it isn't going to be utilized.

Something like:

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

// Return the new stuff you're adding here...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, done in 77f78d9

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