diff --git a/.github/spelling-skip-words b/.github/spelling-skip-words
index 8e48a9eca..f0f87c808 100644
--- a/.github/spelling-skip-words
+++ b/.github/spelling-skip-words
@@ -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
diff --git a/Documents/Cancel.md b/Documents/Cancel.md
new file mode 100644
index 000000000..2811019cc
--- /dev/null
+++ b/Documents/Cancel.md
@@ -0,0 +1,464 @@
+# Cancelling Promises
+
+PromiseKit 7 adds clear and concise cancellation abilities to promises and to the [PromiseKit extensions](#extensions-pane). Cancelling promises and their associated tasks is now simple and straightforward. Promises and promise chains can safely and efficiently be cancelled from any thread at any time.
+
+```swift
+UIApplication.shared.isNetworkActivityIndicatorVisible = true
+
+let fetchImage = URLSession.shared.dataTask(.promise, with: url).cancellize().compactMap{ UIImage(data: $0.data) }
+let fetchLocation = CLLocationManager.requestLocation().cancellize().lastValue
+
+let finalizer = firstly {
+ when(fulfilled: fetchImage, fetchLocation)
+}.done { image, location in
+ self.imageView.image = image
+ self.label.text = "\(location)"
+}.ensure {
+ UIApplication.shared.isNetworkActivityIndicatorVisible = false
+}.catch(policy: .allErrors) { error in
+ /* 'catch' will be invoked with 'PMKError.cancelled' when cancel is called on the context.
+ Use the default policy of '.allErrorsExceptCancellation' to ignore cancellation errors. */
+ self.show(UIAlertController(for: error), sender: self)
+}
+
+//…
+
+// Cancel currently active tasks and reject all cancellable promises with 'PMKError.cancelled'.
+// 'cancel()' can be called from any thread at any time.
+finalizer.cancel()
+
+/* 'finalizer' here refers to the 'CancellableFinalizer' for the chain. Calling 'cancel' on
+ any promise in the chain or on the finalizer cancels the entire chain. Therefore
+ calling 'cancel' on the finalizer cancels everything. */
+```
+
+# Cancel Chains
+
+Promises can be cancelled using a `CancellablePromise`. The `cancellize()` method on `Promise` is used to convert a `Promise` into a `CancellablePromise`. If a promise chain is initialized with a `CancellablePromise`, then the entire chain is cancellable. Calling `cancel()` on any promise in the chain cancels the entire chain.
+
+Creating a chain where the entire chain can be cancelled is the recommended usage for cancellable promises.
+
+The `CancellablePromise` contains a `CancelContext` that keeps track of the tasks and promises for the chain. Promise chains can be cancelled either by calling the `cancel()` method on any `CancellablePromise` in the chain, or by calling `cancel()` on the `CancelContext` for the chain. It may be desirable to hold on to the `CancelContext` directly rather than a promise so that the promise can be deallocated by ARC when it is resolved.
+
+For example:
+
+```swift
+let context = firstly {
+ login()
+ /* The 'Thenable.cancellize' method initiates a cancellable promise chain by
+ returning a 'CancellablePromise'. */
+}.cancellize().then { creds in
+ fetch(avatar: creds.user)
+}.done { image in
+ self.imageView = image
+}.catch(policy: .allErrors) { error in
+ if error.isCancelled {
+ // the chain has been cancelled!
+ }
+}.cancelContext
+
+// …
+
+/* Note: Promises can be cancelled using the 'cancel()' method on the 'CancellablePromise'.
+ However, it may be desirable to hold on to the 'CancelContext' directly rather than a
+ promise so that the promise can be deallocated by ARC when it is resolved. */
+context.cancel()
+```
+
+### Creating a partially cancellable chain
+
+A `CancellablePromise` can be placed at the start of a chain, but it cannot be embedded directly in the middle of a standard (non-cancellable) promise chain. Instead, a partially cancellable promise chain can be used. A partially cancellable chain is not the recommended way to use cancellable promises, although there may be cases where this is useful.
+
+**Convert a cancellable chain to a standard chain**
+
+`CancellablePromise` wraps a delegate `Promise`, which can be accessed with the `promise` property. The above example can be modified as follows so that once `login()` completes, the chain can no longer be cancelled:
+
+```swift
+/* Here, by calling 'promise.then' rather than 'then' the chain is converted from a cancellable
+ promise chain to a standard promise chain. In this example, calling 'cancel()' during 'login'
+ will cancel the chain but calling 'cancel()' during the 'fetch' operation will have no effect: */
+let cancellablePromise = firstly {
+ login().cancellize()
+}
+cancellablePromise.promise.then {
+ fetch(avatar: creds.user)
+}.done { image in
+ self.imageView = image
+}.catch(policy: .allErrors) { error in
+ if error.isCancelled {
+ // the chain has been cancelled!
+ }
+}
+
+// …
+
+/* This will cancel the 'login' but will not cancel the 'fetch'. So whether or not the
+ chain is cancelled depends on how far the chain has progressed. */
+cancellablePromise.cancel()
+```
+
+**Convert a standard chain to a cancellable chain**
+
+A non-cancellable chain can be converted to a cancellable chain in the middle of the chain as follows:
+
+```swift
+/* In this example, calling 'cancel()' during 'login' will not cancel the login. However,
+ the chain will be cancelled immediately, and the 'fetch' will not be executed. If 'cancel()'
+ is called during the 'fetch' then both the 'fetch' itself and the promise chain will be
+ cancelled immediately. */
+let promise = firstly {
+ login()
+}.then {
+ fetch(avatar: creds.user).cancellize()
+}.done { image in
+ self.imageView = image
+}.catch(policy: .allErrors) { error in
+ if error.isCancelled {
+ // the chain has been cancelled!
+ }
+}
+
+// …
+
+promise.cancel()
+```
+
+# Core Cancellable PromiseKit API
+
+The following classes, methods and functions have been added to PromiseKit to support cancellation. Existing functions or methods with underlying tasks that can be cancelled are indicated by being appended with '.cancellize()'.
+
+
Thenable
+ cancellize(_:) - Converts the Promise or Guarantee (Thenable) into a
+ CancellablePromise, which is a cancellable variant of the given
+ Promise or Guarantee (Thenable)
+
+Global functions
+ after(seconds:).cancellize() - 'after' with seconds can be cancelled
+ after(_:).cancellize - 'after' with interval can be cancelled
+
+ firstly(execute:) - Accepts body returning Promise or CancellablePromise
+ hang(_:) - Accepts Promise and CancellablePromise
+ race(_:) - Accepts [Promise] and [CancellablePromise]
+ when(fulfilled:) - Accepts [Promise] and [CancellablePromise]
+ when(fulfilled:concurrently:) - Accepts iterator of type Promise or CancellablePromise
+ when(resolved:) - Accepts [Promise] and [CancellablePromise]
+
+CancellablePromise properties and methods
+ promise - Delegate Promise for this CancellablePromise
+ result - The current Result
+
+ init(_ bridge:cancelContext:) - Initialize a new cancellable promise bound to the provided Thenable
+ init(cancellable:resolver body:). - Initialize a new cancellable promise that can be resolved with
+ the provided '(Resolver) throws -> Void' body
+ init(cancellable:promise:resolver:) - Initialize a new cancellable promise using the given Promise
+ and its Resolver
+ init(cancellable:error:) - Initialize a new rejected cancellable promise
+ init(cancellable:) - Initializes a new cancellable promise fulfilled with Void
+
+ pending() -> (promise:resolver:) - Returns a tuple of a new cancellable pending promise and its
+ Resolver
+
+CancellableThenable properties and methods
+ thenable - Delegate Thenable for this CancellableThenable
+
+ cancel(error:) - Cancels all members of the promise chain
+ cancelContext - The CancelContext associated with this CancellableThenable
+ cancelItemList - Tracks the cancel items for this CancellableThenable
+ isCancelled - True if all members of the promise chain have been successfully
+ cancelled, false otherwise
+ cancelAttempted - True if 'cancel' has been called on the promise chain associated
+ with this CancellableThenable, false otherwise
+ cancelledError - The error generated when the promise is cancelled
+ appendCancellable(cancellable:reject:) - Append the Cancellable task to our cancel context
+ appendCancelContext(from:) - Append the cancel context associated with 'from' to our
+ CancelContext
+
+ then(on:flags:_ body:) - Accepts body returning CancellableThenable
+ cancellableThen(on:flags:_ body:) - Accepts body returning Thenable
+ map(on:flags:_ transform:)
+ compactMap(on:flags:_ transform:)
+ done(on:flags:_ body:)
+ get(on:flags:_ body:)
+ tap(on:flags:_ body:)
+ asVoid()
+
+ error
+ isPending
+ isResolved
+ isFulfilled
+ isRejected
+ value
+
+ mapValues(on:flags:_ transform:)
+ flatMapValues(on:flags:_ transform:)
+ compactMapValues(on:flags:_ transform:)
+ thenMap(on:flags:_ transform:) - Accepts transform returning CancellableThenable
+ cancellableThenMap(on:flags:_ transform:) - Accepts transform returning Thenable
+ thenFlatMap(on:flags:_ transform:) - Accepts transform returning CancellableThenable
+ cancellableThenFlatMap(on:flags:_ transform:) - Accepts transform returning Thenable
+ filterValues(on:flags:_ isIncluded:)
+ firstValue
+ lastValue
+ sortedValues(on:flags:)
+
+CancellableCatchable properties and methods
+ catchable - Delegate Catchable for this CancellableCatchable
+ catch(on:flags:policy::_ body:) - Accepts body returning Void
+ recover(on:flags:policy::_ body:) - Accepts body returning CancellableThenable
+ cancellableRecover(on:flags:policy::_ body:) - Accepts body returning Thenable
+ ensure(on:flags:_ body:) - Accepts body returning Void
+ ensureThen(on:flags:_ body:) - Accepts body returning CancellablePromise
+ finally(_ body:)
+ cauterize()
+
+
+# Extensions
+
+Cancellation support has been added to the PromiseKit extensions, but only where the underlying asynchronous tasks can be cancelled. This example Podfile lists the PromiseKit extensions that support cancellation along with a usage example:
+
+pod "PromiseKit/Alamofire"
+# Alamofire.request("http://example.com", method: .get).responseDecodable(DecodableObject.self).cancellize()
+
+pod "PromiseKit/Bolts"
+# CancellablePromise(…).then() { _ -> BFTask in /*…*/ } // Returns CancellablePromise
+
+pod "PromiseKit/CoreLocation"
+# CLLocationManager.requestLocation().cancellize().then { /*…*/ }
+
+pod "PromiseKit/Foundation"
+# URLSession.shared.dataTask(.promise, with: request).cancellize().then { /*…*/ }
+
+pod "PromiseKit/MapKit"
+# MKDirections(…).calculate().cancellize().then { /*…*/ }
+
+pod "PromiseKit/OMGHTTPURLRQ"
+# URLSession.shared.GET("http://example.com").cancellize().then { /*…*/ }
+
+pod "PromiseKit/StoreKit"
+# SKProductsRequest(…).start(.promise).cancellize().then { /*…*/ }
+
+pod "PromiseKit/SystemConfiguration"
+# SCNetworkReachability.promise().cancellize().then { /*…*/ }
+
+pod "PromiseKit/UIKit"
+# UIViewPropertyAnimator(…).startAnimation(.promise).cancellize().then { /*…*/ }
+
+
+Here is a complete list of PromiseKit extension methods that support cancellation:
+
+[Alamofire](http://github.com/PromiseKit/Alamofire-)
+
+Alamofire.DataRequest
+ response(_:queue:).cancellize()
+ responseData(queue:).cancellize()
+ responseString(queue:).cancellize()
+ responseJSON(queue:options:).cancellize()
+ responsePropertyList(queue:options:).cancellize()
+ responseDecodable(queue::decoder:).cancellize()
+ responseDecodable(_ type:queue:decoder:).cancellize()
+
+Alamofire.DownloadRequest
+ response(_:queue:).cancellize()
+ responseData(queue:).cancellize()
+
+
+[Bolts](http://github.com/PromiseKit/Bolts)
+
+CancellablePromise<T>
+ then<U>(on: DispatchQueue?, body: (T) -> BFTask<U>) -> CancellablePromise
+
+
+[CoreLocation](http://github.com/PromiseKit/CoreLocation)
+
+CLLocationManager
+ requestLocation(authorizationType:satisfying:).cancellize()
+ requestAuthorization(type requestedAuthorizationType:).cancellize()
+
+
+[Foundation](http://github.com/PromiseKit/Foundation)
+
+NotificationCenter:
+ observe(once:object:).cancellize()
+
+NSObject
+ observe(_:keyPath:).cancellize()
+
+Process
+ launch(_:).cancellize()
+
+URLSession
+ dataTask(_:with:).cancellize()
+ uploadTask(_:with:from:).cancellize()
+ uploadTask(_:with:fromFile:).cancellize()
+ downloadTask(_:with:to:).cancellize()
+
+CancellablePromise
+ validate()
+
+
+[HomeKit](http://github.com/PromiseKit/HomeKit)
+
+HMPromiseAccessoryBrowser
+ start(scanInterval:).cancellize()
+
+HMHomeManager
+ homes().cancellize()
+
+
+[MapKit](http://github.com/PromiseKit/MapKit)
+
+MKDirections
+ calculate().cancellize()
+ calculateETA().cancellize()
+
+MKMapSnapshotter
+ start().cancellize()
+
+
+[StoreKit](http://github.com/PromiseKit/StoreKit)
+
+SKProductsRequest
+ start(_:).cancellize()
+
+SKReceiptRefreshRequest
+ promise().cancellize()
+
+
+[SystemConfiguration](http://github.com/PromiseKit/SystemConfiguration)
+
+SCNetworkReachability
+ promise().cancellize()
+
+
+[UIKit](http://github.com/PromiseKit/UIKit)
+
+UIViewPropertyAnimator
+ startAnimation(_:).cancellize()
+
+
+## Choose Your Networking Library
+
+All the networking library extensions supported by PromiseKit are now simple to cancel!
+
+[Alamofire](http://github.com/PromiseKit/Alamofire-)
+
+```swift
+// pod 'PromiseKit/Alamofire'
+// # https://github.com/PromiseKit/Alamofire
+
+let context = firstly {
+ Alamofire
+ .request("http://example.com", method: .post, parameters: params)
+ .responseDecodable(Foo.self)
+}.cancellize().done { foo in
+ //…
+}.catch { error in
+ //…
+}.cancelContext
+
+//…
+
+context.cancel()
+```
+
+And (of course) plain `URLSession` from [Foundation](http://github.com/PromiseKit/Foundation):
+
+```swift
+// pod 'PromiseKit/Foundation'
+// # https://github.com/PromiseKit/Foundation
+
+let context = firstly {
+ URLSession.shared.dataTask(.promise, with: try makeUrlRequest())
+}.cancellize().map {
+ try JSONDecoder().decode(Foo.self, with: $0.data)
+}.done { foo in
+ //…
+}.catch { error in
+ //…
+}.cancelContext
+
+//…
+
+context.cancel()
+
+func makeUrlRequest() throws -> URLRequest {
+ var rq = URLRequest(url: url)
+ rq.httpMethod = "POST"
+ rq.addValue("application/json", forHTTPHeaderField: "Content-Type")
+ rq.addValue("application/json", forHTTPHeaderField: "Accept")
+ rq.httpBody = try JSONSerialization.jsonData(with: obj)
+ return rq
+}
+```
+
+# Cancellability Goals
+
+* Provide a streamlined way to cancel a promise chain, which rejects all associated promises and cancels all associated tasks. For example:
+
+```swift
+let promise = firstly {
+ login()
+}.cancellize().then { creds in // Use the 'cancellize' function to initiate a cancellable promise chain
+ 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()
+```
+
+* Ensure that subsequent code blocks in a promise chain are _never_ called after the chain has been cancelled
+
+* Fully support concurrency, where all code is thread-safe. Cancellable promises and promise chains can safely and efficiently be cancelled from any thread at any time.
+
+* Provide cancellable support for all PromiseKit extensions whose native tasks can be cancelled (e.g. Alamofire, Bolts, CoreLocation, Foundation, HealthKit, HomeKit, MapKit, StoreKit, SystemConfiguration, UIKit)
+
+* Support cancellation for all PromiseKit primitives such as 'after', 'firstly', 'when', 'race'
+
+* Provide a simple way to make new types of cancellable promises
+
+* Ensure promise branches are properly cancelled. For example:
+
+```swift
+import Alamofire
+import PromiseKit
+
+func updateWeather(forCity searchName: String) {
+ refreshButton.startAnimating()
+ let context = firstly {
+ getForecast(forCity: searchName)
+ }.cancellize().done { response in
+ updateUI(forecast: response)
+ }.ensure {
+ refreshButton.stopAnimating()
+ }.catch { error in
+ // Cancellation errors are ignored by default
+ showAlert(error: error)
+ }.cancelContext
+
+ //…
+
+ /* **** Cancels EVERYTHING (except... the 'ensure' block always executes regardless)
+ Note: non-cancellable tasks cannot be interrupted. For example: if 'cancel()' is
+ called in the middle of 'updateUI()' then the chain will immediately be rejected,
+ however the 'updateUI' call will complete normally because it is not cancellable.
+ Its return value (if any) will be discarded. */
+ context.cancel()
+}
+
+func getForecast(forCity name: String) -> CancellablePromise {
+ return firstly {
+ Alamofire.request("https://autocomplete.weather.com/\(name)")
+ .responseDecodable(AutoCompleteCity.self)
+ }.cancellize().then { city in
+ Alamofire.request("https://forecast.weather.com/\(city.name)")
+ .responseDecodable(WeatherResponse.self).cancellize()
+ }.map { response in
+ format(response)
+ }
+}
+```
diff --git a/Documents/CommonPatterns.md b/Documents/CommonPatterns.md
index 398affc60..8be09e415 100644
--- a/Documents/CommonPatterns.md
+++ b/Documents/CommonPatterns.md
@@ -212,22 +212,42 @@ one promise at a time if you need to.
```swift
let fetches: [Promise] = 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] = makeFetches()
+let cancellableFetches: [CancellablePromise] = 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
@@ -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, cancel: () -> Void) {
- let task = Task(…)
- var cancelme = false
-
- let promise = Promise { 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
diff --git a/Documents/README.md b/Documents/README.md
index 8164ddfec..20da99cd0 100644
--- a/Documents/README.md
+++ b/Documents/README.md
@@ -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)
diff --git a/Documents/Troubleshooting.md b/Documents/Troubleshooting.md
index 417fe17c2..218112124 100644
--- a/Documents/Troubleshooting.md
+++ b/Documents/Troubleshooting.md
@@ -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'
+/// 'fetch(avatar:)' returns 'CancellablePromise'
+
+let promise = firstly {
+ login() /// <-- ERROR: Cannot convert value of type 'Promise' to closure result type 'Guarantee'
+}.then { creds in /// CHANGE TO: "}.cancellize().then { creds in"
+ fetch(avatar: creds.user) /// <-- ERROR: Cannot convert value of type 'CancellablePromise' to
+ /// closure result type 'Guarantee'
+}.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``.
+
+```swift
+/// 'login()' returns 'Promise'
+/// 'fetch(avatar:)' returns 'CancellablePromise'
+
+let promise = firstly {
+ login()
+}.cancellize().then { creds in /// CHANGE TO: "}.cancellize().then { creds -> CancellablePromise 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'
+/// 'fetch(avatar:)' returns 'CancellablePromise'
+
+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
diff --git a/Package.swift b/Package.swift
index 4c5e54ce9..748cc1d70 100644
--- a/Package.swift
+++ b/Package.swift
@@ -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"),
]
diff --git a/README.md b/README.md
index 86c77c4f4..28369124f 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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)
diff --git a/Sources/CancelContext.swift b/Sources/CancelContext.swift
new file mode 100644
index 000000000..2e9ae677d
--- /dev/null
+++ b/Sources/CancelContext.swift
@@ -0,0 +1,257 @@
+import Dispatch
+import Foundation
+
+/**
+ Keeps track of all promises in a promise chain with pending or currently running tasks, and cancels them all when `cancel` is called.
+ */
+public class CancelContext: Hashable {
+ public static func == (lhs: CancelContext, rhs: CancelContext) -> Bool {
+ return lhs === rhs
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(ObjectIdentifier(self))
+ }
+
+ // Create a barrier queue that is used as a read/write lock for the CancelContext
+ // For reads: barrier.sync { }
+ // For writes: barrier.sync(flags: .barrier) { }
+ private let barrier = DispatchQueue(label: "org.promisekit.barrier.cancel", attributes: .concurrent)
+
+ private var cancelItems = [CancelItem]()
+ private var cancelItemSet = Set()
+
+ /**
+ Cancel all members of the promise chain and their associated asynchronous operations.
+
+ - Parameter error: Specifies the cancellation error to use for the cancel operation, defaults to `PMKError.cancelled`
+ */
+ public func cancel(with error: Error = PMKError.cancelled) {
+ self.cancel(with: error, visited: Set())
+ }
+
+ func cancel(with error: Error = PMKError.cancelled, visited: Set) {
+ var items: [CancelItem]!
+ barrier.sync(flags: .barrier) {
+ internalCancelledError = error
+ items = cancelItems
+ }
+
+ for item in items {
+ item.cancel(with: error, visited: visited)
+ }
+ }
+
+ /**
+ True if all members of the promise chain have been successfully cancelled, false otherwise.
+ */
+ public var isCancelled: Bool {
+ var items: [CancelItem]!
+ barrier.sync {
+ items = cancelItems
+ }
+
+ for item in items where !item.isCancelled {
+ return false
+ }
+ return true
+ }
+
+ /**
+ True if `cancel` has been called on the CancelContext associated with this promise, false otherwise. `cancelAttempted` will be true if `cancel` is called on any promise in the chain.
+ */
+ public var cancelAttempted: Bool {
+ return cancelledError != nil
+ }
+
+ private var internalCancelledError: Error?
+
+ /**
+ The cancellation error initialized when the promise is cancelled, or `nil` if not cancelled.
+ */
+ public private(set) var cancelledError: Error? {
+ get {
+ var err: Error!
+ barrier.sync {
+ err = internalCancelledError
+ }
+ return err
+ }
+
+ set {
+ barrier.sync(flags: .barrier) {
+ internalCancelledError = newValue
+ }
+ }
+ }
+
+ func append(cancellable: Cancellable?, reject: ((Error) -> Void)?, thenable: Z) {
+ if cancellable == nil && reject == nil {
+ return
+ }
+ let item = CancelItem(cancellable: cancellable, reject: reject)
+
+ var error: Error?
+ barrier.sync(flags: .barrier) {
+ error = internalCancelledError
+ cancelItems.append(item)
+ cancelItemSet.insert(item)
+ thenable.cancelItemList.append(item)
+ }
+
+ if error != nil {
+ item.cancel(with: error!)
+ }
+ }
+
+ func append(context childContext: CancelContext, thenable: Z) {
+ guard childContext !== self else {
+ return
+ }
+ let item = CancelItem(context: childContext)
+
+ var error: Error?
+ barrier.sync(flags: .barrier) {
+ error = internalCancelledError
+ cancelItems.append(item)
+ cancelItemSet.insert(item)
+ thenable.cancelItemList.append(item)
+ }
+
+ crossCancel(childContext: childContext, parentCancelledError: error)
+ }
+
+ func append(context childContext: CancelContext, thenableCancelItemList: CancelItemList) {
+ guard childContext !== self else {
+ return
+ }
+ let item = CancelItem(context: childContext)
+
+ var error: Error?
+ barrier.sync(flags: .barrier) {
+ error = internalCancelledError
+ cancelItems.append(item)
+ cancelItemSet.insert(item)
+ thenableCancelItemList.append(item)
+ }
+
+ crossCancel(childContext: childContext, parentCancelledError: error)
+ }
+
+ private func crossCancel(childContext: CancelContext, parentCancelledError: Error?) {
+ let parentError = parentCancelledError
+ let childError = childContext.cancelledError
+
+ if parentError != nil {
+ if childError == nil {
+ childContext.cancel(with: parentError!)
+ }
+ } else if childError != nil {
+ if parentError == nil {
+ cancel(with: childError!)
+ }
+ }
+ }
+
+ func recover() {
+ cancelledError = nil
+ }
+
+ func removeItems(_ list: CancelItemList, clearList: Bool) -> Error? {
+ var error: Error?
+ barrier.sync(flags: .barrier) {
+ error = internalCancelledError
+ if error == nil && list.items.count != 0 {
+ var currentIndex = 1
+ // The `list` parameter should match a block of items in the cancelItemList, remove them from the cancelItemList
+ // in one operation for efficiency
+ if cancelItemSet.remove(list.items[0]) != nil {
+ let removeIndex = cancelItems.firstIndex(of: list.items[0])!
+ while currentIndex < list.items.count {
+ let item = list.items[currentIndex]
+ if item != cancelItems[removeIndex + currentIndex] {
+ break
+ }
+ cancelItemSet.remove(item)
+ currentIndex += 1
+ }
+ cancelItems.removeSubrange(removeIndex..<(removeIndex+currentIndex))
+ }
+
+ // Remove whatever falls outside of the block
+ while currentIndex < list.items.count {
+ let item = list.items[currentIndex]
+ if cancelItemSet.remove(item) != nil {
+ cancelItems.remove(at: cancelItems.firstIndex(of: item)!)
+ }
+ currentIndex += 1
+ }
+
+ if clearList {
+ list.removeAll()
+ }
+ }
+ }
+ return error
+ }
+}
+
+/// Tracks the cancel items for a CancellablePromise. These items are removed from the associated CancelContext when the promise resolves.
+public class CancelItemList {
+ fileprivate var items: [CancelItem]
+
+ init() {
+ self.items = []
+ }
+
+ fileprivate func append(_ item: CancelItem) {
+ items.append(item)
+ }
+
+ fileprivate func removeAll() {
+ items.removeAll()
+ }
+}
+
+fileprivate class CancelItem: Hashable {
+ static func == (lhs: CancelItem, rhs: CancelItem) -> Bool {
+ return lhs === rhs
+ }
+
+ public func hash(into hasher: inout Hasher) {
+ hasher.combine(ObjectIdentifier(self))
+ }
+
+ let cancellable: Cancellable?
+ var reject: ((Error) -> Void)?
+ weak var context: CancelContext?
+ var cancelAttempted = false
+
+ init(cancellable: Cancellable?, reject: ((Error) -> Void)?) {
+ self.cancellable = cancellable
+ self.reject = reject
+ }
+
+ init(context: CancelContext) {
+ self.cancellable = nil
+ self.context = context
+ }
+
+ func cancel(with error: Error, visited: Set? = nil) {
+ cancelAttempted = true
+
+ cancellable?.cancel()
+ reject?(error)
+
+ if var v = visited, let c = context {
+ if !v.contains(c) {
+ v.insert(c)
+ c.cancel(with: error, visited: v)
+ }
+ }
+ }
+
+ var isCancelled: Bool {
+ return cancellable?.isCancelled ?? cancelAttempted
+ }
+}
diff --git a/Sources/CancellableCatchable.swift b/Sources/CancellableCatchable.swift
new file mode 100644
index 000000000..9a53fd642
--- /dev/null
+++ b/Sources/CancellableCatchable.swift
@@ -0,0 +1,301 @@
+import Dispatch
+
+/// Provides `catch` and `recover` to your object that conforms to `CancellableThenable`
+public protocol CancellableCatchMixin: CancellableThenable {
+ /// Type of the delegate `catchable`
+ associatedtype C: CatchMixin
+
+ /// Delegate `catchable` for this CancellablePromise
+ var catchable: C { get }
+}
+
+public extension CancellableCatchMixin {
+ /**
+ The provided closure executes when this cancellable promise rejects.
+
+ Rejecting a promise cascades: rejecting all subsequent promises (unless
+ recover is invoked) thus you will typically place your catch at the end
+ of a chain. Often utility promises will not have a catch, instead
+ delegating the error handling to the caller.
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter policy: The default policy does not execute your handler for cancellation errors.
+ - Parameter execute: The handler to execute if this promise is rejected.
+ - Returns: A promise finalizer.
+ - SeeAlso: [Cancellation](https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#cancellation)
+ */
+ @discardableResult
+ func `catch`(on: Dispatcher = conf.D.return, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> CancellableFinalizer {
+ return CancellableFinalizer(self.catchable.catch(on: on, policy: policy, body), cancel: self.cancelContext)
+ }
+}
+
+/**
+ Cancellable finalizer returned from `catch`. Use `finally` to specify a code block that executes when the promise chain resolves.
+ */
+public class CancellableFinalizer {
+ let pmkFinalizer: PMKFinalizer
+
+ /// The CancelContext associated with this finalizer
+ public let cancelContext: CancelContext
+
+ init(_ pmkFinalizer: PMKFinalizer, cancel: CancelContext) {
+ self.pmkFinalizer = pmkFinalizer
+ self.cancelContext = cancel
+ }
+
+ /// `finally` is the same as `ensure`, but it is not chainable
+ @discardableResult
+ public func finally(on: Dispatcher = conf.D.return, _ body: @escaping () -> Void) -> CancelContext {
+ pmkFinalizer.finally(on: on, body)
+ return cancelContext
+ }
+
+ /**
+ Cancel all members of the promise chain and their associated asynchronous operations.
+
+ - Parameter error: Specifies the cancellation error to use for the cancel operation, defaults to `PMKError.cancelled`
+ */
+ public func cancel(with error: Error = PMKError.cancelled) {
+ cancelContext.cancel(with: error)
+ }
+
+ /**
+ True if all members of the promise chain have been successfully cancelled, false otherwise.
+ */
+ public var isCancelled: Bool {
+ return cancelContext.isCancelled
+ }
+
+ /**
+ True if `cancel` has been called on the CancelContext associated with this promise, false otherwise. `cancelAttempted` will be true if `cancel` is called on any promise in the chain.
+ */
+ public var cancelAttempted: Bool {
+ return cancelContext.cancelAttempted
+ }
+
+ /**
+ The cancellation error generated when the promise is cancelled, or `nil` if not cancelled.
+ */
+ public var cancelledError: Error? {
+ return cancelContext.cancelledError
+ }
+}
+
+public extension CancellableCatchMixin {
+ /**
+ The provided closure executes when this cancellable promise rejects.
+
+ Unlike `catch`, `recover` continues the chain.
+ Use `recover` in circumstances where recovering the chain from certain errors is a possibility. For example:
+
+ let context = firstly {
+ CLLocationManager.requestLocation()
+ }.recover { error in
+ guard error == CLError.unknownLocation else { throw error }
+ return .value(CLLocation.chicago)
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The handler to execute if this promise is rejected.
+ - SeeAlso: [Cancellation](https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#cancellation)
+ */
+ func recover(on: Dispatcher = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> V) -> CancellablePromise where V.U.T == C.T {
+
+ let cancelItemList = CancelItemList()
+
+ let cancelBody = { (error: Error) throws -> V.U in
+ _ = self.cancelContext.removeItems(self.cancelItemList, clearList: true)
+ let rval = try body(error)
+ if policy == .allErrors {
+ self.cancelContext.recover()
+ }
+ self.cancelContext.append(context: rval.cancelContext, thenableCancelItemList: cancelItemList)
+ return rval.thenable
+ }
+
+ let promise = self.catchable.recover(on: on, policy: policy, cancelBody)
+ if thenable.result != nil && policy == .allErrors {
+ self.cancelContext.recover()
+ }
+ return CancellablePromise(promise: promise, context: self.cancelContext, cancelItemList: cancelItemList)
+ }
+
+ /**
+ The provided closure executes when this cancellable promise rejects.
+
+ Unlike `catch`, `recover` continues the chain.
+ Use `recover` in circumstances where recovering the chain from certain errors is a possibility. For example:
+
+ let context = firstly {
+ CLLocationManager.requestLocation()
+ }.cancellize().recover { error in
+ guard error == CLError.unknownLocation else { throw error }
+ return .value(CLLocation.chicago)
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The handler to execute if this promise is rejected.
+ - SeeAlso: [Cancellation](https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#cancellation)
+ - Note: Methods with the `cancellable` prefix create a new CancellablePromise, and those without the `cancellable` prefix accept an existing CancellablePromise.
+ */
+ func recover(on: Dispatcher = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> V) -> CancellablePromise where V.T == C.T {
+
+ let cancelBody = { (error: Error) throws -> V in
+ _ = self.cancelContext.removeItems(self.cancelItemList, clearList: true)
+ let rval = try body(error)
+ if policy == .allErrors {
+ self.cancelContext.recover()
+ }
+ return rval
+ }
+
+ let promise = self.catchable.recover(on: on, policy: policy, cancelBody)
+ if thenable.result != nil && policy == .allErrors {
+ self.cancelContext.recover()
+ }
+ let cancellablePromise = CancellablePromise(promise: promise, context: self.cancelContext)
+ if let cancellable = promise.cancellable {
+ self.cancelContext.append(cancellable: cancellable, reject: promise.rejectIfCancelled, thenable: cancellablePromise)
+ }
+ return cancellablePromise
+ }
+
+ /**
+ The provided closure executes when this cancellable promise resolves, whether it rejects or not.
+
+ let context = firstly {
+ UIApplication.shared.networkActivityIndicatorVisible = true
+ //… returns a cancellable promise
+ }.done {
+ //…
+ }.ensure {
+ UIApplication.shared.networkActivityIndicatorVisible = false
+ }.catch {
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The closure that executes when this promise resolves.
+ - Returns: A new promise, resolved with this promise’s resolution.
+ */
+ func ensure(on: Dispatcher = conf.D.return, _ body: @escaping () -> Void) -> CancellablePromise {
+ let rp = CancellablePromise.pending()
+ rp.promise.cancelContext = self.cancelContext
+ self.catchable.pipe { result in
+ on.dispatch {
+ body()
+ switch result {
+ case .success(let value):
+ if let error = self.cancelContext.cancelledError {
+ rp.resolver.reject(error)
+ } else {
+ rp.resolver.fulfill(value)
+ }
+ case .failure(let error):
+ rp.resolver.reject(error)
+ }
+ }
+ }
+ return rp.promise
+ }
+
+ /**
+ The provided closure executes when this cancellable promise resolves, whether it rejects or not.
+ The chain waits on the returned `CancellablePromise`.
+
+ let context = firstly {
+ setup() // returns a cancellable promise
+ }.done {
+ //…
+ }.ensureThen {
+ teardown() // -> CancellablePromise
+ }.catch {
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The closure that executes when this promise resolves.
+ - Returns: A new promise, resolved with this promise’s resolution.
+ */
+ func ensureThen(on: Dispatcher = conf.D.return, _ body: @escaping () -> CancellablePromise) -> CancellablePromise {
+ let rp = CancellablePromise.pending()
+ rp.promise.cancelContext = cancelContext
+ self.catchable.pipe { result in
+ on.dispatch {
+ let rv = body()
+ rp.promise.appendCancelContext(from: rv)
+
+ rv.done {
+ switch result {
+ case .success(let value):
+ if let error = self.cancelContext.cancelledError {
+ rp.resolver.reject(error)
+ } else {
+ rp.resolver.fulfill(value)
+ }
+ case .failure(let error):
+ rp.resolver.reject(error)
+ }
+ }.catch(policy: .allErrors) {
+ rp.resolver.reject($0)
+ }
+ }
+ }
+ return rp.promise
+ }
+
+ /**
+ Consumes the Swift unused-result warning.
+ - Note: You should `catch`, but in situations where you know you don’t need a `catch`, `cauterize` makes your intentions clear.
+ */
+ @discardableResult
+ func cauterize() -> CancellableFinalizer {
+ return self.catch(policy: .allErrors) {
+ Swift.print("PromiseKit:cauterized-error:", $0)
+ }
+ }
+}
+
+public extension CancellableCatchMixin where C.T == Void {
+ /**
+ The provided closure executes when this cancellable promise rejects.
+
+ This variant of `recover` ensures that no error is thrown from the handler and allows specifying a catch policy.
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The handler to execute if this promise is rejected.
+ - SeeAlso: [Cancellation](https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#cancellation)
+ */
+ func recover(on: Dispatcher = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> Void) -> CancellablePromise {
+ let cancelBody = { (error: Error) throws -> Void in
+ _ = self.cancelContext.removeItems(self.cancelItemList, clearList: true)
+ try body(error)
+ if policy == .allErrors {
+ self.cancelContext.recover()
+ }
+ }
+
+ let promise = self.catchable.recover(on: on, policy: policy, cancelBody)
+ if thenable.result != nil && policy == .allErrors {
+ self.cancelContext.recover()
+ }
+ return CancellablePromise(promise: promise, context: self.cancelContext)
+ }
+}
diff --git a/Sources/CancellablePromise.swift b/Sources/CancellablePromise.swift
new file mode 100644
index 000000000..212d8777d
--- /dev/null
+++ b/Sources/CancellablePromise.swift
@@ -0,0 +1,143 @@
+import class Foundation.Thread
+import Dispatch
+
+/**
+ A `CancellablePromise` is a functional abstraction around a failable and cancellable asynchronous operation.
+
+ At runtime the promise can become a member of a chain of promises, where the `cancelContext` is used to track and cancel (if desired) all promises in this chain.
+
+ - See: `CancellableThenable`
+ */
+public class CancellablePromise: CancellableThenable, CancellableCatchMixin {
+ /// Delegate `promise` for this CancellablePromise
+ public let promise: Promise
+
+ /// Type of the delegate `thenable`
+ public typealias U = Promise
+
+ /// Delegate `thenable` for this CancellablePromise
+ public var thenable: U {
+ return promise
+ }
+
+ /// Type of the delegate `catchable`
+ public typealias C = Promise
+
+ /// Delegate `catchable` for this CancellablePromise
+ public var catchable: C {
+ return promise
+ }
+
+ /// The CancelContext associated with this CancellablePromise
+ public var cancelContext: CancelContext
+
+ /// Tracks the cancel items for this CancellablePromise. These items are removed from the associated CancelContext when the promise resolves.
+ public var cancelItemList: CancelItemList
+
+ init(promise: Promise, context: CancelContext? = nil, cancelItemList: CancelItemList? = nil) {
+ self.promise = promise
+ self.cancelContext = context ?? CancelContext()
+ self.cancelItemList = cancelItemList ?? CancelItemList()
+ }
+
+ /// Initialize a new rejected cancellable promise.
+ public convenience init(cancellable: Cancellable? = nil, error: Error) {
+ var reject: ((Error) -> Void)!
+ self.init(promise: Promise { seal in
+ reject = seal.reject
+ seal.reject(error)
+ })
+ self.appendCancellable(cancellable, reject: reject)
+ }
+
+ /// Initialize a new cancellable promise bound to the provided `Thenable`.
+ public convenience init(_ bridge: U, cancelContext: CancelContext? = nil) where U.T == T {
+ var promise: Promise!
+ let cancellable: Cancellable!
+ var reject: ((Error) -> Void)!
+
+ if let p = bridge as? Promise {
+ cancellable = p.cancellable
+ if let r = p.rejectIfCancelled {
+ promise = p
+ reject = r
+ }
+ } else if let g = bridge as? Guarantee {
+ cancellable = g.cancellable
+ } else {
+ cancellable = nil
+ }
+
+ if promise == nil {
+ // Wrapper promise
+ promise = Promise { seal in
+ reject = seal.reject
+ bridge.done(on: nil) {
+ seal.fulfill($0)
+ }.catch(policy: .allErrors) {
+ seal.reject($0)
+ }
+ }
+ }
+
+ self.init(promise: promise, context: cancelContext)
+ self.appendCancellable(cancellable, reject: reject)
+ }
+
+ /// Initialize a new cancellable promise that can be resolved with the provided `Resolver`.
+ public convenience init(cancellable: Cancellable? = nil, resolver body: (Resolver) throws -> Void) {
+ var reject: ((Error) -> Void)!
+ self.init(promise: Promise { seal in
+ reject = seal.reject
+ try body(seal)
+ })
+ self.appendCancellable(cancellable, reject: reject)
+ }
+
+ /// Initialize a new cancellable promise using the given Promise and its Resolver.
+ public convenience init(cancellable: Cancellable? = nil, promise: Promise, resolver: Resolver) {
+ self.init(promise: promise)
+ self.appendCancellable(cancellable, reject: resolver.reject)
+ }
+
+ /// - Returns: a tuple of a new cancellable pending promise and its `Resolver`.
+ public class func pending() -> (promise: CancellablePromise, resolver: Resolver) {
+ let rp = Promise.pending()
+ return (promise: CancellablePromise(promise: rp.promise), resolver: rp.resolver)
+ }
+
+ /// Internal function required for `Thenable` conformance.
+ /// - See: `Thenable.pipe`
+ public func pipe(to: @escaping (Result) -> Void) {
+ promise.pipe(to: to)
+ }
+
+ /// - Returns: The current `Result` for this cancellable promise.
+ /// - See: `Thenable.result`
+ public var result: Result? {
+ return promise.result
+ }
+
+ /**
+ Blocks this thread, so—you know—don’t call this on a serial thread that
+ any part of your chain may use. Like the main thread for example.
+ */
+ public func wait() throws -> T {
+ return try promise.wait()
+ }
+}
+
+#if swift(>=3.1)
+extension CancellablePromise where T == Void {
+ /// Initializes a new cancellable promise fulfilled with `Void`
+ public convenience init() {
+ self.init(promise: Promise())
+ }
+
+ /// Initializes a new cancellable promise fulfilled with `Void` and with the given ` Cancellable`
+ public convenience init(cancellable: Cancellable) {
+ self.init()
+ self.appendCancellable(cancellable, reject: nil)
+ }
+}
+#endif
diff --git a/Sources/CancellableTask.swift b/Sources/CancellableTask.swift
new file mode 100644
index 000000000..28b57f5a4
--- /dev/null
+++ b/Sources/CancellableTask.swift
@@ -0,0 +1,12 @@
+import Dispatch
+
+/**
+ Use this protocol to define cancellable tasks for CancellablePromise.
+ */
+public protocol Cancellable {
+ /// Cancel the associated task
+ func cancel()
+
+ /// `true` if the task was successfully cancelled, `false` otherwise
+ var isCancelled: Bool { get }
+}
diff --git a/Sources/CancellableThenable.swift b/Sources/CancellableThenable.swift
new file mode 100644
index 000000000..646ada924
--- /dev/null
+++ b/Sources/CancellableThenable.swift
@@ -0,0 +1,520 @@
+import Dispatch
+
+/**
+ CancellableThenable represents an asynchronous operation that can be both chained and cancelled. When chained, all CancellableThenable members of the chain are cancelled when `cancel` is called on the associated CancelContext.
+ */
+public protocol CancellableThenable: class {
+ /// Type of the delegate `thenable`
+ associatedtype U: Thenable
+
+ /// Delegate `thenable` for this `CancellableThenable`
+ var thenable: U { get }
+
+ /// The `CancelContext` associated with this `CancellableThenable`
+ var cancelContext: CancelContext { get }
+
+ /// Tracks the cancel items for this `CancellableThenable`. These items are removed from the associated `CancelContext` when the thenable resolves.
+ var cancelItemList: CancelItemList { get }
+}
+
+public extension CancellableThenable {
+ /// Append the `task` and `reject` function for a cancellable task to the cancel context
+ func appendCancellable(_ cancellable: Cancellable?, reject: ((Error) -> Void)?) {
+ self.cancelContext.append(cancellable: cancellable, reject: reject, thenable: self)
+ }
+
+ /// Append the cancel context associated with `from` to our cancel context. Typically `from` is a branch of our chain.
+ func appendCancelContext(from: Z) {
+ self.cancelContext.append(context: from.cancelContext, thenable: self)
+ }
+
+ /**
+ Cancel all members of the promise chain and their associated asynchronous operations.
+
+ - Parameter error: Specifies the cancellation error to use for the cancel operation, defaults to `PMKError.cancelled`
+ */
+ func cancel(with error: Error = PMKError.cancelled) {
+ self.cancelContext.cancel(with: error)
+ }
+
+ /**
+ True if all members of the promise chain have been successfully cancelled, false otherwise.
+ */
+ var isCancelled: Bool {
+ return self.cancelContext.isCancelled
+ }
+
+ /**
+ True if `cancel` has been called on the CancelContext associated with this promise, false otherwise. `cancelAttempted` will be true if `cancel` is called on any promise in the chain.
+ */
+ var cancelAttempted: Bool {
+ return self.cancelContext.cancelAttempted
+ }
+
+ /**
+ The cancellation error generated when the promise is cancelled, or `nil` if not cancelled.
+ */
+ var cancelledError: Error? {
+ return self.cancelContext.cancelledError
+ }
+
+ /**
+ The provided closure executes when this cancellable promise resolves.
+
+ This allows chaining promises. The cancellable promise returned by the provided closure is resolved before the cancellable promise returned by this closure resolves.
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The closure that executes when this cancellable promise fulfills. It must return a cancellable promise.
+ - Returns: A new cancellable promise that resolves when the cancellable promise returned from the provided closure resolves. For example:
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url1)
+ }.cancellize().then { response in
+ transform(data: response.data) // returns a CancellablePromise
+ }.done { transformation in
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func then(on: Dispatcher = conf.D.map, _ body: @escaping (U.T) throws -> V) -> CancellablePromise {
+
+ let cancelItemList = CancelItemList()
+
+ let cancelBody = { (value: U.T) throws -> V.U in
+ if let error = self.cancelContext.removeItems(self.cancelItemList, clearList: true) {
+ throw error
+ } else {
+ let rv = try body(value)
+ self.cancelContext.append(context: rv.cancelContext, thenableCancelItemList: cancelItemList)
+ return rv.thenable
+ }
+ }
+
+ let promise = self.thenable.then(on: on, cancelBody)
+ return CancellablePromise(promise: promise, context: self.cancelContext, cancelItemList: cancelItemList)
+ }
+
+ /**
+ The provided closure executes when this cancellable promise resolves.
+
+ This allows chaining promises. The promise returned by the provided closure is resolved before the cancellable promise returned by this closure resolves.
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The closure that executes when this promise fulfills. It must return a promise (not a cancellable promise).
+ - Returns: A new cancellable promise that resolves when the promise returned from the provided closure resolves. For example:
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url1)
+ }.cancellize().then { response in
+ transform(data: response.data) // returns a Promise
+ }.done { transformation in
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func then(on: Dispatcher = conf.D.map, _ body: @escaping (U.T) throws -> V) -> CancellablePromise {
+ let cancelBody = { (value: U.T) throws -> V in
+ if let error = self.cancelContext.removeItems(self.cancelItemList, clearList: true) {
+ throw error
+ } else {
+ return try body(value)
+ }
+ }
+
+ let promise = self.thenable.then(on: on, cancelBody)
+ return CancellablePromise(promise, cancelContext: self.cancelContext)
+ }
+
+ /**
+ The provided closure is executed when this cancellable promise is resolved.
+
+ This is like `then` but it requires the closure to return a non-promise and non-cancellable-promise.
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter transform: The closure that is executed when this CancellablePromise is fulfilled. It must return a non-promise and non-cancellable-promise.
+ - Returns: A new cancellable promise that is resolved with the value returned from the provided closure. For example:
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url1)
+ }.cancellize().map { response in
+ response.data.length
+ }.done { length in
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func map(on: Dispatcher = conf.D.map, _ transform: @escaping (U.T) throws -> V) -> CancellablePromise {
+ let cancelTransform = { (value: U.T) throws -> V in
+ if let error = self.cancelContext.removeItems(self.cancelItemList, clearList: true) {
+ throw error
+ } else {
+ return try transform(value)
+ }
+ }
+
+ let promise = self.thenable.map(on: on, cancelTransform)
+ return CancellablePromise(promise: promise, context: self.cancelContext)
+ }
+
+ /**
+ The provided closure is executed when this cancellable promise is resolved.
+
+ In your closure return an `Optional`, if you return `nil` the resulting cancellable promise is rejected with `PMKError.compactMap`, otherwise the cancellable promise is fulfilled with the unwrapped value.
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url)
+ }.cancellize().compactMap {
+ try JSONSerialization.jsonObject(with: $0.data) as? [String: String]
+ }.done { dictionary in
+ //…
+ }.catch {
+ // either `PMKError.compactMap` or a `JSONError`
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func compactMap(on: Dispatcher = conf.D.map, _ transform: @escaping (U.T) throws -> V?) -> CancellablePromise {
+ let cancelTransform = { (value: U.T) throws -> V? in
+ if let error = self.cancelContext.removeItems(self.cancelItemList, clearList: true) {
+ throw error
+ } else {
+ return try transform(value)
+ }
+ }
+
+ let promise = self.thenable.compactMap(on: on, cancelTransform)
+ return CancellablePromise(promise: promise, context: self.cancelContext)
+ }
+
+ /**
+ The provided closure is executed when this cancellable promise is resolved.
+
+ Equivalent to `map { x -> Void in`, but since we force the `Void` return Swift
+ is happier and gives you less hassle about your closure’s qualification.
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The closure that is executed when this promise is fulfilled.
+ - Returns: A new cancellable promise fulfilled as `Void`.
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url)
+ }.cancellize().done { response in
+ print(response.data)
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func done(on: Dispatcher = conf.D.return, _ body: @escaping (U.T) throws -> Void) -> CancellablePromise {
+ let cancelBody = { (value: U.T) throws -> Void in
+ if let error = self.cancelContext.removeItems(self.cancelItemList, clearList: true) {
+ throw error
+ } else {
+ try body(value)
+ }
+ }
+
+ let promise = self.thenable.done(on: on, cancelBody)
+ return CancellablePromise(promise: promise, context: self.cancelContext)
+ }
+
+ /**
+ The provided closure is executed when this cancellable promise is resolved.
+
+ This is like `done` but it returns the same value that the handler is fed.
+ `get` immutably accesses the fulfilled value; the returned CancellablePromise maintains that value.
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The closure that is executed when this promise is fulfilled.
+ - Returns: A new cancellable promise that is resolved with the value that the handler is fed. For example:
+
+ let context = firstly {
+ cancellize(Promise.value(1))
+ }.get { foo in
+ print(foo, " is 1")
+ }.done { foo in
+ print(foo, " is 1")
+ }.done { foo in
+ print(foo, " is Void")
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func get(on: Dispatcher = conf.D.return, _ body: @escaping (U.T) throws -> Void) -> CancellablePromise {
+ return map(on: on) {
+ try body($0)
+ return $0
+ }
+ }
+
+ /**
+ The provided closure is executed with cancellable promise result.
+
+ This is like `get` but provides the Result of the CancellablePromise so you can inspect the value of the chain at this point without causing any side effects.
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The closure that is executed with Result of CancellablePromise.
+ - Returns: A new cancellable promise that is resolved with the result that the handler is fed. For example:
+
+ promise.tap{ print($0) }.then{ /*…*/ }
+ */
+ func tap(on: Dispatcher = conf.D.map, _ body: @escaping(Result) -> Void) -> CancellablePromise {
+ let rp = CancellablePromise.pending()
+ rp.promise.cancelContext = self.cancelContext
+ self.thenable.pipe { result in
+ on.dispatch {
+ if let error = self.cancelContext.removeItems(self.cancelItemList, clearList: true) {
+ rp.resolver.reject(error)
+ } else {
+ body(result)
+ rp.resolver.resolve(result)
+ }
+ }
+ }
+ return rp.promise
+ }
+
+ /// - Returns: a new cancellable promise chained off this cancellable promise but with its value discarded.
+ func asVoid() -> CancellablePromise {
+ return map(on: nil) { _ in }
+ }
+}
+
+public extension CancellableThenable {
+ /**
+ - Returns: The error with which this cancellable promise was rejected; `nil` if this promise is not rejected.
+ */
+ var error: Error? {
+ return thenable.error
+ }
+
+ /**
+ - Returns: `true` if the cancellable promise has not yet resolved.
+ */
+ var isPending: Bool {
+ return thenable.isPending
+ }
+
+ /**
+ - Returns: `true` if the cancellable promise has resolved.
+ */
+ var isResolved: Bool {
+ return thenable.isResolved
+ }
+
+ /**
+ - Returns: `true` if the cancellable promise was fulfilled.
+ */
+ var isFulfilled: Bool {
+ return thenable.isFulfilled
+ }
+
+ /**
+ - Returns: `true` if the cancellable promise was rejected.
+ */
+ var isRejected: Bool {
+ return thenable.isRejected
+ }
+
+ /**
+ - Returns: The value with which this cancellable promise was fulfilled or `nil` if this cancellable promise is pending or rejected.
+ */
+ var value: U.T? {
+ return thenable.value
+ }
+}
+
+public extension CancellableThenable where U.T: Sequence {
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `V` => `CancellablePromise<[V]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.mapValues { integer in
+ integer * 2
+ }.done {
+ // $0 => [2,4,6]
+ }
+ */
+ func mapValues(on: Dispatcher = conf.D.map, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V]> {
+ return map(on: on) { try $0.map(transform) }
+ }
+
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `[V]` => `CancellablePromise<[V]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.flatMapValues { integer in
+ [integer, integer]
+ }.done {
+ // $0 => [1,1,2,2,3,3]
+ }
+ */
+ func flatMapValues(on: Dispatcher = conf.D.map, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.Iterator.Element]> {
+ return map(on: on) { (foo: U.T) in
+ try foo.flatMap { try transform($0) }
+ }
+ }
+
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `V?` => `CancellablePromise<[V]>`
+
+ firstly {
+ cancellize(Promise.value(["1","2","a","3"]))
+ }.compactMapValues {
+ Int($0)
+ }.done {
+ // $0 => [1,2,3]
+ }
+ */
+ func compactMapValues(on: Dispatcher = conf.D.map, _ transform: @escaping(U.T.Iterator.Element) throws -> V?) -> CancellablePromise<[V]> {
+ return map(on: on) { foo -> [V] in
+ return try foo.compactMap(transform)
+ }
+ }
+
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `CancellablePromise` => `CancellablePromise<[V]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.thenMap { integer in
+ cancellize(Promise.value(integer * 2))
+ }.done {
+ // $0 => [2,4,6]
+ }
+ */
+ func thenMap(on: Dispatcher = conf.D.map, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.U.T]> {
+ return then(on: on) {
+ when(fulfilled: try $0.map(transform))
+ }
+ }
+
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `Promise` => `CancellablePromise<[V]>`
+
+ firstly {
+ Promise.value([1,2,3])
+ }.cancellize().thenMap { integer in
+ .value(integer * 2)
+ }.done {
+ // $0 => [2,4,6]
+ }
+ */
+ func thenMap(on: Dispatcher = conf.D.map, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.T]> {
+ return then(on: on) {
+ when(fulfilled: try $0.map(transform))
+ }
+ }
+
+ /**
+ `CancellablePromise<[T]>` => `T` -> `CancellablePromise<[U]>` => `CancellablePromise<[U]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.thenFlatMap { integer in
+ cancellize(Promise.value([integer, integer]))
+ }.done {
+ // $0 => [1,1,2,2,3,3]
+ }
+ */
+ func thenFlatMap(on: Dispatcher = conf.D.map, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.U.T.Iterator.Element]> where V.U.T: Sequence {
+ return then(on: on) {
+ when(fulfilled: try $0.map(transform))
+ }.map(on: nil) {
+ $0.flatMap { $0 }
+ }
+ }
+
+ /**
+ `CancellablePromise<[T]>` => `T` -> `Promise<[U]>` => `CancellablePromise<[U]>`
+
+ firstly {
+ Promise.value([1,2,3])
+ }.cancellize().thenFlatMap { integer in
+ .value([integer, integer])
+ }.done {
+ // $0 => [1,1,2,2,3,3]
+ }
+ */
+ func thenFlatMap(on: Dispatcher = conf.D.map, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.T.Iterator.Element]> where V.T: Sequence {
+ return then(on: on) {
+ when(fulfilled: try $0.map(transform))
+ }.map(on: nil) {
+ $0.flatMap { $0 }
+ }
+ }
+
+ /**
+ `CancellablePromise<[T]>` => `T` -> Bool => `CancellablePromise<[U]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.filterValues {
+ $0 > 1
+ }.done {
+ // $0 => [2,3]
+ }
+ */
+ func filterValues(on: Dispatcher = conf.D.map, _ isIncluded: @escaping (U.T.Iterator.Element) -> Bool) -> CancellablePromise<[U.T.Iterator.Element]> {
+ return map(on: on) {
+ $0.filter(isIncluded)
+ }
+ }
+}
+
+public extension CancellableThenable where U.T: Collection {
+ /// - Returns: a cancellable promise fulfilled with the first value of this `Collection` or, if empty, a promise rejected with PMKError.emptySequence.
+ var firstValue: CancellablePromise {
+ return map(on: nil) { aa in
+ if let a1 = aa.first {
+ return a1
+ } else {
+ throw PMKError.emptySequence
+ }
+ }
+ }
+
+ func firstValue(on: Dispatcher = conf.D.map, where test: @escaping (U.T.Iterator.Element) -> Bool) -> CancellablePromise {
+ return map(on: on) {
+ for x in $0 where test(x) {
+ return x
+ }
+ throw PMKError.emptySequence
+ }
+ }
+
+ /// - Returns: a cancellable promise fulfilled with the last value of this `Collection` or, if empty, a promise rejected with PMKError.emptySequence.
+ var lastValue: CancellablePromise {
+ return map(on: nil) { aa in
+ if aa.isEmpty {
+ throw PMKError.emptySequence
+ } else {
+ let i = aa.index(aa.endIndex, offsetBy: -1)
+ return aa[i]
+ }
+ }
+ }
+}
+
+public extension CancellableThenable where U.T: Sequence, U.T.Iterator.Element: Comparable {
+ /// - Returns: a cancellable promise fulfilled with the sorted values of this `Sequence`.
+ func sortedValues(on: Dispatcher = conf.D.map) -> CancellablePromise<[U.T.Iterator.Element]> {
+ return map(on: on) { $0.sorted() }
+ }
+}
diff --git a/Sources/Dispatcher.swift b/Sources/Dispatcher.swift
index 835cb6fd4..0000e5e79 100644
--- a/Sources/Dispatcher.swift
+++ b/Sources/Dispatcher.swift
@@ -541,3 +541,481 @@ public extension CatchMixin where T == Void {
return recover(on: dispatcher, policy: policy, body)
}
}
+
+//////////////////////////////////////////////////////////// Cancellation
+
+public extension CancellableThenable {
+ /**
+ The provided closure executes when this cancellable promise resolves.
+
+ This allows chaining promises. The cancellable promise returned by the provided closure is resolved before the cancellable promise returned by this closure resolves.
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter body: The closure that executes when this cancellable promise fulfills. It must return a cancellable promise.
+ - Returns: A new cancellable promise that resolves when the promise returned from the provided closure resolves. For example:
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url1)
+ }.cancellize().then { response in
+ transform(data: response.data) // returns a CancellablePromise
+ }.done { transformation in
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func then(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (U.T) throws -> V) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return then(on: dispatcher, body)
+ }
+
+ /**
+ The provided closure executes when this cancellable promise resolves.
+
+ This allows chaining promises. The promise returned by the provided closure is resolved before the cancellable promise returned by this closure resolves.
+
+ - Parameter on: The dispatcher that executes the provided closure.
+ - Parameter body: The closure that executes when this cancellable promise fulfills. It must return a promise (not a cancellable promise).
+ - Returns: A new cancellable promise that resolves when the promise returned from the provided closure resolves. For example:
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url1)
+ }.cancellize().then { response in
+ transform(data: response.data) // returns a Promise
+ }.done { transformation in
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func then(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (U.T) throws -> V) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return then(on: dispatcher, body)
+ }
+
+ /**
+ The provided closure is executed when this cancellable promise is resolved.
+
+ This is like `then` but it requires the closure to return a non-promise and non-cancellable-promise.
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter transform: The closure that is executed when this CancellablePromise is fulfilled. It must return a non-promise and non-cancellable-promise.
+ - Returns: A new cancellable promise that is resolved with the value returned from the provided closure. For example:
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url1)
+ }.cancellize().map { response in
+ response.data.length
+ }.done { length in
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func map(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping (U.T) throws -> V) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return map(on: dispatcher, transform)
+ }
+
+ /**
+ The provided closure is executed when this cancellable promise is resolved.
+
+ In your closure return an `Optional`, if you return `nil` the resulting cancellable promise is rejected with `PMKError.compactMap`, otherwise the cancellable promise is fulfilled with the unwrapped value.
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url)
+ }.cancellize().compactMap {
+ try JSONSerialization.jsonObject(with: $0.data) as? [String: String]
+ }.done { dictionary in
+ //…
+ }.catch {
+ // either `PMKError.compactMap` or a `JSONError`
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func compactMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping (U.T) throws -> V?) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return compactMap(on: dispatcher, transform)
+ }
+
+ /**
+ The provided closure is executed when this cancellable promise is resolved.
+
+ Equivalent to `map { x -> Void in`, but since we force the `Void` return Swift
+ is happier and gives you less hassle about your closure’s qualification.
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter body: The closure that is executed when this promise is fulfilled.
+ - Returns: A new cancellable promise fulfilled as `Void`.
+
+ let context = firstly {
+ URLSession.shared.dataTask(.promise, with: url)
+ }.cancellize().done { response in
+ print(response.data)
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func done(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (U.T) throws -> Void) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags)
+ return done(on: dispatcher, body)
+ }
+
+ /**
+ The provided closure is executed when this cancellable promise is resolved.
+
+ This is like `done` but it returns the same value that the handler is fed.
+ `get` immutably accesses the fulfilled value; the returned CancellablePromise maintains that value.
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter body: The closure that is executed when this CancellablePromise is fulfilled.
+ - Returns: A new cancellable promise that is resolved with the value that the handler is fed. For example:
+
+ let context = firstly {
+ cancellize(Promise.value(1))
+ }.get { foo in
+ print(foo, " is 1")
+ }.done { foo in
+ print(foo, " is 1")
+ }.done { foo in
+ print(foo, " is Void")
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+ */
+ func get(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (U.T) throws -> Void) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags)
+ return get(on: dispatcher, body)
+ }
+
+ /**
+ The provided closure is executed with cancellable promise result.
+
+ This is like `get` but provides the Result of the CancellablePromise so you can inspect the value of the chain at this point without causing any side effects.
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter body: The closure that is executed with Result of CancellablePromise.
+ - Returns: A new cancellable promise that is resolved with the result that the handler is fed. For example:
+
+ promise.tap{ print($0) }.then{ /*…*/ }
+ */
+ func tap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Result) -> Void) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return tap(on: dispatcher, body)
+ }
+}
+
+public extension CancellableThenable where U.T: Sequence {
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `V` => `CancellablePromise<[V]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.mapValues { integer in
+ integer * 2
+ }.done {
+ // $0 => [2,4,6]
+ }
+ */
+ func mapValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V]> {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return mapValues(on: dispatcher, transform)
+ }
+
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `[V]` => `CancellablePromise<[V]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.flatMapValues { integer in
+ [integer, integer]
+ }.done {
+ // $0 => [1,1,2,2,3,3]
+ }
+ */
+ func flatMapValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.Iterator.Element]> {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return flatMapValues(on: dispatcher, transform)
+ }
+
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `V?` => `CancellablePromise<[V]>`
+
+ firstly {
+ cancellize(Promise.value(["1","2","a","3"]))
+ }.compactMapValues {
+ Int($0)
+ }.done {
+ // $0 => [1,2,3]
+ }
+ */
+ func compactMapValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(U.T.Iterator.Element) throws -> V?) -> CancellablePromise<[V]> {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return compactMapValues(on: dispatcher, transform)
+ }
+
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `CancellablePromise` => `CancellablePromise<[V]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.thenMap { integer in
+ cancellize(Promise.value(integer * 2))
+ }.done {
+ // $0 => [2,4,6]
+ }
+ */
+ func thenMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.U.T]> {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return thenMap(on: dispatcher, transform)
+ }
+
+ /**
+ `CancellablePromise<[U.T]>` => `U.T` -> `Promise` => `CancellablePromise<[V]>`
+
+ firstly {
+ Promise.value([1,2,3])
+ }.cancellize().thenMap { integer in
+ .value(integer * 2)
+ }.done {
+ // $0 => [2,4,6]
+ }
+ */
+ func thenMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.T]> {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return thenMap(on: dispatcher, transform)
+ }
+
+ /**
+ `CancellablePromise<[T]>` => `T` -> `CancellablePromise<[U]>` => `CancellablePromise<[U]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.thenFlatMap { integer in
+ cancellize(Promise.value([integer, integer]))
+ }.done {
+ // $0 => [1,1,2,2,3,3]
+ }
+ */
+ func thenFlatMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.U.T.Iterator.Element]> where V.U.T: Sequence {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return thenFlatMap(on: dispatcher, transform)
+ }
+
+ /**
+ `CancellablePromise<[T]>` => `T` -> `Promise<[U]>` => `CancellablePromise<[U]>`
+
+ firstly {
+ Promise.value([1,2,3])
+ }.cancellize().thenFlatMap { integer in
+ .value([integer, integer])
+ }.done {
+ // $0 => [1,1,2,2,3,3]
+ }
+ */
+ func thenFlatMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(U.T.Iterator.Element) throws -> V) -> CancellablePromise<[V.T.Iterator.Element]> where V.T: Sequence {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return thenFlatMap(on: dispatcher, transform)
+ }
+
+ /**
+ `CancellablePromise<[T]>` => `T` -> Bool => `CancellablePromise<[U]>`
+
+ firstly {
+ cancellize(Promise.value([1,2,3]))
+ }.filterValues {
+ $0 > 1
+ }.done {
+ // $0 => [2,3]
+ }
+ */
+ func filterValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ isIncluded: @escaping (U.T.Iterator.Element) -> Bool) -> CancellablePromise<[U.T.Iterator.Element]> {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return filterValues(on: dispatcher, isIncluded)
+ }
+}
+
+public extension CancellableThenable where U.T: Collection {
+ func firstValue(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, where test: @escaping (U.T.Iterator.Element) -> Bool) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return firstValue(on: dispatcher, where: test)
+ }
+}
+
+public extension CancellableThenable where U.T: Sequence, U.T.Iterator.Element: Comparable {
+ /// - Returns: a cancellable promise fulfilled with the sorted values of this `Sequence`.
+ func sortedValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil) -> CancellablePromise<[U.T.Iterator.Element]> {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return sortedValues(on: dispatcher)
+ }
+}
+
+public extension CancellableCatchMixin {
+ /**
+ The provided closure executes when this cancellable promise rejects.
+
+ Rejecting a promise cascades: rejecting all subsequent promises (unless
+ recover is invoked) thus you will typically place your catch at the end
+ of a chain. Often utility promises will not have a catch, instead
+ delegating the error handling to the caller.
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter policy: The default policy does not execute your handler for cancellation errors.
+ - Parameter body: The handler to execute if this promise is rejected.
+ - Returns: A promise finalizer.
+ - SeeAlso: [Cancellation](https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#cancellation)
+ */
+ @discardableResult
+ func `catch`(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> CancellableFinalizer {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags)
+ return `catch`(on: dispatcher, policy: policy, body)
+ }
+
+ /**
+ The provided closure executes when this cancellable promise rejects.
+
+ Unlike `catch`, `recover` continues the chain.
+ Use `recover` in circumstances where recovering the chain from certain errors is a possibility. For example:
+
+ let context = firstly {
+ CLLocationManager.requestLocation()
+ }.recover { error in
+ guard error == CLError.unknownLocation else { throw error }
+ return .value(CLLocation.chicago)
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter policy: The default policy does not execute your handler for cancellation errors.
+ - Parameter body: The handler to execute if this promise is rejected.
+ - SeeAlso: [Cancellation](https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#cancellation)
+ */
+ func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> V) -> CancellablePromise where V.U.T == C.T {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return recover(on: dispatcher, policy: policy, body)
+ }
+
+ /**
+ The provided closure executes when this cancellable promise rejects.
+
+ Unlike `catch`, `recover` continues the chain.
+ Use `recover` in circumstances where recovering the chain from certain errors is a possibility. For example:
+
+ let context = firstly {
+ CLLocationManager.requestLocation()
+ }.cancellize().recover { error in
+ guard error == CLError.unknownLocation else { throw error }
+ return .value(CLLocation.chicago)
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter policy: The default policy does not execute your handler for cancellation errors.
+ - Parameter body: The handler to execute if this promise is rejected.
+ - SeeAlso: [Cancellation](https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#cancellation)
+ - Note: Methods with the `cancellable` prefix create a new CancellablePromise, and those without the `cancellable` prefix accept an existing CancellablePromise.
+ */
+ func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> V) -> CancellablePromise where V.T == C.T {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return recover(on: dispatcher, body)
+ }
+
+ /**
+ The provided closure executes when this cancellable promise resolves, whether it rejects or not.
+
+ let context = firstly {
+ UIApplication.shared.networkActivityIndicatorVisible = true
+ //… returns a cancellable promise
+ }.done {
+ //…
+ }.ensure {
+ UIApplication.shared.networkActivityIndicatorVisible = false
+ }.catch {
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter body: The closure that executes when this promise resolves.
+ - Returns: A new promise, resolved with this promise’s resolution.
+ */
+ func ensure(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags)
+ return ensure(on: dispatcher, body)
+ }
+
+ /**
+ The provided closure executes when this cancellable promise resolves, whether it rejects or not.
+ The chain waits on the returned `CancellablePromise`.
+
+ let context = firstly {
+ setup() // returns a cancellable promise
+ }.done {
+ //…
+ }.ensureThen {
+ teardown() // -> CancellablePromise
+ }.catch {
+ //…
+ }.cancelContext
+
+ //…
+
+ context.cancel()
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter body: The closure that executes when this promise resolves.
+ - Returns: A new cancellable promise, resolved with this promise’s resolution.
+ */
+ func ensureThen(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> CancellablePromise) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags)
+ return ensureThen(on: dispatcher, body)
+ }
+}
+
+public extension CancellableFinalizer {
+ /// `finally` is the same as `ensure`, but it is not chainable
+ @discardableResult
+ func finally(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) -> CancelContext {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags)
+ return finally(on: dispatcher, body)
+ }
+}
+
+public extension CancellableCatchMixin where C.T == Void {
+ /**
+ The provided closure executes when this cancellable promise rejects.
+
+ This variant of `recover` ensures that no error is thrown from the handler and allows specifying a catch policy.
+
+ - Parameter on: The queue to which the provided closure dispatches.
+ - Parameter policy: The default policy does not execute your handler for cancellation errors.
+ - Parameter body: The handler to execute if this promise is rejected.
+ - SeeAlso: [Cancellation](https://github.com/mxcl/PromiseKit/blob/master/Documentation/CommonPatterns.md#cancellation)
+ */
+ func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> Void) -> CancellablePromise {
+ let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags)
+ return recover(on: dispatcher, policy: policy, body)
+ }
+}
diff --git a/Sources/Error.swift b/Sources/Error.swift
index 7229e6f49..1b8238a69 100644
--- a/Sources/Error.swift
+++ b/Sources/Error.swift
@@ -18,7 +18,10 @@ public enum PMKError: Error {
/// The operation was cancelled
case cancelled
-
+
+ /// The operation timed out and was cancelled
+ case timedOut
+
/// `nil` was returned from `flatMap`
@available(*, deprecated, message: "See: `compactMap`")
case flatMap(Any, Any.Type)
@@ -49,6 +52,8 @@ extension PMKError: CustomDebugStringConvertible {
return "Bad input was provided to a PromiseKit function"
case .cancelled:
return "The asynchronous sequence was cancelled"
+ case .timedOut:
+ return "The asynchronous sequence timed out"
case .emptySequence:
return "The first or last element was requested for an empty sequence"
}
@@ -76,6 +81,8 @@ extension Error {
throw self
} catch PMKError.cancelled {
return true
+ } catch PMKError.timedOut {
+ return true
} catch let error as CancellableError {
return error.isCancelled
} catch URLError.cancelled {
diff --git a/Sources/Guarantee.swift b/Sources/Guarantee.swift
index d007f8214..a33e87d85 100644
--- a/Sources/Guarantee.swift
+++ b/Sources/Guarantee.swift
@@ -23,6 +23,12 @@ public final class Guarantee: Thenable {
body(box.seal)
}
+ /// Returns a pending `Guarantee` that can be resolved with the provided closure’s parameter.
+ public convenience init(cancellable: Cancellable, resolver body: (@escaping(T) -> Void) -> Void) {
+ self.init(resolver: body)
+ setCancellable(cancellable)
+ }
+
/// - See: `Thenable.pipe`
public func pipe(to: @escaping(Result) -> Void) {
pipe{ to(.success($0)) }
@@ -55,10 +61,13 @@ public final class Guarantee: Thenable {
}
final private class Box: EmptyBox {
+ var cancelled = false
deinit {
switch inspect() {
case .pending:
- PromiseKit.conf.logHandler(.pendingGuaranteeDeallocated)
+ if !cancelled {
+ PromiseKit.conf.logHandler(.pendingGuaranteeDeallocated)
+ }
case .resolved:
break
}
@@ -73,6 +82,35 @@ public final class Guarantee: Thenable {
public class func pending() -> (guarantee: Guarantee, resolve: (T) -> Void) {
return { ($0, $0.box.seal) }(Guarantee(.pending))
}
+
+ var cancellable: Cancellable?
+
+ public func setCancellable(_ cancellable: Cancellable) {
+ if let gb = (box as? Guarantee.Box) {
+ self.cancellable = CancellableWrapper(box: gb, cancellable: cancellable)
+ } else {
+ self.cancellable = cancellable
+ }
+ }
+
+ final private class CancellableWrapper: Cancellable {
+ let box: Guarantee.Box
+ let cancellable: Cancellable
+
+ init(box: Guarantee.Box, cancellable: Cancellable) {
+ self.box = box
+ self.cancellable = cancellable
+ }
+
+ func cancel() {
+ box.cancelled = true
+ cancellable.cancel()
+ }
+
+ var isCancelled: Bool {
+ return cancellable.isCancelled
+ }
+ }
}
public extension Guarantee {
diff --git a/Sources/Promise.swift b/Sources/Promise.swift
index 792f2d4da..74e7d1db6 100644
--- a/Sources/Promise.swift
+++ b/Sources/Promise.swift
@@ -64,6 +64,19 @@ public final class Promise: Thenable, CatchMixin {
}
}
+ /// Initialize a new promise that can be resolved with the provided `Resolver`.
+ public init(cancellable: Cancellable, resolver body: (Resolver) throws -> Void) {
+ box = EmptyBox()
+ let resolver = Resolver(box)
+ self.cancellable = cancellable
+ self.rejectIfCancelled = resolver.reject
+ do {
+ try body(resolver)
+ } catch {
+ resolver.reject(error)
+ }
+ }
+
/// - Returns: a tuple of a new pending promise and its `Resolver`.
public class func pending() -> (promise: Promise, resolver: Resolver) {
return { ($0, Resolver($0.box)) }(Promise(.pending))
@@ -99,6 +112,14 @@ public final class Promise: Thenable, CatchMixin {
init(_: PMKUnambiguousInitializer) {
box = EmptyBox()
}
+
+ var cancellable: Cancellable?
+ var rejectIfCancelled: ((Error) -> Void)?
+
+ public func setCancellable(_ cancellable: Cancellable?, reject: ((Error) -> Void)? = nil) {
+ self.cancellable = cancellable
+ rejectIfCancelled = reject
+ }
}
public extension Promise {
diff --git a/Sources/Thenable.swift b/Sources/Thenable.swift
index 055d956f9..98206116f 100644
--- a/Sources/Thenable.swift
+++ b/Sources/Thenable.swift
@@ -274,6 +274,18 @@ public extension Thenable {
}
}
+public extension Thenable {
+ /**
+ Converts a Promise or Guarantee into a promise that can be cancelled.
+ - Parameter thenable: The Thenable (Promise or Guarantee) to be made cancellable.
+ - Returns: A CancellablePromise that is a cancellable variant of the given Promise or Guarantee.
+ */
+ func cancellize(cancelContext: CancelContext? = nil) -> CancellablePromise {
+ return CancellablePromise(self, cancelContext: cancelContext)
+ }
+}
+
+
public extension Thenable where T: Sequence {
/**
`Promise<[T]>` => `T` -> `U` => `Promise<[U]>`
diff --git a/Sources/after.swift b/Sources/after.swift
index ca31911fc..eadb0aa6f 100644
--- a/Sources/after.swift
+++ b/Sources/after.swift
@@ -1,17 +1,25 @@
import struct Foundation.TimeInterval
import Dispatch
+
+/// Extend DispatchWorkItem to be cancellable
+extension DispatchWorkItem: Cancellable { }
+
/**
after(seconds: 1.5).then {
//…
}
- Returns: A guarantee that resolves after the specified duration.
+- Note: cancelling this guarantee will cancel the underlying timer task
+- SeeAlso: [Cancellation](http://promisekit.org/docs/)
*/
public func after(seconds: TimeInterval) -> Guarantee {
let (rg, seal) = Guarantee.pending()
let when = DispatchTime.now() + seconds
- q.asyncAfter(deadline: when, execute: { seal(()) })
+ let task = DispatchWorkItem { seal(()) }
+ rg.setCancellable(task)
+ q.asyncAfter(deadline: when, execute: task)
return rg
}
@@ -21,11 +29,15 @@ public func after(seconds: TimeInterval) -> Guarantee {
}
- Returns: A guarantee that resolves after the specified duration.
+ - Note: cancelling this guarantee will cancel the underlying timer task
+ - SeeAlso: [Cancellation](http://promisekit.org/docs/)
*/
public func after(_ interval: DispatchTimeInterval) -> Guarantee {
let (rg, seal) = Guarantee.pending()
let when = DispatchTime.now() + interval
- q.asyncAfter(deadline: when, execute: { seal(()) })
+ let task = DispatchWorkItem { seal(()) }
+ rg.setCancellable(task)
+ q.asyncAfter(deadline: when, execute: task)
return rg
}
diff --git a/Sources/firstly.swift b/Sources/firstly.swift
index a5b477da1..e5db90f50 100644
--- a/Sources/firstly.swift
+++ b/Sources/firstly.swift
@@ -37,3 +37,53 @@ public func firstly(execute body: () throws -> U) -> Promise {
public func firstly(execute body: () -> Guarantee) -> Guarantee {
return body()
}
+
+//////////////////////////////////////////////////////////// Cancellation
+
+/**
+ `firstly` for cancellable promises.
+
+ Compare:
+
+ let context = URLSession.shared.dataTask(url: url1).cancellize().then {
+ URLSession.shared.dataTask(url: url2)
+ }.then {
+ URLSession.shared.dataTask(url: url3)
+ }.cancelContext
+
+ // …
+
+ context.cancel()
+
+ With:
+
+ let context = firstly {
+ URLSession.shared.dataTask(url: url1)
+ }.cancellize().then {
+ URLSession.shared.dataTask(url: url2)
+ }.then {
+ URLSession.shared.dataTask(url: url3)
+ }.cancelContext
+
+ // …
+
+ context.cancel()
+
+ - Note: the block you pass excecutes immediately on the current thread/queue.
+ - See: firstly(execute: () -> Thenable)
+*/
+public func firstly(execute body: () throws -> V) -> CancellablePromise {
+ do {
+ let rv = try body()
+ let rp: CancellablePromise
+ if let promise = rv as? CancellablePromise {
+ rp = promise
+ } else {
+ rp = CancellablePromise(rv.thenable)
+ }
+ rp.appendCancelContext(from: rv)
+ return rp
+ } catch {
+ return CancellablePromise(error: error)
+ }
+}
diff --git a/Sources/hang.swift b/Sources/hang.swift
index 4831a73cd..7dda20736 100644
--- a/Sources/hang.swift
+++ b/Sources/hang.swift
@@ -49,3 +49,14 @@ public func hang(_ promise: Promise) throws -> T {
return value
}
}
+
+//////////////////////////////////////////////////////////// Cancellation
+
+/**
+ Runs the active run-loop until the provided promise resolves.
+
+ Simply calls `hang` directly on the delegate promise, so the behavior is exactly the same with Promise and CancellablePromise.
+ */
+public func hang(_ promise: CancellablePromise) throws -> T {
+ return try hang(promise.promise)
+}
diff --git a/Sources/race.swift b/Sources/race.swift
index 2b817de26..7b55a0178 100644
--- a/Sources/race.swift
+++ b/Sources/race.swift
@@ -1,3 +1,5 @@
+import struct Foundation.TimeInterval
+
@inline(__always)
private func _race(_ thenables: [U]) -> Promise {
let rp = Promise(.pending)
@@ -55,3 +57,88 @@ public func race(_ guarantees: Guarantee...) -> Guarantee {
}
return rg
}
+
+//////////////////////////////////////////////////////////// Cancellation
+
+/**
+ Resolves with the first resolving cancellable promise from a set of cancellable promises. Calling
+ `cancel` on the race promise cancels all pending promises. All promises will be cancelled if any
+ promise rejects.
+
+ let racePromise = race(promise1, promise2, promise3).then { winner in
+ //…
+ }
+
+ //…
+
+ racePromise.cancel()
+
+ - Returns: A new promise that resolves when the first promise in the provided promises resolves.
+ - Warning: If any of the provided promises reject, the returned promise is rejected.
+ - Warning: aborts if the array is empty.
+*/
+public func race(_ thenables: V...) -> CancellablePromise {
+ return race(thenables)
+}
+
+/**
+ Resolves with the first resolving promise from a set of promises. Calling `cancel` on the race
+ promise cancels all pending promises. All promises will be cancelled if any promise rejects.
+
+ let racePromise = race(promise1, promise2, promise3).then { winner in
+ //…
+ }
+
+ //…
+
+ racePromise.cancel()
+
+ - Returns: A new promise that resolves when the first promise in the provided promises resolves.
+ - Warning: If any of the provided promises reject, the returned promise is rejected.
+ - Remark: Returns promise rejected with PMKError.badInput if empty array provided
+*/
+public func race(_ thenables: [V]) -> CancellablePromise {
+ guard !thenables.isEmpty else {
+ return CancellablePromise(error: PMKError.badInput)
+ }
+
+ let cancelThenables: (Result) -> Void = { result in
+ if case .failure = result {
+ for t in thenables {
+ if !t.cancelAttempted {
+ t.cancel()
+ }
+ }
+ }
+ }
+
+ let promise = CancellablePromise(race(asThenables(thenables)))
+ for t in thenables {
+ t.thenable.pipe(to: cancelThenables)
+ promise.appendCancelContext(from: t)
+ }
+ return promise
+}
+
+/**
+ Returns a promise that can be used to set a timeout for `race`.
+
+ let promise1, promise2: Promise
+ race(promise1, promise2, timeout(seconds: 1.0)).done { winner in
+ //…
+ }.catch(policy: .allErrors) {
+ // Rejects with `PMKError.timedOut` if the timeout is exceeded before either `promise1` or
+ // `promise2` succeeds.
+ }
+
+ When used with cancellable promises, all promises will be cancelled if the timeout is
+ exceeded or any promise rejects:
+
+ let promise1, promise2: CancellablePromise
+ race(promise1, promise2, cancellize(timeout(seconds: 1.0))).done { winner in
+ //…
+ }
+ */
+public func timeout(seconds: TimeInterval) -> Promise {
+ return after(seconds: seconds).done { throw PMKError.timedOut }
+}
diff --git a/Sources/when.swift b/Sources/when.swift
index ed654e0c5..6ab4f6b53 100644
--- a/Sources/when.swift
+++ b/Sources/when.swift
@@ -106,7 +106,7 @@ public func when Promise {
- // ...
+ // …
}
let urls: [URL] = /*…*/
@@ -120,7 +120,7 @@ public func when...) -> Guarantee {
public func when(guarantees: [Guarantee]) -> Guarantee {
return when(fulfilled: guarantees).recover{ _ in }.asVoid()
}
+
+//////////////////////////////////////////////////////////// Cancellation
+
+/**
+ Wait for all cancellable promises in a set to fulfill.
+
+ For example:
+
+ let p = when(fulfilled: promise1, promise2).then { results in
+ //…
+ }.catch { error in
+ switch error {
+ case URLError.notConnectedToInternet:
+ //…
+ case CLError.denied:
+ //…
+ }
+ }
+
+ //…
+
+ p.cancel()
+
+ - Note: If *any* of the provided promises reject, the returned promise is immediately rejected with that error.
+ - Warning: In the event of rejection the other promises will continue to resolve and, as per any other promise, will either fulfill or reject. This is the right pattern for `getter` style asynchronous tasks, but often for `setter` tasks (eg. storing data on a server), you most likely will need to wait on all tasks and then act based on which have succeeded and which have failed, in such situations use `when(resolved:)`.
+ - Parameter promises: The promises upon which to wait before the returned promise resolves.
+ - Returns: A new promise that resolves when all the provided promises fulfill or one of the provided promises rejects.
+ - Note: `when` provides `NSProgress`.
+ - SeeAlso: `when(resolved:)`
+*/
+public func when(fulfilled thenables: V...) -> CancellablePromise<[V.U.T]> {
+ let rp = CancellablePromise(when(fulfilled: asThenables(thenables)))
+ for t in thenables {
+ rp.appendCancelContext(from: t)
+ }
+ return rp
+}
+
+public func when(fulfilled thenables: [V]) -> CancellablePromise<[V.U.T]> {
+ let rp = CancellablePromise(when(fulfilled: asThenables(thenables)))
+ for t in thenables {
+ rp.appendCancelContext(from: t)
+ }
+ return rp
+}
+
+/// Wait for all cancellable promises in a set to fulfill.
+public func when(fulfilled promises: V...) -> CancellablePromise where V.U.T == Void {
+ let rp = CancellablePromise(when(fulfilled: asThenables(promises)))
+ for p in promises {
+ rp.appendCancelContext(from: p)
+ }
+ return rp
+}
+
+/// Wait for all cancellable promises in a set to fulfill.
+public func when(fulfilled promises: [V]) -> CancellablePromise where V.U.T == Void {
+ let rp = CancellablePromise(when(fulfilled: asThenables(promises)))
+ for p in promises {
+ rp.appendCancelContext(from: p)
+ }
+ return rp
+}
+
+/**
+ Wait for all cancellable promises in a set to fulfill.
+
+ - Note: by convention the cancellable 'when' functions should not have a 'cancellable' prefix, however the prefix is necessary due to a compiler bug exemplified by the following:
+
+ ````
+ This works fine:
+ 1 func hi(_: String...) { }
+ 2 func hi(_: String, _: String) { }
+ 3 hi("hi", "there")
+
+ This does not compile:
+ 1 func hi(_: String...) { }
+ 2 func hi(_: String, _: String) { }
+ 3 func hi(_: Int...) { }
+ 4 func hi(_: Int, _: Int) { }
+ 5
+ 6 hi("hi", "there") // Ambiguous use of 'hi' (lines 1 & 2 are candidates)
+ 7 hi(1, 2) // Ambiguous use of 'hi' (lines 3 & 4 are candidates)
+ ````
+
+ - SeeAlso: `when(fulfilled:,_:)`
+*/
+public func cancellableWhen(fulfilled pu: U, _ pv: V) -> CancellablePromise<(U.U.T, V.U.T)> {
+ return when(fulfilled: [pu.asVoid(), pv.asVoid()]).map(on: nil) { (pu.value!, pv.value!) }
+}
+
+/// Wait for all cancellable promises in a set to fulfill.
+/// - SeeAlso: `when(fulfilled:,_:)`
+public func cancellableWhen(fulfilled pu: U, _ pv: V, _ pw: W) -> CancellablePromise<(U.U.T, V.U.T, W.U.T)> {
+ return when(fulfilled: [pu.asVoid(), pv.asVoid(), pw.asVoid()]).map(on: nil) { (pu.value!, pv.value!, pw.value!) }
+}
+
+/// Wait for all cancellable promises in a set to fulfill.
+/// - SeeAlso: `when(fulfilled:,_:)`
+public func cancellableWhen(fulfilled pu: U, _ pv: V, _ pw: W, _ px: X) -> CancellablePromise<(U.U.T, V.U.T, W.U.T, X.U.T)> {
+ return when(fulfilled: [pu.asVoid(), pv.asVoid(), pw.asVoid(), px.asVoid()]).map(on: nil) { (pu.value!, pv.value!, pw.value!, px.value!) }
+}
+
+/// Wait for all cancellable promises in a set to fulfill.
+/// - SeeAlso: `when(fulfilled:,_:)`
+public func cancellableWhen(fulfilled pu: U, _ pv: V, _ pw: W, _ px: X, _ py: Y) -> CancellablePromise<(U.U.T, V.U.T, W.U.T, X.U.T, Y.U.T)> {
+ return when(fulfilled: [pu.asVoid(), pv.asVoid(), pw.asVoid(), px.asVoid(), py.asVoid()]).map(on: nil) { (pu.value!, pv.value!, pw.value!, px.value!, py.value!) }
+}
+
+/**
+ Generate cancellable promises at a limited rate and wait for all to fulfill. Call `cancel` on the returned promise to cancel all currently pending promises.
+
+ For example:
+
+ func downloadFile(url: URL) -> CancellablePromise {
+ // …
+ }
+
+ let urls: [URL] = /*…*/
+ let urlGenerator = urls.makeIterator()
+
+ let generator = AnyIterator> {
+ guard url = urlGenerator.next() else {
+ return nil
+ }
+ return downloadFile(url)
+ }
+
+ let promise = when(generator, concurrently: 3).done { datas in
+ // …
+ }
+
+ // …
+
+ promise.cancel()
+
+
+ No more than three downloads will occur simultaneously.
+
+ - Note: The generator is called *serially* on a *background* queue.
+ - Warning: Refer to the warnings on `when(fulfilled:)`
+ - Parameter promiseGenerator: Generator of promises.
+ - Parameter cancel: Optional cancel context, overrides the default context.
+ - Returns: A new promise that resolves when all the provided promises fulfill or one of the provided promises rejects.
+ - SeeAlso: `when(resolved:)`
+ */
+public func when(fulfilled promiseIterator: It, concurrently: Int) -> CancellablePromise<[It.Element.U.T]> where It.Element: CancellableThenable {
+ guard concurrently > 0 else {
+ return CancellablePromise(error: PMKError.badInput)
+ }
+
+ var pi = promiseIterator
+ var generatedPromises: [CancellablePromise] = []
+ var rootPromise: CancellablePromise<[It.Element.U.T]>!
+
+ let generator = AnyIterator> {
+ guard let promise = pi.next() as? CancellablePromise else {
+ return nil
+ }
+ if let root = rootPromise {
+ root.appendCancelContext(from: promise)
+ } else {
+ generatedPromises.append(promise)
+ }
+ return promise.promise
+ }
+
+ rootPromise = CancellablePromise(when(fulfilled: generator, concurrently: concurrently))
+ for p in generatedPromises {
+ rootPromise.appendCancelContext(from: p)
+ }
+ return rootPromise
+}
+
+/**
+ Waits on all provided cancellable promises.
+
+ `when(fulfilled:)` rejects as soon as one of the provided promises rejects. `when(resolved:)` waits on all provided promises and *never* rejects. When cancelled, all promises will attempt to be cancelled and those that are successfully cancelled will have a result of
+ PMKError.cancelled.
+
+ let p = when(resolved: promise1, promise2, promise3, cancel: context).then { results in
+ for result in results where case .fulfilled(let value) {
+ //…
+ }
+ }.catch { error in
+ // invalid! Never rejects
+ }
+
+ //…
+
+ p.cancel()
+
+ - Returns: A new promise that resolves once all the provided promises resolve. The array is ordered the same as the input, ie. the result order is *not* resolution order.
+ - Note: Any promises that error are implicitly consumed.
+ - Remark: Doesn't take CancellableThenable due to protocol associatedtype paradox
+*/
+public func when(resolved promises: CancellablePromise...) -> CancellablePromise<[Result]> {
+ return when(resolved: promises)
+}
+
+/// Waits on all provided cancellable promises.
+/// - SeeAlso: `when(resolved:)`
+public func when(resolved promises: [CancellablePromise]) -> CancellablePromise<[Result]> {
+ let rp = CancellablePromise(when(resolved: asPromises(promises)))
+ for p in promises {
+ rp.appendCancelContext(from: p)
+ }
+ return rp
+}
+
+func asThenables(_ cancellableThenables: [V]) -> [V.U] {
+ var thenables: [V.U] = []
+ for ct in cancellableThenables {
+ thenables.append(ct.thenable)
+ }
+ return thenables
+}
+
+func asPromises(_ cancellablePromises: [CancellablePromise]) -> [Promise] {
+ var promises = [Promise]()
+ for cp in cancellablePromises {
+ promises.append(cp.promise)
+ }
+ return promises
+}
diff --git a/Tests/Cancel/AfterTests.swift b/Tests/Cancel/AfterTests.swift
new file mode 100644
index 000000000..27460ea9b
--- /dev/null
+++ b/Tests/Cancel/AfterTests.swift
@@ -0,0 +1,121 @@
+import Foundation
+import XCTest
+import PromiseKit
+
+extension XCTestExpectation {
+ open func fulfill(error: Error) {
+ fulfill()
+ }
+}
+
+class AfterTests: XCTestCase {
+ func fail() { XCTFail() }
+
+ func testZero() {
+ let ex2 = expectation(description: "")
+ let cc2 = after(seconds: 0).cancellize().done(fail).catch(policy: .allErrors, ex2.fulfill)
+ cc2.cancel()
+ waitForExpectations(timeout: 2, handler: nil)
+
+ let ex3 = expectation(description: "")
+ let cc3 = after(.seconds(0)).cancellize().done(fail).catch(policy: .allErrors, ex3.fulfill)
+ cc3.cancel()
+ waitForExpectations(timeout: 2, handler: nil)
+ }
+
+ func testNegative() {
+ let ex2 = expectation(description: "")
+ let cc2 = after(seconds: -1).cancellize().done(fail).catch(policy: .allErrors, ex2.fulfill)
+ cc2.cancel()
+ waitForExpectations(timeout: 2, handler: nil)
+
+ let ex3 = expectation(description: "")
+ let cc3 = after(.seconds(-1)).cancellize().done(fail).catch(policy: .allErrors, ex3.fulfill)
+ cc3.cancel()
+ waitForExpectations(timeout: 2, handler: nil)
+ }
+
+ func testPositive() {
+ let ex2 = expectation(description: "")
+ let cc2 = after(seconds: 1).cancellize().done(fail).catch(policy: .allErrors, ex2.fulfill)
+ cc2.cancel()
+ waitForExpectations(timeout: 2, handler: nil)
+
+ let ex3 = expectation(description: "")
+ let cc3 = after(.seconds(1)).cancellize().done(fail).catch(policy: .allErrors, ex3.fulfill)
+ cc3.cancel()
+ waitForExpectations(timeout: 2, handler: nil)
+ }
+
+ func testCancellableAfter() {
+ // This is an example of a functional test case.
+ // Use XCTAssert and related functions to verify your tests produce the correct results.
+
+ // Test the normal 'after' function
+ let exComplete = expectation(description: "after completes")
+ let afterPromise = after(seconds: 0)
+ afterPromise.done {
+ exComplete.fulfill()
+ }.catch { error in
+ XCTFail("afterPromise failed with error: \(error)")
+ }
+
+ let exCancelComplete = expectation(description: "after completes")
+
+ // Test cancellable `after` to ensure it is fulfilled if not cancelled
+ let cancelIgnoreAfterPromise = after(seconds: 0).cancellize()
+ cancelIgnoreAfterPromise.done {
+ exCancelComplete.fulfill()
+ }.catch(policy: .allErrors) { error in
+ XCTFail("cancelIgnoreAfterPromise failed with error: \(error)")
+ }
+
+ // Test cancellable `after` to ensure it is cancelled
+ let cancellableAfterPromise = after(seconds: 0).cancellize()
+ cancellableAfterPromise.done {
+ XCTFail("cancellableAfter not cancelled")
+ }.catch(policy: .allErrorsExceptCancellation) { error in
+ XCTFail("cancellableAfterPromise failed with error: \(error)")
+ }.cancel()
+
+ // Test cancellable `after` to ensure it is cancelled and throws a `CancellableError`
+ let exCancel = expectation(description: "after cancels")
+ let cancellableAfterPromiseWithError = after(seconds: 0).cancellize()
+ cancellableAfterPromiseWithError.done {
+ XCTFail("cancellableAfterWithError not cancelled")
+ }.catch(policy: .allErrors) { error in
+ error.isCancelled ? exCancel.fulfill() : XCTFail("unexpected error \(error)")
+ }.cancel()
+
+ wait(for: [exComplete, exCancelComplete, exCancel], timeout: 1)
+ }
+
+ func testCancelForPromise_Done() {
+ let exComplete = expectation(description: "done is cancelled")
+
+ let promise = CancellablePromise { seal in
+ seal.fulfill(())
+ }
+ promise.done { _ in
+ XCTFail("done not cancelled")
+ }.catch(policy: .allErrors) { error in
+ error.isCancelled ? exComplete.fulfill() : XCTFail("error: \(error)")
+ }
+
+ promise.cancel()
+
+ wait(for: [exComplete], timeout: 1)
+ }
+
+ func testCancelForGuarantee_Done() {
+ let exComplete = expectation(description: "done is cancelled")
+
+ after(seconds: 0).cancellize().done { _ in
+ XCTFail("done not cancelled")
+ }.catch(policy: .allErrors) { error in
+ error.isCancelled ? exComplete.fulfill() : XCTFail("error: \(error)")
+ }.cancel()
+
+ wait(for: [exComplete], timeout: 1)
+ }
+}
diff --git a/Tests/Cancel/CancelChain.swift b/Tests/Cancel/CancelChain.swift
new file mode 100644
index 000000000..e2345637d
--- /dev/null
+++ b/Tests/Cancel/CancelChain.swift
@@ -0,0 +1,246 @@
+import XCTest
+import PromiseKit
+
+class CancelChain: XCTestCase {
+ // Using a distinct type for each promise so we can tell which promise is which when using trace messages inside Thenable
+ struct A { }
+ struct B { }
+ struct C { }
+ struct D { }
+ struct E { }
+
+ struct Chain {
+ let pA: CancellablePromise
+ let pB: CancellablePromise
+ let pC: CancellablePromise
+ let pD: CancellablePromise
+ let pE: CancellablePromise
+ }
+
+ func trace(_ message: String) {
+ // print(message)
+ }
+
+ func cancelChainPromises() -> Chain {
+ let pA = CancellablePromise { seal in
+ self.trace("A IN")
+ after(seconds: 0.05).cancellize().done {
+ self.trace("A FULFILL")
+ seal.fulfill(A())
+ }.catch(policy: .allErrors) {
+ self.trace("A ERR")
+ seal.reject($0)
+ }
+ }
+
+ let pB = CancellablePromise { seal in
+ self.trace("B IN")
+ after(seconds: 0.1).cancellize().done {
+ self.trace("B FULFILL")
+ seal.fulfill(B())
+ }.catch(policy: .allErrors) {
+ self.trace("B ERR")
+ seal.reject($0)
+ }
+ }
+
+ let pC = CancellablePromise { seal in
+ self.trace("C IN")
+ after(seconds: 0.15).cancellize().done {
+ self.trace("C FULFILL")
+ seal.fulfill(C())
+ }.catch(policy: .allErrors) {
+ self.trace("C ERR")
+ seal.reject($0)
+ }
+ }
+
+ let pD = CancellablePromise { seal in
+ self.trace("D IN")
+ after(seconds: 0.2).cancellize().done {
+ self.trace("D FULFILL")
+ seal.fulfill(D())
+ }.catch(policy: .allErrors) {
+ self.trace("D ERR")
+ seal.reject($0)
+ }
+ }
+
+ let pE = CancellablePromise { seal in
+ self.trace("E IN")
+ after(seconds: 0.25).cancellize().done {
+ self.trace("E FULFILL")
+ seal.fulfill(E())
+ }.catch(policy: .allErrors) {
+ self.trace("E ERR")
+ seal.reject($0)
+ }
+ }
+
+ return Chain(pA: pA, pB: pB, pC: pC, pD: pD, pE: pE)
+ }
+
+ struct exABCDE {
+ let a: XCTestExpectation?
+ let b: XCTestExpectation?
+ let c: XCTestExpectation?
+ let d: XCTestExpectation?
+ let e: XCTestExpectation?
+ let cancelled: XCTestExpectation?
+
+ let cancelA: Bool
+ let cancelB: Bool
+ let cancelC: Bool
+ let cancelD: Bool
+ let cancelE: Bool
+ }
+
+ func cancelChainSetup(ex: exABCDE) {
+ {
+ let c = cancelChainPromises()
+
+ c.pA.then { (_: A) -> CancellablePromise in
+ self.trace("pA.then")
+ return firstly { () -> CancellablePromise in
+ self.trace("pB.firstly")
+ return c.pB
+ }.then { (_: B) -> CancellablePromise in
+ self.trace("pB.then")
+ return firstly { () -> CancellablePromise in
+ self.trace("pC.firstly")
+ if ex.cancelB {
+ self.trace("CANCEL")
+ c.pA.cancel()
+ }
+ ex.b?.fulfill() ?? XCTFail("pB.then")
+ return c.pC
+ }.then { (_: C) -> CancellablePromise in
+ ex.c?.fulfill() ?? XCTFail("pC.then")
+ if ex.cancelC {
+ self.trace("CANCEL")
+ c.pA.cancel()
+ }
+ self.trace("pC.then")
+ return c.pD
+ }
+ }.then { (_: D) -> CancellablePromise in
+ ex.d?.fulfill() ?? XCTFail("pD.done")
+ if ex.cancelD {
+ self.trace("CANCEL")
+ c.pA.cancel()
+ }
+ return c.pA // Intentional reuse of pA -- causes a loop that CancelContext must detect
+ }
+ }.then { (_: A) -> CancellablePromise in
+ self.trace("pA.then")
+ ex.a?.fulfill() ?? XCTFail("pA completed")
+ if ex.cancelA {
+ self.trace("CANCEL")
+ c.pA.cancel()
+ }
+ return c.pE
+ }.done { _ in
+ ex.e?.fulfill() ?? XCTFail("pE completed")
+ if ex.cancelE {
+ self.trace("CANCEL")
+ c.pA.cancel()
+ }
+ self.trace("pE.done")
+ }.catch(policy: .allErrors) {
+ self.trace("Error: \($0)")
+ $0.isCancelled ? ex.cancelled?.fulfill() : XCTFail("Error: \($0)")
+ }
+
+ self.trace("SETUP COMPLETE")
+
+#if swift(>=4.1)
+ let expectations = [ex.a, ex.b, ex.c, ex.d, ex.e, ex.cancelled].compactMap { $0 }
+#else
+ let expectations = [ex.a, ex.b, ex.c, ex.d, ex.e, ex.cancelled].flatMap { $0 }
+#endif
+ wait(for: expectations, timeout: 1)
+
+ XCTAssert(c.pA.cancelContext.cancelAttempted)
+ XCTAssert(ex.a == nil || isFulfilled(c.pB) || c.pB.cancelContext.cancelAttempted)
+ XCTAssert(ex.b == nil || isFulfilled(c.pC) || c.pC.cancelContext.cancelAttempted)
+ XCTAssert(ex.c == nil || isFulfilled(c.pD) || c.pD.cancelContext.cancelAttempted)
+ XCTAssert(ex.d == nil || isFulfilled(c.pE) || c.pE.cancelContext.cancelAttempted)
+ }()
+
+ self.trace("DONE")
+
+ return
+ }
+
+ func isFulfilled(_ p: CancellablePromise) -> Bool {
+ if let result = p.promise.result {
+ if case .success = result {
+ return true
+ } else {
+ return false
+ }
+ } else {
+ return false
+ }
+ }
+
+ func testCancelChainPB() {
+ let ex = exABCDE(a: nil,
+ b: expectation(description: "pB completed"),
+ c: nil,
+ d: nil,
+ e: nil,
+ cancelled: expectation(description: "cancelled"),
+ cancelA: false,
+ cancelB: true,
+ cancelC: false,
+ cancelD: false,
+ cancelE: false)
+ cancelChainSetup(ex: ex)
+ }
+
+ func testCancelChainPC() {
+ let ex = exABCDE(a: nil,
+ b: expectation(description: "pB completed"),
+ c: expectation(description: "pC completed"),
+ d: nil,
+ e: nil,
+ cancelled: expectation(description: "cancelled"),
+ cancelA: false,
+ cancelB: false,
+ cancelC: true,
+ cancelD: false,
+ cancelE: false)
+ cancelChainSetup(ex: ex)
+ }
+
+ func testCancelChainPAD() {
+ let ex = exABCDE(a: expectation(description: "pA completed"),
+ b: expectation(description: "pB completed"),
+ c: expectation(description: "pC completed"),
+ d: expectation(description: "pD completed"),
+ e: nil,
+ cancelled: expectation(description: "cancelled"),
+ cancelA: true,
+ cancelB: false,
+ cancelC: false,
+ cancelD: false,
+ cancelE: false)
+ cancelChainSetup(ex: ex)
+ }
+
+ func testCancelChainSuccess() {
+ let ex = exABCDE(a: expectation(description: "pA completed"),
+ b: expectation(description: "pB completed"),
+ c: expectation(description: "pC completed"),
+ d: expectation(description: "pD completed"),
+ e: expectation(description: "pE completed"),
+ cancelled: nil,
+ cancelA: false,
+ cancelB: false,
+ cancelC: false,
+ cancelD: false,
+ cancelE: true)
+ cancelChainSetup(ex: ex)
+ }
+}
diff --git a/Tests/Cancel/CancellableErrorTests.swift b/Tests/Cancel/CancellableErrorTests.swift
new file mode 100644
index 000000000..612925b74
--- /dev/null
+++ b/Tests/Cancel/CancellableErrorTests.swift
@@ -0,0 +1,136 @@
+import Foundation
+import PromiseKit
+import XCTest
+
+class CancellationTests: XCTestCase {
+ func testCancellation() {
+ let ex1 = expectation(description: "")
+
+ let p = after(seconds: 0).cancellize().done { _ in
+ XCTFail()
+ }
+ p.catch { _ in
+ XCTFail()
+ }
+ p.catch(policy: .allErrors) {
+ XCTAssertTrue($0.isCancelled)
+ ex1.fulfill()
+ }
+
+ p.cancel(with: LocalError.cancel)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testThrowCancellableErrorThatIsNotCancelled() {
+ let expect = expectation(description: "")
+
+ let cc = after(seconds: 0).cancellize().done {
+ XCTFail()
+ }.catch {
+ XCTAssertFalse($0.isCancelled)
+ expect.fulfill()
+ }
+
+ cc.cancel(with: LocalError.notCancel)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testRecoverWithCancellation() {
+ let ex1 = expectation(description: "")
+ let ex2 = expectation(description: "")
+
+ let p = after(seconds: 0).cancellize().done { _ in
+ XCTFail()
+ }.recover(policy: .allErrors) { err -> CancellablePromise in
+ ex1.fulfill()
+ XCTAssertTrue(err.isCancelled)
+ throw err
+ }.done { _ in
+ XCTFail()
+ }
+ p.catch { _ in
+ XCTFail()
+ }
+ p.catch(policy: .allErrors) {
+ XCTAssertTrue($0.isCancelled)
+ ex2.fulfill()
+ }
+
+ p.cancel(with: CocoaError.cancelled)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testFoundationBridging1() {
+ let ex = expectation(description: "")
+
+ let p = after(seconds: 0).cancellize().done { _ in
+ XCTFail()
+ }
+ p.catch { _ in
+ XCTFail()
+ }
+ p.catch(policy: .allErrors) {
+ XCTAssertTrue($0.isCancelled)
+ ex.fulfill()
+ }
+
+ p.cancel(with: CocoaError.cancelled)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testFoundationBridging2() {
+ let ex = expectation(description: "")
+
+ let p = CancellablePromise().done {
+ XCTFail()
+ }
+ p.catch { _ in
+ XCTFail()
+ }
+ p.catch(policy: .allErrors) {
+ XCTAssertTrue($0.isCancelled)
+ ex.fulfill()
+ }
+
+ p.cancel(with: URLError.cancelled)
+
+ waitForExpectations(timeout: 1)
+ }
+
+#if swift(>=3.2)
+ func testIsCancelled() {
+ XCTAssertTrue(PMKError.cancelled.isCancelled)
+ XCTAssertTrue(URLError.cancelled.isCancelled)
+ XCTAssertTrue(CocoaError.cancelled.isCancelled)
+ XCTAssertFalse(CocoaError(_nsError: NSError(domain: NSCocoaErrorDomain, code: CocoaError.Code.coderInvalidValue.rawValue)).isCancelled)
+ }
+#endif
+}
+
+private enum LocalError: CancellableError {
+ case notCancel
+ case cancel
+
+ var isCancelled: Bool {
+ switch self {
+ case .notCancel: return false
+ case .cancel: return true
+ }
+ }
+}
+
+private extension URLError {
+ static var cancelled: URLError {
+ return .init(_nsError: NSError(domain: NSURLErrorDomain, code: URLError.Code.cancelled.rawValue))
+ }
+}
+
+private extension CocoaError {
+ static var cancelled: CocoaError {
+ return .init(_nsError: NSError(domain: NSCocoaErrorDomain, code: CocoaError.Code.userCancelled.rawValue))
+ }
+}
diff --git a/Tests/Cancel/CancellablePromiseTests.swift b/Tests/Cancel/CancellablePromiseTests.swift
new file mode 100644
index 000000000..7485a3c50
--- /dev/null
+++ b/Tests/Cancel/CancellablePromiseTests.swift
@@ -0,0 +1,205 @@
+import Foundation
+import PromiseKit
+import XCTest
+
+class CancellablePromiseTests: XCTestCase {
+ func login() -> Promise {
+ return Promise.value(1)
+ }
+
+ func fetch(avatar: Int) -> CancellablePromise {
+ return Promise.value(avatar + 2).cancellize()
+ }
+
+ func testCancellablePromiseEmbeddedInStandardPromiseChain() {
+ let ex = expectation(description: "")
+ var imageView: Int?
+ let promise = firstly { /// <-- ERROR: Ambiguous reference to member 'firstly(execute:)'
+ /* The 'cancellize' method initiates a cancellable promise chain by
+ returning a 'CancellablePromise'. */
+ login().cancellize() /// CHANGE TO: "login().cancellize()"
+ }.then { creds in
+ self.fetch(avatar: creds)
+ }.done { image in
+ imageView = image
+ XCTAssert(imageView == 3)
+ XCTFail()
+ }.catch(policy: .allErrors) { error in
+ if error.isCancelled {
+ // the chain has been cancelled!
+ ex.fulfill()
+ } else {
+ XCTFail()
+ }
+ }
+
+ // …
+
+ promise.cancel()
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testReturnTypeForAMultiLineClosureIsNotExplicitlyStated() {
+ let ex = expectation(description: "")
+ var imageView: Int?
+ firstly {
+ login()
+ }.cancellize().then { creds -> CancellablePromise in
+ let f = self.fetch(avatar: creds)
+ return f
+ }.done { image in
+ imageView = image
+ XCTAssert(imageView == 3)
+ ex.fulfill()
+ }.catch(policy: .allErrors) { error in
+ XCTFail()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testTryingToCancelAStandardPromiseChain() {
+ let ex = expectation(description: "")
+ var imageView: Int?
+ let promise = firstly {
+ login()
+ }.cancellize().then { creds in
+ self.fetch(avatar: creds)
+ }.done { image in
+ imageView = image
+ XCTAssert(imageView == 3)
+ XCTFail()
+ }.catch(policy: .allErrors) { error in
+ if error.isCancelled {
+ // the chain has been cancelled!
+ ex.fulfill()
+ } else {
+ XCTFail()
+ }
+ }
+
+ // …
+
+ promise.cancel() /// <-- ERROR: Value of type 'PMKFinalizer' has no member 'cancel'
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testCancel() {
+ let ex = expectation(description: "")
+ let p = CancellablePromise.pending()
+ p.promise.then { (val: Int) -> Promise in
+ Promise.value("hi")
+ }.done { _ in
+ XCTFail()
+ ex.fulfill()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }
+ p.resolver.fulfill(3)
+ p.promise.cancel()
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testFirstly() {
+ let ex = expectation(description: "")
+ firstly {
+ Promise.value(3)
+ }.cancellize().then { (_: Int) -> Promise in
+ XCTFail()
+ return Promise.value("hi")
+ }.done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testFirstlyWithPromise() {
+ let ex = expectation(description: "")
+ firstly {
+ return Promise.value(3)
+ }.cancellize().then { (_: Int) -> Promise in
+ XCTFail()
+ return Promise.value("hi")
+ }.done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail("\($0)")
+ }.cancel()
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testThenMapSuccess() {
+ let ex = expectation(description: "")
+ firstly {
+ Promise.value([1,2,3])
+ }.cancellize().thenMap { (integer: Int) -> Promise in
+ return Promise.value(integer * 2)
+ }.done { _ in
+ ex.fulfill()
+ // $0 => [2,4,6]
+ }.catch(policy: .allErrors) { _ in
+ XCTFail()
+ }
+ waitForExpectations(timeout: 1)
+ }
+
+ func testThenMapCancel() {
+ let ex = expectation(description: "")
+ firstly {
+ Promise.value([1,2,3])
+ }.cancellize().thenMap { (integer: Int) -> Promise in
+ XCTFail()
+ return Promise.value(integer * 2)
+ }.done { _ in
+ XCTFail()
+ // $0 => [2,4,6]
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+ waitForExpectations(timeout: 1)
+ }
+
+ func testChain() {
+ let ex = expectation(description: "")
+ firstly {
+ Promise.value(1)
+ }.cancellize().then { (integer: Int) -> Promise in
+ XCTFail()
+ return Promise.value(integer * 2)
+ }.done { _ in
+ // $0 => [2,4,6]
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+ waitForExpectations(timeout: 1)
+ }
+
+ func testBridge() {
+ let ex1 = expectation(description: "")
+ let ex2 = expectation(description: "")
+
+ let (promise, seal) = Promise.pending()
+ DispatchQueue.global(qos: .default).asyncAfter(deadline: DispatchTime.now() + 0.2) { seal.fulfill(()) }
+
+ CancellablePromise(promise).done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex2.fulfill() : XCTFail()
+ }.cancel()
+
+ promise.done { _ in
+ ex1.fulfill()
+ }.catch(policy: .allErrors) { _ in
+ XCTFail()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+}
diff --git a/Tests/Cancel/CatchableTests.swift b/Tests/Cancel/CatchableTests.swift
new file mode 100644
index 000000000..fc4520986
--- /dev/null
+++ b/Tests/Cancel/CatchableTests.swift
@@ -0,0 +1,395 @@
+import PromiseKit
+import Dispatch
+import XCTest
+
+class CatchableTests: XCTestCase {
+
+ func testFinally() {
+ let finallyQueue = DispatchQueue(label: "\(#file):\(#line)", attributes: .concurrent)
+
+ func helper(error: Error, on queue: DispatchQueue = .main, flags: DispatchWorkItemFlags? = nil) {
+ let ex = (expectation(description: ""), expectation(description: ""))
+ var x = 0
+ let p = after(seconds: 0.01).cancellize().catch(policy: .allErrors) { _ in
+ XCTAssertEqual(x, 0)
+ x += 1
+ ex.0.fulfill()
+ }.finally(on: queue, flags: flags) {
+ if let flags = flags, flags.contains(.barrier) {
+ dispatchPrecondition(condition: .onQueueAsBarrier(queue))
+ } else {
+ dispatchPrecondition(condition: .onQueue(queue))
+ }
+ XCTAssertEqual(x, 1)
+ x += 1
+ ex.1.fulfill()
+ }
+
+ p.cancel(with: error)
+
+ wait(for: [ex.0, ex.1], timeout: 10)
+ }
+
+ helper(error: Error.dummy)
+ helper(error: Error.cancelled)
+ helper(error: Error.dummy, on: finallyQueue)
+ helper(error: Error.dummy, on: finallyQueue, flags: .barrier)
+ }
+
+ func testCauterize() {
+ let ex = expectation(description: "")
+ let p = after(seconds: 0.01).cancellize()
+
+ // cannot test specifically that this outputs to console,
+ // but code-coverage will note that the line is run
+ p.cauterize()
+
+ p.catch { _ in
+ ex.fulfill()
+ }
+
+ p.cancel(with: Error.dummy)
+
+ wait(for: [ex], timeout: 1)
+ }
+}
+
+/// `Promise.recover`
+extension CatchableTests {
+ func test__void_specialized_full_recover() {
+
+ func helper(policy: CatchPolicy, error: Swift.Error, line: UInt = #line) {
+ let ex = expectation(description: "error caught")
+ CancellablePromise(error: error).recover { _ in }.done { _ in XCTFail() }.catch(policy: .allErrors, ex.fulfill).cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ helper(policy: .allErrorsExceptCancellation, error: Error.dummy)
+ helper(policy: .allErrors, error: Error.dummy)
+ helper(policy: .allErrorsExceptCancellation, error: Error.cancelled)
+ helper(policy: .allErrors, error: Error.cancelled)
+
+ let ex2 = expectation(description: "cancel caught")
+ let d2 = CancellablePromise(error: Error.cancelled).recover(policy: .allErrors) { _ in }.done(ex2.fulfill)
+ d2.cancel()
+ wait(for: [ex2], timeout: 1)
+ }
+
+ func test__void_specialized_full_recover__fulfilled_path() {
+ let ex = expectation(description: "")
+ CancellablePromise().recover { _ in
+ XCTFail()
+ }.done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+
+ let ex2 = expectation(description: "")
+ let promise = CancellablePromise()
+ promise.cancel()
+ promise.recover(policy: .allErrors) { _ in }.done(ex2.fulfill).catch(policy: .allErrors) { _ in XCTFail() }
+ wait(for: [ex2], timeout: 1)
+ }
+
+ func test__void_specialized_conditional_recover() {
+ func helperDone(policy: CatchPolicy, error: Swift.Error, line: UInt = #line) {
+ let ex = expectation(description: "")
+ var x = 0
+ let promise = CancellablePromise(error: error).recover(policy: policy) { (err: Swift.Error) throws -> Void in
+ guard x < 1 else { throw err }
+ x += 1
+ }.done(ex.fulfill).catch(policy: .allErrors) { _ in
+ XCTFail()
+ }
+ promise.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ func helperCatch(policy: CatchPolicy, error: Swift.Error, line: UInt = #line) {
+ let ex = expectation(description: "")
+ var x = 0
+ let promise = CancellablePromise(error: error).recover(policy: policy) { (err: Swift.Error) throws -> Void in
+ guard x < 1 else { throw err }
+ x += 1
+ }.done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }
+ promise.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ for error in [Error.dummy as Swift.Error, Error.cancelled] {
+ helperDone(policy: .allErrors, error: error)
+ }
+ helperCatch(policy: .allErrorsExceptCancellation, error: Error.dummy)
+ }
+
+ func test__void_specialized_conditional_recover__no_recover() {
+
+ func helper(policy: CatchPolicy, error: Error, line: UInt = #line) {
+ let ex = expectation(description: "")
+ CancellablePromise(error: error).recover(policy: .allErrorsExceptCancellation) { err in
+ throw err
+ }.catch(policy: .allErrors) { _ in
+ ex.fulfill()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ for error in [Error.dummy, Error.cancelled] {
+ helper(policy: .allErrors, error: error)
+ }
+ helper(policy: .allErrorsExceptCancellation, error: Error.dummy)
+ }
+
+ func test__void_specialized_conditional_recover__ignores_cancellation_but_fed_cancellation() {
+ let ex = expectation(description: "")
+ CancellablePromise(error: Error.cancelled).recover(policy: .allErrorsExceptCancellation) { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ XCTAssertEqual(Error.cancelled, $0 as? Error)
+ ex.fulfill()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ func test__void_specialized_conditional_recover__fulfilled_path() {
+ let ex = expectation(description: "")
+ let p = CancellablePromise().recover { _ in
+ XCTFail()
+ }.catch { _ in
+ XCTFail() // this `catch` to ensure we are calling the `recover` variant we think we are
+ }.finally {
+ ex.fulfill()
+ }
+ p.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+}
+
+/// `Promise.recover`
+extension CatchableTests {
+ func test__full_recover() {
+ func helper(error: Swift.Error) {
+ let ex = expectation(description: "")
+ CancellablePromise(error: error).recover { _ in
+ return Promise.value(2).cancellize()
+ }.done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors, ex.fulfill).cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ helper(error: Error.dummy)
+ helper(error: Error.cancelled)
+ }
+
+ func test__full_recover__fulfilled_path() {
+ let ex = expectation(description: "")
+ Promise.value(1).cancellize().recover { _ -> CancellablePromise in
+ XCTFail()
+ return Promise.value(2).cancellize()
+ }.done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) { error in
+ error.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ func test__conditional_recover() {
+ func helper(policy: CatchPolicy, error: Swift.Error, line: UInt = #line) {
+ let ex = expectation(description: "\(policy) \(error) \(line)")
+ var x = 0
+ CancellablePromise(error: error).recover(policy: policy) { (err: Swift.Error) throws -> CancellablePromise in
+ guard x < 1 else {
+ throw err
+ }
+ x += 1
+ return Promise.value(x).cancellize()
+ }.done { _ in
+ ex.fulfill()
+ }.catch(policy: .allErrors) { error in
+ if policy == .allErrorsExceptCancellation {
+ error.isCancelled ? ex.fulfill() : XCTFail()
+ } else {
+ XCTFail()
+ }
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ for error in [Error.dummy as Swift.Error, Error.cancelled] {
+ helper(policy: .allErrors, error: error)
+ }
+ helper(policy: .allErrorsExceptCancellation, error: Error.dummy)
+ }
+
+ func test__conditional_recover__no_recover() {
+
+ func helper(policy: CatchPolicy, error: Error, line: UInt = #line) {
+ let ex = expectation(description: "\(policy) \(error) \(line)")
+ CancellablePromise(error: error).recover(policy: policy) { err -> CancellablePromise in
+ throw err
+ }.catch(policy: .allErrors) {
+ if !(($0 as? PMKError)?.isCancelled ?? false) {
+ XCTAssertEqual(error, $0 as? Error)
+ }
+ ex.fulfill()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ for error in [Error.dummy, Error.cancelled] {
+ helper(policy: .allErrors, error: error)
+ }
+ helper(policy: .allErrorsExceptCancellation, error: Error.dummy)
+ }
+
+ func test__conditional_recover__ignores_cancellation_but_fed_cancellation() {
+ let ex = expectation(description: "")
+ CancellablePromise(error: Error.cancelled).recover(policy: .allErrorsExceptCancellation) { _ -> CancellablePromise in
+ XCTFail()
+ return Promise.value(1).cancellize()
+ }.catch(policy: .allErrors) {
+ XCTAssertEqual(Error.cancelled, $0 as? Error)
+ ex.fulfill()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ func test__conditional_recover__fulfilled_path() {
+ let ex = expectation(description: "")
+ Promise.value(1).cancellize().recover { err -> CancellablePromise in
+ XCTFail()
+ throw err
+ }.done {
+ XCTFail()
+ XCTAssertEqual($0, 1)
+ }.catch(policy: .allErrors) { error in
+ error.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ func test__cancellable_conditional_recover__fulfilled_path() {
+ let ex = expectation(description: "")
+ Promise.value(1).cancellize().recover { err -> Promise in
+ XCTFail()
+ throw err
+ }.done {
+ XCTFail()
+ XCTAssertEqual($0, 1)
+ }.catch(policy: .allErrors) { error in
+ error.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testEnsureThen_Error() {
+ let ex = expectation(description: "")
+
+ let p = Promise.value(1).cancellize().done {
+ XCTAssertEqual($0, 1)
+ throw Error.dummy
+ }.ensureThen {
+ return after(seconds: 0.01).cancellize()
+ }.catch(policy: .allErrors) {
+ XCTAssert(($0 as? PMKError)?.isCancelled ?? false)
+ }.finally {
+ ex.fulfill()
+ }
+ p.cancel()
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testEnsureThen_Value() {
+ let ex = expectation(description: "")
+
+ Promise.value(1).cancellize().ensureThen {
+ after(seconds: 0.01).cancellize()
+ }.done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ if !$0.isCancelled {
+ XCTFail()
+ }
+ }.finally {
+ ex.fulfill()
+ }.cancel()
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testEnsureThen_Value_NotCancelled() {
+ let ex = expectation(description: "")
+
+ Promise.value(1).cancellize().ensureThen {
+ after(seconds: 0.01).cancellize()
+ }.done {
+ XCTAssertEqual($0, 1)
+ }.catch(policy: .allErrors) { _ in
+ XCTFail()
+ }.finally {
+ ex.fulfill()
+ }
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testCancellableFinalizerHelpers() {
+ let ex = expectation(description: "")
+
+ let f = Promise.value(1).cancellize().done { _ in
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }
+ f.cancel()
+
+ XCTAssertEqual(f.isCancelled, true)
+ XCTAssertEqual(f.cancelAttempted, true)
+ XCTAssert(f.cancelledError?.isCancelled ?? false)
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testCancellableRecoverFromError() {
+ let ex = expectation(description: "")
+
+ let p = Promise(error: PMKError.emptySequence).cancellize().recover(policy: .allErrors) { _ in
+ Promise.value(1)
+ }.done {
+ XCTAssertEqual($0, 1)
+ ex.fulfill()
+ }
+ let f = p.catch(policy: .allErrors) { _ in
+ XCTFail()
+ }
+
+ XCTAssertEqual(f.isCancelled, false)
+ XCTAssertEqual(f.cancelAttempted, false)
+ XCTAssert(f.cancelledError == nil)
+ XCTAssert(p.cancelledError == nil)
+
+ wait(for: [ex], timeout: 1)
+
+ XCTAssertEqual(p.isPending, false)
+ XCTAssertEqual(p.isResolved, true)
+ XCTAssertEqual(p.isFulfilled, true)
+ }
+}
+
+private enum Error: CancellableError {
+ case dummy
+ case cancelled
+
+ var isCancelled: Bool {
+ return self == Error.cancelled
+ }
+}
diff --git a/Tests/Cancel/DefaultDispatchQueueTests.swift b/Tests/Cancel/DefaultDispatchQueueTests.swift
new file mode 100644
index 000000000..2cc3c79ee
--- /dev/null
+++ b/Tests/Cancel/DefaultDispatchQueueTests.swift
@@ -0,0 +1,74 @@
+import class Foundation.Thread
+import PromiseKit
+import Dispatch
+import XCTest
+
+private enum Error: Swift.Error { case dummy }
+
+class CancellableDefaultDispatchQueueTest: XCTestCase {
+
+ let myQueue = DispatchQueue(label: "myQueue")
+
+ override func setUp() {
+ // can actually only set the default queue once
+ // - See: PMKSetDefaultDispatchQueue
+ conf.Q = (myQueue, myQueue)
+ }
+
+ override func tearDown() {
+ conf.Q = (.main, .main)
+ }
+
+ func testOverrodeDefaultThenQueue() {
+ let ex = expectation(description: "resolving")
+
+ let p = Promise.value(1).cancellize()
+ p.cancel()
+ p.then { _ -> CancellablePromise in
+ XCTFail()
+ XCTAssertFalse(Thread.isMainThread)
+ return CancellablePromise()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ XCTAssertFalse(Thread.isMainThread)
+ }
+
+ XCTAssertTrue(Thread.isMainThread)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testOverrodeDefaultCatchQueue() {
+ let ex = expectation(description: "resolving")
+
+ let p = CancellablePromise(error: Error.dummy)
+ p.cancel()
+ p.catch { _ in
+ ex.fulfill()
+ XCTAssertFalse(Thread.isMainThread)
+ }
+
+ XCTAssertTrue(Thread.isMainThread)
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testOverrodeDefaultAlwaysQueue() {
+ let ex = expectation(description: "resolving")
+ let ex2 = expectation(description: "catching")
+
+ let p = Promise.value(1).cancellize()
+ p.cancel()
+ p.ensure {
+ ex.fulfill()
+ XCTAssertFalse(Thread.isMainThread)
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex2.fulfill() : XCTFail()
+ XCTAssertFalse(Thread.isMainThread)
+ }
+
+ XCTAssertTrue(Thread.isMainThread)
+
+ waitForExpectations(timeout: 1)
+ }
+}
diff --git a/Tests/Cancel/DispatcherTests.swift b/Tests/Cancel/DispatcherTests.swift
new file mode 100644
index 000000000..0274212cb
--- /dev/null
+++ b/Tests/Cancel/DispatcherTests.swift
@@ -0,0 +1,205 @@
+import Dispatch
+import PromiseKit
+import XCTest
+
+fileprivate let queueIDKey = DispatchSpecificKey()
+
+class RecordingDispatcher: Dispatcher {
+
+ static var queueIndex = 1
+
+ var dispatchCount = 0
+ let queue: DispatchQueue
+
+ init() {
+ queue = DispatchQueue(label: "org.promisekit.testqueue \(RecordingDispatcher.queueIndex)")
+ RecordingDispatcher.queueIndex += 1
+ }
+
+ func dispatch(_ body: @escaping () -> Void) {
+ dispatchCount += 1
+ queue.async(execute: body)
+ }
+
+}
+
+class DispatcherTests: XCTestCase {
+
+ var dispatcher = RecordingDispatcher()
+
+ override func setUp() {
+ dispatcher = RecordingDispatcher()
+ }
+
+ func testDispatcherWithThrow() {
+ let ex = expectation(description: "Dispatcher with throw")
+ CancellablePromise { seal in
+ seal.fulfill(42)
+ }.map(on: dispatcher) { _ in
+ throw PMKError.badInput
+ }.catch(on: dispatcher) { _ in
+ ex.fulfill()
+ }
+ waitForExpectations(timeout: 1)
+ XCTAssertEqual(self.dispatcher.dispatchCount, 2)
+ }
+
+ func testDispatchQueueSelection() {
+
+ let ex = expectation(description: "DispatchQueue compatibility")
+
+ let oldConf = PromiseKit.conf.D
+ PromiseKit.conf.D = (map: dispatcher, return: dispatcher)
+
+ let background = DispatchQueue.global(qos: .background)
+ background.setSpecific(key: queueIDKey, value: 100)
+ DispatchQueue.main.setSpecific(key: queueIDKey, value: 102)
+ dispatcher.queue.setSpecific(key: queueIDKey, value: 103)
+
+ Promise.value(42).cancellize().map(on: .global(qos: .background), flags: .barrier) { (x: Int) -> Int in
+ let queueID = DispatchQueue.getSpecific(key: queueIDKey)
+ XCTAssertNotNil(queueID)
+ XCTAssertEqual(queueID!, 100)
+ return x + 10
+ }.get(on: .global(qos: .background), flags: .barrier) { _ in
+ }.tap(on: .global(qos: .background), flags: .barrier) { _ in
+ }.then(on: .main, flags: []) { (x: Int) -> CancellablePromise in
+ XCTAssertEqual(x, 52)
+ let queueID = DispatchQueue.getSpecific(key: queueIDKey)
+ XCTAssertNotNil(queueID)
+ XCTAssertEqual(queueID!, 102)
+ return Promise.value(50).cancellize()
+ }.map(on: nil) { (x: Int) -> Int in
+ let queueID = DispatchQueue.getSpecific(key: queueIDKey)
+ XCTAssertNotNil(queueID)
+ XCTAssertEqual(queueID!, 102)
+ return x + 10
+ }.map { (x: Int) -> Int in
+ XCTAssertEqual(x, 60)
+ let queueID = DispatchQueue.getSpecific(key: queueIDKey)
+ XCTAssertNotNil(queueID)
+ XCTAssertEqual(queueID!, 103)
+ return x + 10
+ }.done(on: background) {
+ XCTAssertEqual($0, 70)
+ let queueID = DispatchQueue.getSpecific(key: queueIDKey)
+ XCTAssertNotNil(queueID)
+ XCTAssertEqual(queueID!, 100)
+ ex.fulfill()
+ }.cauterize()
+
+ waitForExpectations(timeout: 1)
+ PromiseKit.conf.D = oldConf
+
+ }
+
+ func testMapValues() {
+ let ex1 = expectation(description: "DispatchQueue MapValues compatibility")
+ Promise.value([42, 52]).cancellize().then(on: .global(qos: .background), flags: .barrier) { v -> Promise<[Int]> in
+ Promise.value(v)
+ }.compactMap(on: .global(qos: .background), flags: .barrier) {
+ $0
+ }.mapValues(on: .global(qos: .background), flags: .barrier) {
+ $0 + 10
+ }.flatMapValues(on: .global(qos: .background), flags: .barrier) {
+ [$0 + 10]
+ }.compactMapValues(on: .global(qos: .background), flags: .barrier) {
+ $0 + 10
+ }.thenMap(on: .global(qos: .background), flags: .barrier) { v -> CancellablePromise in
+ Promise.value(v + 10).cancellize()
+ }.thenMap(on: .global(qos: .background), flags: .barrier) { v -> Promise in
+ Promise.value(v + 10)
+ }.thenFlatMap(on: .global(qos: .background), flags: .barrier) {
+ Promise.value([$0 + 10]).cancellize()
+ }.thenFlatMap(on: .global(qos: .background), flags: .barrier) { v -> Promise<[Int]> in
+ Promise.value([v + 10])
+ }.filterValues(on: .global(qos: .background), flags: .barrier) { _ in
+ true
+ }.sortedValues(on: .global(qos: .background), flags: .barrier).firstValue(on: .global(qos: .background), flags: .barrier) { _ in
+ true
+ }.done(on: .global(qos: .background), flags: .barrier) {
+ XCTAssertEqual($0, 112)
+ ex1.fulfill()
+ }.catch(on: .global(qos: .background), flags: .barrier) { _ in
+ XCTFail()
+ }
+
+ let ex2 = expectation(description: "DispatchQueue firstValue property")
+ Promise.value([42, 52]).cancellize().firstValue.done(on: .global(qos: .background), flags: .barrier) {
+ XCTAssertEqual($0, 42)
+ ex2.fulfill()
+ }.catch(on: .global(qos: .background), flags: .barrier, policy: .allErrors) { _ in
+ XCTFail()
+ }
+
+ let ex3 = expectation(description: "DispatchQueue lastValue property")
+ Promise.value([42, 52]).cancellize().lastValue.done(on: .global(qos: .background), flags: .barrier) {
+ XCTAssertEqual($0, 52)
+ ex3.fulfill()
+ }.catch(on: .global(qos: .background), flags: .barrier, policy: .allErrors) { _ in
+ XCTFail()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ func testRecover() {
+ let ex1 = expectation(description: "DispatchQueue CatchMixin compatibility")
+ Promise.value(42).cancellize().recover(on: .global(qos: .background), flags: .barrier) { _ in
+ Promise.value(42)
+ }.ensure(on: .global(qos: .background), flags: .barrier) {
+ }.ensureThen(on: .global(qos: .background), flags: .barrier) {
+ Promise.value(42).asVoid().cancellize()
+ }.recover(on: .global(qos: .background), flags: .barrier) { _ in
+ Promise.value(42).cancellize()
+ }.recover(on: .global(qos: .background), flags: .barrier) { _ in
+ Promise.value(42)
+ }.done(on: .global(qos: .background), flags: .barrier) {
+ XCTAssertEqual($0, 42)
+ ex1.fulfill()
+ }.catch(on: .global(qos: .background), flags: .barrier) { _ in
+ XCTFail()
+ }
+
+ let ex2 = expectation(description: "DispatchQueue CatchMixin Void recover")
+ firstly {
+ Promise.value(42).asVoid()
+ }.cancellize().recover(on: .global(qos: .background), flags: .barrier) { _ in
+ }.done {
+ ex2.fulfill()
+ }.catch(on: .global(qos: .background), flags: .barrier) { _ in
+ XCTFail()
+ }
+
+ waitForExpectations(timeout: 1)
+ }
+
+ @available(macOS 10.10, iOS 2.0, tvOS 10.0, watchOS 2.0, *)
+ func testDispatcherExtensionReturnsGuarantee() {
+ let ex = expectation(description: "Dispatcher.promise")
+ dispatcher.dispatch() { () -> Int in
+ XCTAssertFalse(Thread.isMainThread)
+ return 1
+ }.cancellize().done { one in
+ XCTAssertEqual(one, 1)
+ ex.fulfill()
+ }.catch { _ in
+ XCTFail()
+ }
+ waitForExpectations(timeout: 1)
+ }
+
+ @available(macOS 10.10, iOS 2.0, tvOS 10.0, watchOS 2.0, *)
+ func testDispatcherExtensionCanThrowInBody() {
+ let ex = expectation(description: "Dispatcher.promise")
+ dispatcher.dispatch() { () -> Int in
+ throw PMKError.badInput
+ }.cancellize().done { _ in
+ XCTFail()
+ }.catch { _ in
+ ex.fulfill()
+ }
+ waitForExpectations(timeout: 1)
+ }
+
+}
diff --git a/Tests/Cancel/ErrorTests.swift b/Tests/Cancel/ErrorTests.swift
new file mode 100644
index 000000000..b97c7c8ab
--- /dev/null
+++ b/Tests/Cancel/ErrorTests.swift
@@ -0,0 +1,12 @@
+import PromiseKit
+import XCTest
+
+class CancellableErrorTests: XCTestCase {
+ func testCustomStringConvertible() {
+ XCTAssertNotNil(PMKError.cancelled.errorDescription)
+ }
+
+ func testCustomDebugStringConvertible() {
+ XCTAssertFalse(PMKError.cancelled.debugDescription.isEmpty)
+ }
+}
diff --git a/Tests/Cancel/GuaranteeTests.swift b/Tests/Cancel/GuaranteeTests.swift
new file mode 100644
index 000000000..51f3a9f9f
--- /dev/null
+++ b/Tests/Cancel/GuaranteeTests.swift
@@ -0,0 +1,119 @@
+import PromiseKit
+import XCTest
+
+class GuaranteeTests: XCTestCase {
+ func testInit() {
+ let ex = expectation(description: "")
+ Guarantee { seal in
+ seal(1)
+ }.cancellize().done {
+ XCTFail()
+ XCTAssertEqual(1, $0)
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testWait() {
+ let ex = expectation(description: "")
+ do {
+ let p = after(.milliseconds(100)).cancellize().map(on: nil) { 1 }
+ p.cancel()
+ let value = try p.wait()
+ XCTAssertEqual(value, 1)
+ } catch {
+ error.isCancelled ? ex.fulfill() : XCTFail()
+ }
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testThenMap() {
+ let ex = expectation(description: "")
+
+ Guarantee.value([1, 2, 3]).cancellize().thenMap { Guarantee.value($0 * 2).cancellize() }
+ .done { values in
+ XCTAssertEqual([], values)
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail()
+ }.cancel()
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testCancellable() {
+#if swift(>=4.0)
+ var resolver: ((()) -> Void)!
+#else
+ var resolver: ((Void) -> Void)!
+#endif
+
+ let task = DispatchWorkItem {
+#if swift(>=4.0)
+ resolver(())
+#else
+ resolver()
+#endif
+ }
+
+ q.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: task)
+
+ let g = Guarantee(cancellable: task) { seal in
+ resolver = seal
+ }
+
+ let ex = expectation(description: "")
+ firstly {
+ CancellablePromise(g)
+ }.done {
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail("\($0)")
+ } .cancel()
+
+ wait(for: [ex], timeout: 1)
+ }
+
+ func testSetCancellable() {
+#if swift(>=4.0)
+ var resolver: ((()) -> Void)!
+#else
+ var resolver: ((Void) -> Void)!
+#endif
+
+ let task = DispatchWorkItem {
+#if swift(>=4.0)
+ resolver(())
+#else
+ resolver()
+#endif
+ }
+
+ q.asyncAfter(deadline: DispatchTime.now() + 0.5, execute: task)
+
+ let g = Guarantee { seal in
+ resolver = seal
+ }
+ g.setCancellable(task)
+
+ let ex = expectation(description: "")
+ firstly {
+ g
+ }.cancellize().done {
+ XCTFail()
+ }.catch(policy: .allErrors) {
+ $0.isCancelled ? ex.fulfill() : XCTFail("\($0)")
+ } .cancel()
+
+ wait(for: [ex], timeout: 1)
+ }
+}
+
+private var q: DispatchQueue {
+ if #available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) {
+ return DispatchQueue.global(qos: .default)
+ } else {
+ return DispatchQueue.global(priority: .default)
+ }
+}
diff --git a/Tests/Cancel/HangTests.swift b/Tests/Cancel/HangTests.swift
new file mode 100644
index 000000000..5c5524399
--- /dev/null
+++ b/Tests/Cancel/HangTests.swift
@@ -0,0 +1,48 @@
+import PromiseKit
+import XCTest
+
+class HangTests: XCTestCase {
+ func test() {
+ let ex = expectation(description: "block executed")
+ do {
+ let p = after(seconds: 0.02).cancellize().then { _ -> Promise