import Dispatch /// Provides `catch` and `recover` to your object that conforms to `Thenable` public protocol CatchMixin: Thenable {} public extension CatchMixin { /** The provided closure executes when this 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 execute: The handler to execute if this promise is rejected. - Returns: A promise finalizer. - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ @discardableResult func `catch`(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> PMKFinalizer { let finalizer = PMKFinalizer() pipe { switch $0 { case .rejected(let error): guard policy == .allErrors || !error.isCancelled else { fallthrough } on.async(flags: flags) { body(error) finalizer.pending.resolve(()) } case .fulfilled: finalizer.pending.resolve(()) } } return finalizer } } public class PMKFinalizer { let pending = Guarantee.pending() /// `finally` is the same as `ensure`, but it is not chainable public func finally(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) { pending.guarantee.done(on: on, flags: flags) { body() } } } public extension CatchMixin { /** The provided closure executes when this promise rejects. Unlike `catch`, `recover` continues the chain. Use `recover` in circumstances where recovering the chain from certain errors is a possibility. For example: firstly { CLLocationManager.requestLocation() }.recover { error in guard error == CLError.unknownLocation else { throw error } return .value(CLLocation.chicago) } - Parameter on: The queue to which the provided closure dispatches. - Parameter body: The handler to execute if this promise is rejected. - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> U) -> Promise where U.T == T { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): rp.box.seal(.fulfilled(value)) case .rejected(let error): if policy == .allErrors || !error.isCancelled { on.async(flags: flags) { do { let rv = try body(error) guard rv !== rp else { throw PMKError.returnedSelf } rv.pipe(to: rp.box.seal) } catch { rp.box.seal(.rejected(error)) } } } else { rp.box.seal(.rejected(error)) } } } return rp } /** The provided closure executes when this promise rejects. This variant of `recover` requires the handler to return a Guarantee, thus it returns a Guarantee itself and your closure cannot `throw`. - Note it is logically impossible for this to take a `catchPolicy`, thus `allErrors` are handled. - Parameter on: The queue to which the provided closure dispatches. - Parameter body: The handler to execute if this promise is rejected. - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ @discardableResult func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Error) -> Guarantee) -> Guarantee { let rg = Guarantee(.pending) pipe { switch $0 { case .fulfilled(let value): rg.box.seal(value) case .rejected(let error): on.async(flags: flags) { body(error).pipe(to: rg.box.seal) } } } return rg } /** The provided closure executes when this promise resolves, whether it rejects or not. firstly { UIApplication.shared.networkActivityIndicatorVisible = true }.done { //… }.ensure { UIApplication.shared.networkActivityIndicatorVisible = false }.catch { //… } - 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) -> Promise { let rp = Promise(.pending) pipe { result in on.async(flags: flags) { body() rp.box.seal(result) } } return rp } /** The provided closure executes when this promise resolves, whether it rejects or not. The chain waits on the returned `Guarantee`. firstly { setup() }.done { //… }.ensureThen { teardown() // -> Guarante }.catch { //… } - 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 ensureThen(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Guarantee) -> Promise { let rp = Promise(.pending) pipe { result in on.async(flags: flags) { body().done { rp.box.seal(result) } } } return rp } /** 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() -> PMKFinalizer { return self.catch { Swift.print("PromiseKit:cauterized-error:", $0) } } } public extension CatchMixin where T == Void { /** The provided closure executes when this promise rejects. This variant of `recover` is specialized for `Void` promises and de-errors your chain returning a `Guarantee`, thus you cannot `throw` and you must handle all errors including cancellation. - Parameter on: The queue to which the provided closure dispatches. - Parameter body: The handler to execute if this promise is rejected. - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ @discardableResult func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Error) -> Void) -> Guarantee { let rg = Guarantee(.pending) pipe { switch $0 { case .fulfilled: rg.box.seal(()) case .rejected(let error): on.async(flags: flags) { body(error) rg.box.seal(()) } } } return rg } /** The provided closure executes when this 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 body: The handler to execute if this promise is rejected. - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ func recover(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> Void) -> Promise { let rg = Promise(.pending) pipe { switch $0 { case .fulfilled: rg.box.seal(.fulfilled(())) case .rejected(let error): if policy == .allErrors || !error.isCancelled { on.async(flags: flags) { do { rg.box.seal(.fulfilled(try body(error))) } catch { rg.box.seal(.rejected(error)) } } } else { rg.box.seal(.rejected(error)) } } } return rg } }