Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Promise chain cancellation #339

Closed
o15a3d4l11s2 opened this issue Dec 8, 2015 · 24 comments
Closed

Promise chain cancellation #339

o15a3d4l11s2 opened this issue Dec 8, 2015 · 24 comments

Comments

@o15a3d4l11s2
Copy link

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.

@o15a3d4l11s2 o15a3d4l11s2 changed the title Promie Promise chain cancellation Dec 8, 2015
@nathanhosselton
Copy link
Contributor

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.

@nathanhosselton
Copy link
Contributor

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

@mxcl
Copy link
Owner

mxcl commented Dec 12, 2015

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

@o15a3d4l11s2
Copy link
Author

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.

@adrenalin
Copy link

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.

@o15a3d4l11s2
Copy link
Author

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.

@nathanhosselton
Copy link
Contributor

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.

@mxcl
Copy link
Owner

mxcl commented Jul 23, 2016

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.

@mxcl mxcl added the question label Jul 23, 2016
@mxcl
Copy link
Owner

mxcl commented Jul 25, 2016

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

@mxcl mxcl closed this as completed Jul 25, 2016
@ded
Copy link

ded commented Aug 6, 2016

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()
  }
}

@mxcl
Copy link
Owner

mxcl commented Aug 6, 2016

@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()
            }
        }
    } 
}

@tib
Copy link

tib commented Aug 24, 2016

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?

@mxcl
Copy link
Owner

mxcl commented Aug 24, 2016

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
    } 
}

@tib
Copy link

tib commented Aug 24, 2016

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. ;)

@ded
Copy link

ded commented Oct 4, 2016

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

@sudhakrish
Copy link

could you give some sample in Objective c

@mxcl
Copy link
Owner

mxcl commented Mar 7, 2017

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

@bitwit
Copy link
Contributor

bitwit commented Apr 7, 2017

@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.

@mxcl
Copy link
Owner

mxcl commented Apr 7, 2017

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.

@mxcl
Copy link
Owner

mxcl commented Apr 8, 2017

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.

@bavarskis
Copy link

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?

@mxcl
Copy link
Owner

mxcl commented Oct 10, 2018

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

@mxcl
Copy link
Owner

mxcl commented Oct 10, 2018

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

@sebastianludwig
Copy link

sebastianludwig commented May 21, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants