CancelForPromiseKit

master

CancelForPromiseKit provides clear and concise cancellation abilities for PromiseKit and for the PromiseKit Extensions. While PromiseKit includes basic support for cancellation, CancelForPromiseKit extends this to make cancelling promises and their associated tasks simple and straightforward.
dougzilla32/CancelForPromiseKit

CancelForPromiseKit

badge-pod badge-languages badge-pms badge-platforms badge-mit badge-docs Build Status


API Docs: CancelForPromiseKit CPKAlamofire CPKCoreLocation CPKFoundation

CancelForPromiseKit provides clear and concise cancellation abilities for the most excellent PromiseKit and PromiseKit Extensions. While PromiseKit includes basic support for cancellation, CancelForPromiseKit extends this to make cancelling promises and their associated tasks simple and straightforward.

For example:

UIApplication.shared.isNetworkActivityIndicatorVisible = true

let fetchImage = URLSession.shared.dataTaskCC(.promise, with: url).compactMap{ UIImage(data: $0.data) }
let fetchLocation = CLLocationManager.requestLocationCC().lastValue

let context = 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
    /* Will be invoked with a PromiseCancelledError when cancel is called on the context.
       Use the default policy of .allErrorsExceptCancellation to ignore cancellation errors. */
    self.show(UIAlertController(for: error), sender: self)
}.cancelContext

//…

// Cancel currently active tasks and reject all cancellable promises with PromiseCancelledError
context.cancel()

/* Note: Cancellable promises can be cancelled directly.  However by holding on to
   the CancelContext rather than a promise, each promise in the chain can be
   deallocated by ARC as it is resolved. */

Note: The format for this README and for the project as a whole mirrors PromiseKit in an attempt to be readable and concise. For all code samples, the differences between PromiseKit and CancelForPromiseKit are highlighted in bold.

Quick Start with CocoaPods

In your Podfile:

use_frameworks!

target "Change Me!" do
  pod "PromiseKit", "~> 6.0"
  pod "CancelForPromiseKit", "~> 1.0"
end

CancelForPromiseKit has the same platform and Xcode support as PromiseKit

Examples

  • Cancelling a chain
let promise = firstly {
    /* Methods and functions with the CC (a.k.a. cancel chain) suffix initiate a
    cancellable promise chain by returning a CancellablePromise. */
    loginCC()
}.then { creds in
    /* 'fetch' in this example may return either Promise or CancellablePromise --
        If 'fetch' returns a CancellablePromise then the fetch task can be cancelled.
        If 'fetch' returns a standard Promise then the fetch task cannot be cancelled,
        however if cancel is called during the fetch then the promise chain will still be
        rejected with a PromiseCancelledError as soon as the 'fetch' task completes.
        
        Note: if 'fetch' returns a CancellablePromise then the convention is to name
        it 'fetchCC'. */
    fetch(avatar: creds.user)
}.done { image in
    self.imageView = image
}.catch(policy: .allErrors) { error in
    if error.isCancelled {
        // the chain has been cancelled!
    }
}

// …

/* 'promise' here refers to the last promise in the chain.  Calling 'cancel' on
   any promise in the chain cancels the entire chain.  Therefore cancelling the
   last promise in the chain cancels everything.
   
   Note: 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. */
promise.cancel()
  • Mixing Promise and CancellablePromise to cancel some branches and not others

In the example above: if fetch(avatar: creds.user) returns a standard Promise then the fetch cannot be cancelled. However, if cancel is called in the middle of the fetch then the promise chain will still be rejected with a PromiseCancelledError once the fetch completes. The done block will not be called and the catch(policy: .allErrors) block will be called instead.

If fetch returns a CancellablePromise then the fetch will be cancelled when cancel() is invoked, and the catch block will be called immediately.

  • Use the 'delegate' promise

CancellablePromise wraps a delegate Promise, which can be accessed with the promise property. The above example can be modified as follows so that once 'loginCC' completes, the chain cannot be cancelled:

let cancellablePromise = firstly {
    loginCC()
}
cancellablePromise.then { creds in
    // For this example 'fetch' returns a standard Promise
    fetch(avatar: creds.user)  
    
    /* Here, by calling 'promise.done' rather than 'done' the chain is converted from a
       cancellable promise chain to a standard Promise chain. In this case, calling
       'cancel' during the 'fetch' operation has no effect: */
}.promise.done { image in
    self.imageView = image
}.catch(policy: .allErrors) { error in
    if error.isCancelled {
        // the chain has been cancelled!
    }
}

// …

cancellablePromise.cancel()

Documentation

The following functions and methods are part of the core CancelForPromiseKit module. Functions and Methods with the CC suffix create a new CancellablePromise, and those without the CC suffix accept an existing CancellablePromise.

Here is the Jazzy generated CancelForPromiseKit API documentation.

Global functions (all returning CancellablePromise unless otherwise noted)
	afterCC(seconds:)
	afterCC(_ interval:)

	firstly(execute body:)       // Accepts body returning CancellableTheanble
	firstlyCC(execute body:)     // Accepts body returning Theanble

	hang(_ promise:)             // Accepts CancellablePromise
	
	race(_ thenables:)           // Accepts CancellableThenable
	race(_ guarantees:)          // Accepts CancellableGuarantee
	raceCC(_ thenables:)         // Accepts Theanable
	raceCC(_ guarantees:)        // Accepts Guarantee

	when(fulfilled thenables:)   // Accepts CancellableThenable
	when(fulfilled promiseIterator:concurrently:)   // Accepts CancellablePromise
	whenCC(fulfilled thenables:) // Accepts Thenable
	whenCC(fulfilled promiseIterator:concurrently:) // Accepts Promise

	// These functions return CancellableGuarantee
	when(resolved promises:)     // Accepts CancellablePromise
	when(_ guarantees:)          // Accepts CancellableGuarantee
	when(guarantees:)            // Accepts CancellableGuarantee
	whenCC(resolved promises:)   // Accepts Promise
	whenCC(_ guarantees:)        // Accepts Guarantee
	whenCC(guarantees:)          // Accepts Guarantee


CancellablePromise: CancellableThenable
	CancellablePromise.value(_ value:)
	init(task:resolver:)
	init(task:bridge:)
	init(task:error:)
	promise                      // The delegate Promise
	result

CancellableGuarantee: CancellableThenable
	CancellableGuarantee.value(_ value:)
	init(task:resolver:)
	init(task:bridge:)
	init(task:error:)
	guarantee                    // The delegate Guarantee
	result

CancellableThenable
	thenable                     // The delegate Thenable
	cancel(error:)               // Accepts optional Error to use for cancellation
	cancelContext                // CancelContext for the cancel chain we are a member of
	isCancelled
	cancelAttempted
	cancelledError
	appendCancellableTask(task:reject:)
	appendCancelContext(from:)
	
	then(on:_ body:)             // Accepts body returning CancellableThenable or Thenable
	map(on:_ transform:)
	compactMap(on:_ transform:)
	done(on:_ body:)
	get(on:_ body:)
	asVoid()
	
	error
	isPending
	isResolved
	isFulfilled
	isRejected
	value
	
	mapValues(on:_ transform:)
	flatMapValues(on:_ transform:)
	compactMapValues(on:_ transform:)
	thenMap(on:_ transform:)     // Accepts transform returning CancellableThenable or Thenable
	thenFlatMap(on:_ transform:) // Accepts transform returning CancellableThenable or Thenable
	filterValues(on:_ isIncluded:)
	firstValue
	lastValue
	sortedValues(on:)

CancellableCatchable
	catchable                    // The delegate Catchable
	recover(on:policy:_ body:)   // Accepts body returning CancellableThenable or Thenable
	recover(on:_ body:)          // Accepts body returning Void
	ensure(on:_ body:)
	ensureThen(on:_ body:)
	finally(_ body:)
	cauterize()

Extensions

CancelForPromiseKit provides the same extensions and functions as PromiseKit so long as the underlying asynchronous task(s) support cancellation.

The default CocoaPod provides the core cancellable promises and the extension for Foundation. The other extensions are available by specifying additional subspecs in your Podfile, eg:

pod "CancelForPromiseKit/MapKit"
# MKDirections().calculateCC().then { /*…*/ }

pod "CancelForPromiseKit/CoreLocation"
# CLLocationManager.requestLocationCC().then { /*…*/ }

As with PromiseKit, all extensions are separate repositories. Here is a complete list of CancelForPromiseKit extensions listing the specific functions that support cancellation (PromiseKit extensions without any functions supporting cancellation are omitted):

