Promisekit: Promise chain cancellation

Created on 8 Dec 2015  Â·  24Comments  Â·  Source: mxcl/PromiseKit

Is there a possibility to cancel a whole chain of promises (including nested ones), no matter where we are currently in the chain?

Imagine the following case:
We are sealing a promise like [self syncAllData] which chains multiple operations - fetching data, parsing it, persisting it and all these operations for multiple entities.

At the end imagine that we have a Cancel button, that the user can press at any moment during the synchronisation.

Is it possible to implement this?

I have checked the current documentation for cancellation, but not able to understand how exactly it can work in my specific case.

Most helpful comment

Alright, sorry for getting back so late on this.

Using Swift, you could do what I detailed before and then wrap both the promise from pendingPromise() and your syncAllData() promise in a when. That way, when the cancel button is pressed, you could call reject() with the cancelledError() and your sync would halt (because when rejects as soon as any of its containing promises reject). Likewise, you could add a then handler to the end of your sync chain to call fulfill() once it completes, satisfying when. I think this would work and would satisfy your requirements.

Unfortunately, pendingPromise() is only available via Promise<T> and not AnyPromise, so you'd have to write some Swift to get access to it in your Objective-C code. I am not sure what Max had in mind for his solution, but I don't currently see any way to do this using AnyPromise. I didn't realize there wasn't an analogue for pendingPromise() on AnyPromise, so that's something I'll probably try to add to PromiseKit in the near future.

All 24 comments

Currently, unless you are using one of the categories that inherently support cancellation (e.g. UIActionSheet), the only supported way to "cancel" a promise chain is from within the chain itself using cancelledError(). Although I have not done this myself, a solution for your situation might be to create your synchronization promise with pendingPromise() so you have access to the fulfiller and rejector outside of the promise chain. Then in your cancel button handler you can reject the promise with cancelledError().

Again, I haven't tested this, but it sounds like it should work in theory. Let me know how it goes if you try it.

Actually this won't work. Sorry, I need to think on this some more.

Yes it's possible. Can't detail code yet. Swift or ObjC?

Objective-C would be better for me. If you prefer giving an example on Swift, this will also suffice to get the idea how to implement this.

Any news? This does interest me as well, because I am new to Swift and iOS and looking for an easy way to handle promises as a convenience for multithreading processes that might never need to finish due to the user-initiated changes.

I am able to think of workarounds by using out-of-scope variables that the promise checks for termination, but @mxcl 's last comment sounds as if there might be also other possibilities to terminate the block from the outside.

I am still using PromiseKit and haven't implemented a cancellation yet.
On another project I used Bolts: https://github.com/BoltsFramework/Bolts-ObjC and it was very easy to do cancellation. Keep in mind that there are some differences in the way these two libraries work.

Alright, sorry for getting back so late on this.

Using Swift, you could do what I detailed before and then wrap both the promise from pendingPromise() and your syncAllData() promise in a when. That way, when the cancel button is pressed, you could call reject() with the cancelledError() and your sync would halt (because when rejects as soon as any of its containing promises reject). Likewise, you could add a then handler to the end of your sync chain to call fulfill() once it completes, satisfying when. I think this would work and would satisfy your requirements.

Unfortunately, pendingPromise() is only available via Promise<T> and not AnyPromise, so you'd have to write some Swift to get access to it in your Objective-C code. I am not sure what Max had in mind for his solution, but I don't currently see any way to do this using AnyPromise. I didn't realize there wasn't an analogue for pendingPromise() on AnyPromise, so that's something I'll probably try to add to PromiseKit in the near future.

Can we get a code example? For a branching example would be:

foo.then {
  //…
}.then.then…

foo.then {
  //…
}.then.then…

Here cancellation would work provided you cancel foo. How do you cancel foo? Like so:

foo = bar.then {
   throw Error.Cancel
}

enum Error: ErrorType, CancellableErrorType {
    case Cancel
}

So is this not sufficient? I'm guessing, no.

Closing pending feedback, please re-open if you want to continue the discussion.

I'd love to re-open, (sorry I see you just closed this about 12 days ago).

I'm looking for a clean solution as such, where the implementor of the Promise (not the issuer) can cancel the Promise.

My case is an autocomplete... where a user continues to type new values, if the previous request has not been completed, I want to abort it.

var request: Promise<JSON>?

. . .

func valChanged(sender: UITextField) {
  if self.request != nil && !self.request.isCancelled {
    self.request.cancel()
  }
  self.request = getNewResults(sender.text)
  self.request.then { (response) ->
    self.results = response["data"].arrayValue
    self.tableView.reloadData()
  }
}

@ded for your usage you need to implement your own cancel method on a subclass of promise. e.g.:

class MyPromise: Promise<JSON> {
    var mytask
    let reject: (ErrorType) -> Void

    func cancel() {
        mytask.cancel()
        reject(NSError.cancelledError)
    }

