Skip to content

Commit

Permalink
'Cancel' for PromiseKit (#899)
Browse files Browse the repository at this point in the history
This patch adds the concept of a `CancelContext` to PromiseKit allowing chains to handle cancellation properly for each independent promise after the cancel point.
  • Loading branch information
dougzilla32 authored and mxcl committed Mar 19, 2019
1 parent 59553d6 commit f785e0e
Show file tree
Hide file tree
Showing 50 changed files with 7,063 additions and 57 deletions.
59 changes: 59 additions & 0 deletions .github/spelling-skip-words
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,62 @@ top-100
CocoaPod
conformant
PromiseKits
CancellablePromise
CancellableThenable
thenable
CancellableCatchable
catchable
cancellableRecover
ensureThen
Alamofire.request
responseDecodable
DecodableObject.self
BFTask
CLLocationManager.requestLocation
URLSession.shared.dataTask
MapKit
MKDirections
URLSession.shared.GET
StoreKit
SKProductsRequest
SystemConfiguration
SCNetworkReachability.promise
UIViewPropertyAnimator
startAnimation
Alamofire.DataRequest
responseData
responseString
responseJSON
responsePropertyList
Alamofire.DownloadRequest
CLLocationManager
requestLocation
authorizationType
requestAuthorization
requestedAuthorizationType
NotificationCenter
NSObject
keyPath
URLSession
dataTask
uploadTask
fromFile
downloadTask
HomeKit
HMPromiseAccessoryBrowser
scanInterval
HMHomeManager
calculateETA
MKMapSnapshotter
SKReceiptRefreshRequest
SCNetworkReachability
Cancellability
HealthKit
enum
cancellize
UIImage
PMKFinalizercancelthendonePromise
VoidPending
PromiseURLSessionresumewait
unusedResult
discardableResultcatchreturncauterize
464 changes: 464 additions & 0 deletions Documents/Cancel.md

Large diffs are not rendered by default.

81 changes: 31 additions & 50 deletions Documents/CommonPatterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,22 +212,42 @@ one promise at a time if you need to.

```swift
let fetches: [Promise<T>] = makeFetches()
let timeout = after(seconds: 4)

race(when(fulfilled: fetches).asVoid(), timeout).then {
race(when(fulfilled: fetches).asVoid(), timeout(seconds: 4)).then {
//
}.catch(policy: .allErrors) {
// Rejects with 'PMKError.timedOut' if the timeout is exceeded
}
```

`race` continues as soon as one of the promises it is watching finishes.

`timeout(seconds: TimeInterval)` returns a promise that throws
`PMKError.timedOut` when the time interval is exceeded. Note that `PMKError.timedOut`
is a cancellation error therefore the `.allErrors` catch policy must be specified
to handle this exception.

Make sure the promises you pass to `race` are all of the same type. The easiest way
to ensure this is to use `asVoid()`.

Note that if any component promise rejects, the `race` will reject, too.

When used with cancellable promises, all promises will be cancelled if either the timeout is
exceeded or if any promise rejects.

```swift
let fetches: [Promise<T>] = makeFetches()
let cancellableFetches: [CancellablePromise<T>] = fetches.map { return $0.cancellize() }

// All promises are automatically cancelled if any of them reject.
race(when(fulfilled: cancellableFetches).asVoid(), timeout(seconds: 4).cancellize()).then {
//
}.catch(policy: .allErrors) {
// Rejects with 'PMKError.timedOut' if the timeout is exceeded.
}
```

# Minimum Duration
## Minimum Duration

Sometimes you need a task to take *at least* a certain amount of time. (For example,
you want to show a progress spinner, but if it shows for less than 0.3 seconds, the UI
Expand All @@ -245,61 +265,22 @@ firstly {
}
```

The code above works because we create the delay *before* we do work in `foo()`. By the
The code above works because we create the delay *before* we do work in `foo()`. By the
time we get to waiting on that promise, either it will have already timed out or we will wait
for whatever remains of the 0.3 seconds before continuing the chain.


## Cancellation

Promises don’t have a `cancel` function, but they do support cancellation through a
special error type that conforms to the `CancellableError` protocol.

```swift
func foo() -> (Promise<Void>, cancel: () -> Void) {
let task = Task()
var cancelme = false

let promise = Promise<Void> { seal in
task.completion = { value in
guard !cancelme else { return reject(PMKError.cancelled) }
seal.fulfill(value)
}
task.start()
}

let cancel = {
cancelme = true
task.cancel()
}

return (promise, cancel)
}
```

Promises don’t have a `cancel` function because you don’t want code outside of
your control to be able to cancel your operations--*unless*, of course, you explicitly
want to enable that behavior. In cases where you do want cancellation, the exact way
that it should work will vary depending on how the underlying task supports cancellation.
PromiseKit provides cancellation primitives but no concrete API.

Cancelled chains do not call `catch` handlers by default. However you can
intercept cancellation if you like:

```swift
foo.then {
//
}.catch(policy: .allErrors) {
// cancelled errors are handled *as well*
}
```
Starting with version 7, PromiseKit explicitly supports cancellation of promises and
promise chains. There is a new class called `CancellablePromise` that defines a `cancel`
method. Use the `cancellize` method on `Thenable` to obtain a `CancellablePromise` from a
`Promise` or `Guarantee`.

**Important**: Canceling a promise chain is *not* the same as canceling the underlying
asynchronous task. Promises are wrappers around asynchronicity, but they have no
control over the underlying tasks. If you need to cancel an underlying task, you
need to cancel the underlying task!
Invoking `cancel` will both reject the promise with `PMKError.cancelled` and cancel any
underlying asynchronous task(s).

> The library [CancellablePromiseKit](https://github.com/johannesd/CancellablePromiseKit) extends the concept of Promises to fully cover cancellable tasks.
For full details see [Cancelling Promises](Cancel.md).

## Retry / Polling

Expand Down
1 change: 1 addition & 0 deletions Documents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Handbook
* [Getting Started](GettingStarted.md)
* [Promises: Common Patterns](CommonPatterns.md)
* [Cancelling Promises](Cancel.md)
* [Frequently Asked Questions](FAQ.md)
* Manual
* [Installation Guide](Installation.md)
Expand Down
78 changes: 78 additions & 0 deletions Documents/Troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,84 @@ An *inline* function like this is all you need. Here, the problem is that you
forgot to mark the last line of the closure with an explicit `return`. It's required
here because the closure is longer than one line.

### Cancellable promise embedded in the middle of a standard promise chain

Error: ***Cannot convert value of type 'Promise<>' to closure result type 'Guarantee<>'***. Fixed by adding `cancellize` to `firstly { login() }`.

```swift
/// 'login()' returns 'Promise<Creds>'
/// 'fetch(avatar:)' returns 'CancellablePromise<UIImage>'

let promise = firstly {
login() /// <-- ERROR: Cannot convert value of type 'Promise<Creds>' to closure result type 'Guarantee<Creds>'
}.then { creds in /// CHANGE TO: "}.cancellize().then { creds in"
fetch(avatar: creds.user) /// <-- ERROR: Cannot convert value of type 'CancellablePromise<UIImage>' to
/// closure result type 'Guarantee<UIImage>'
}.done { image in
self.imageView = image
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}

//

promise.cancel()
```

### The return type for a multi-line closure returning `CancellablePromise` is not explicitly stated

The Swift compiler cannot (yet) determine the return type of a multi-line closure.

The following example gives the unhelpful error: ***'()' is not convertible to 'UIImage'***. Many other strange errors can result from not explicitly declaring the return type of a multi-line closure. These kinds of errors are fixed by explicitly declaring the return type, which in the following example is a `CancellablePromise<UIImage>``.

```swift
/// 'login()' returns 'Promise<Creds>'
/// 'fetch(avatar:)' returns 'CancellablePromise<UIImage>'

let promise = firstly {
login()
}.cancellize().then { creds in /// CHANGE TO: "}.cancellize().then { creds -> CancellablePromise<UIImage> in"
let f = fetch(avatar: creds.user)
return f
}.done { image in
self.imageView = image /// <-- ERROR: '()' is not convertible to 'UIImage'
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}

//

promise.cancel()
```

### Trying to cancel a standard promise chain

Error: ***Value of type `PMKFinalizer` has no member `cancel`***. Fixed by using cancellable promises instead of standard promises.

```swift
/// 'login()' returns 'Promise<Creds>'
/// 'fetch(avatar:)' returns 'CancellablePromise<UIImage>'

let promise = firstly {
login()
}.then { creds in /// CHANGE TO: "}.cancellize().then { creds in"
fetch(avatar: creds.user).promise /// CHANGE TO: fetch(avatar: creds.user)
}.done { image in
self.imageView = image
}.catch(policy: .allErrors) { error in
if error.isCancelled {
// the chain has been cancelled!
}
}

//

promise.cancel() /// <-- ERROR: Value of type 'PMKFinalizer' has no member 'cancel'
```

## You copied code off the Internet that doesn’t work

Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pkg.swiftLanguageVersions = [
pkg.targets = [
.target(name: "PromiseKit", path: "Sources"),
.testTarget(name: "Core", dependencies: ["PromiseKit"], path: "Tests/Core"),
.testTarget(name: "Cancel", dependencies: ["PromiseKit"], path: "Tests/Cancel"),
.testTarget(name: "A+.swift", dependencies: ["PromiseKit"], path: "Tests/A+/Swift"),
.testTarget(name: "A+.js", dependencies: ["PromiseKit"], path: "Tests/A+/JavaScript"),
]
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ PromiseKit 7 is prerelease, if you’re using it: beware!
PromiseKit 7 uses Swift 5’s `Result`, PromiseKit <7 use our own `Result` type.

PromiseKit 7 generalizes `DispatchQueue`s to a `Dispatcher` protocol. However, `DispatchQueue`s are `Dispatcher`-conformant,
so existing code should not need to change. Please report any issues related to this transition.
so existing code should not need to change. Please report any issues related to this transition.

PromiseKit 7 adds support for cancelling promises and promise chains.

# Quick Start

Expand Down Expand Up @@ -95,6 +97,7 @@ help me continue my work, I appreciate it 🙏🏻
* Handbook
* [Getting Started](Documents/GettingStarted.md)
* [Promises: Common Patterns](Documents/CommonPatterns.md)
* [Cancelling Promises](Documents/Cancel.md)
* [Frequently Asked Questions](Documents/FAQ.md)
* Manual
* [Installation Guide](Documents/Installation.md)
Expand Down
Loading

0 comments on commit f785e0e

Please sign in to comment.