Alamofire

Alamofire.DataRequest
	responseCC(_:queue:)
	responseDataCC(queue:)
	responseStringCC(queue:)
	responseJSONCC(queue:options:)
	responsePropertyListCC(queue:options:)
	responseDecodableCC(queue::decoder:)
	responseDecodableCC(_ type:queue:decoder:)

Alamofire.DownloadRequest
	responseCC(_:queue:)
	responseDataCC(queue:)

Bolts
Cloudkit
CoreLocation
Foundation

Process
	launchCC(_:)
		
URLSession
	dataTaskCC(_:with:)
	uploadTaskCC(_:with:from:)
	uploadTaskCC(_:with:fromFile:)
	downloadTaskCC(_:with:to:)

MapKit
OMGHTTPURLRQ
StoreKit
WatchConnectivity

I don't want the extensions!

As with PromiseKit, extensions are optional:

pod "CancelForPromiseKit/CorePromise", "~> 1.0"

Note Carthage installations come with no extensions by default.

Choose Your Networking Library

All the networking library extensions supported by PromiseKit are now simple to cancel!

Alamofire:

// pod 'CancelForPromiseKit/Alamofire'
// # https://github.com/dougzilla32/CancelForPromiseKit-Alamofire

let context = firstly {
    Alamofire
        .request("http://example.com", method: .post, parameters: params)
        .responseDecodableCC(Foo.self, cancel: context)
}.done { foo in
    //…
}.catch { error in
    //…
}.cancelContext

//…

context.cancel()

OMGHTTPURLRQ:


// pod 'CancelForPromiseKit/OMGHTTPURLRQ'
// # https://github.com/dougzilla32/CancelForPromiseKit-OMGHTTPURLRQ

let context = firstly {
    URLSession.shared.POSTCC("http://example.com", JSON: params)
}.map {
    try JSONDecoder().decoder(Foo.self, with: $0.data)
}.done { foo in
    //…
}.catch { error in
    //…
}.cancelContext

//…

context.cancel()

And (of course) plain URLSession from Foundation:

// pod 'CancelForPromiseKit/Foundation'
// # https://github.com/dougzilla32/CancelForPromiseKit-Foundation

let context = firstly {
    URLSession.shared.dataTaskCC(.promise, with: try makeUrlRequest())
}.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
}

Design Goals

  • Provide a streamlined way to cancel a promise chain, which rejects all associated promises and cancels all associated tasks. For example:
let promise = firstly {
    loginCC() // Use CC (a.k.a. cancel chain) methods or CancellablePromise to
              // initiate a cancellable promise chain
}.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!
    }
}
//…
promise.cancel()
  • Ensure that subsequent code blocks in a promise chain are never called after the chain has been cancelled

  • Fully support concurrecy, where all code is thead-safe

  • Provide cancellable varients for all appropriate PromiseKit extensions (e.g. Foundation, CoreLocation, Alamofire, etc.)

  • 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:

import Alamofire
import PromiseKit
import CancelForPromiseKit

func updateWeather(forCity searchName: String) {
    refreshButton.startAnimating()
    let context = firstly {
        getForecast(forCity: searchName)
    }.done { response in
        updateUI(forecast: response)
    }.ensure {
        refreshButton.stopAnimating()
    }.catch { error in
        // Cancellation errors are ignored by default
        showAlert(error: error) 
    }.cancelContext

    //…

    // **** Cancels EVERYTHING (however the 'ensure' block always executes regardless)
    context.cancel()
}

func getForecast(forCity name: String) -> CancellablePromise<WeatherInfo> {
    return firstly {
        Alamofire.request("https://autocomplete.weather.com/\(name)")
            .responseDecodableCC(AutoCompleteCity.self)
    }.then { city in
        Alamofire.request("https://forecast.weather.com/\(city.name)")
            .responseDecodableCC(WeatherResponse.self)
    }.map { response in
        format(response)
    }
}

Support

If you have a question or an issue to report, please use my bug tracker.

Description

  • Swift Tools 4.0.0
View More Packages from this Author

Dependencies

Last updated: Tue Mar 12 2024 05:51:45 GMT-0900 (Hawaii-Aleutian Daylight Time)