    init() {
        self.init { fulfill, reject in
            self.reject = reject
            mytask = MyTask()
            mytask.startWithCompletionHandler {
                fulfill()
            }
        }
    } 
}

I am trying to solve the same thing, your example above is not compiling because of the self requirement in the init block. Do you have a working solution for this issue maybe?

public class MyPromise: Promise<JSON> {
    private var mytask
    private let reject: (ErrorType) -> Void

    public func cancel() {
        mytask.cancel()
        reject(NSError.cancelledError)
    }

    public func static go() -> MyPromise {
        var reject: ((ErrorType) -> Void)!
        let promise = MyPromise { fulfill, r in
            reject = r
            mytask = MyTask()
            mytask.startWithCompletionHandler {
                fulfill()
            }
        }
        promise.reject = reject
        return promise
    } 
}

Thank you for the help, I could make it:

public class MyPromise: Promise<NSData> {

    private var task: NSURLSessionDataTask?
    private var fulfill: ((NSData) -> Void)?
    private var reject: ((ErrorType) -> Void)?

    override init(@noescape resolvers: (fulfill: (NSData) -> Void, reject: (ErrorType) -> Void) throws -> Void) {
        super.init(resolvers: resolvers)
    }

    init(request: NSURLRequest) {
        var fulfill: ((NSData) -> Void)?
        var reject: ((ErrorType) -> Void)?

        super.init { f, r in
            fulfill = f
            reject = r
        }

        self.fulfill = fulfill
        self.reject  = reject

        self.task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
            if let error = error {
                if error.code == NSURLErrorCancelled {
                    return
                }
                self.reject?(error)
                return
            }
            guard let data = data else {
                self.reject?(NSError(domain: "no-data", code: 0, userInfo: nil))
                return
            }
            self.fulfill?(data)
        }
        self.task?.resume()

    }

    public func cancel() {
        guard !self.rejected else { return }
        self.task?.cancel()
        self.reject?(NSError.cancelledError())
    }

    public class func go() -> MyPromise {
        var fulfill: ((NSData) -> Void)?
        var reject: ((ErrorType) -> Void)?

        let promise = MyPromise { f, r in
            fulfill = f
            reject  = r
        }

        promise.fulfill = fulfill
        promise.reject  = reject

        let url      = NSURL(string: "https://jsonplaceholder.typicode.com/users")!
        let request  = NSURLRequest(URL: url)
        promise.task = NSURLSession.sharedSession().dataTaskWithRequest(request) { data, response, error in
            if let error = error {
                if error.code == NSURLErrorCancelled {
                    return
                }
                promise.reject?(error)
                return
            }
            guard let data = data else {
                promise.reject?(NSError(domain: "no-data", code: 0, userInfo: nil))
                return
            }
            promise.fulfill?(data)
        }
        promise.task?.resume()

        return promise
    }
}

Usage example:

let promise = MyPromise.go()

promise
        .then { data -> Void in
            print("data")
            print(data)
        }
        .error { error in
            print("error")
            print(error)
        }

promise.cancel()
print(promise.rejected)

        let url = NSURL(string: "https://jsonplaceholder.typicode.com/users")!
        let request  = NSURLRequest(URL: url)
        let promise = MyPromise(request: request)

        promise
        .then { data -> Void in
            print("data")
            print(data)
        }
        .error { error in
            print("error")
            print(error)
        }

        promise.cancel()

        print(promise.rejected)

Just in case if anyone needs something like this. ;)

months later... thank you for this! i'll have a play with this style.

could you give some sample in Objective c

foo.then(^{
    @throw NSError.cancelledError;
})

@tib Thanks for your code. It helped us solve a similar issue. However, isn't there a potential issue with retain cycles with the way you strongly reference self inside of init from within the data task closure? We modified our code to use the local fulfill and reject functions so that no reference to self was necessary and all appears to be fine.

URLSession always calls its completion block and then afterwards releases its completion block, so: no. This is the kind of retain cycle you want, it retains the closure and surrounding code until you don't want it anymore. If you want to make your life more difficult you could do [weak self] but it isn't necessary for 99% of usages.

Amendment: however if you didn't start the task with resume() it would be a retain cycle. But this is done above immediately so we're good.

I would like to use the above discussed solution but subclassing Promise is not possible (release 6.5) because it's not an open class. Is it now necessary to create a new class conforming to Thenable or is there a better way to cancel an underlying dataTask?

You don’t need to subclass to use this pattern.

FYI for everyone we are considering a patch to do the original question in #899

Also came here while in need for a cancel-solution. #899 isn't released yet and the detailed approach above doesn't seem to work anymore, because Promise is now final. Any hints on how to handle this scenario until PMK7 is out?

Edit: I guess storing the seal that's handed to Promise { seal in and rejecting that is the way to go now. Please correct me if I'm wrong.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

adrianbg picture adrianbg  Â·  6Comments

rlam3 picture rlam3  Â·  5Comments

drekka picture drekka  Â·  5Comments

dboydor picture dboydor  Â·  4Comments

katopz picture katopz  Â·  4Comments