From 6a709fcceff0e12b9c54651dfec4a788452c7e8b Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Sun, 17 Jun 2018 23:39:52 -0700 Subject: [PATCH 1/7] First draft of Dispatcher conversion --- PromiseKit.xcodeproj/project.pbxproj | 4 + Sources/Box.swift | 12 +- Sources/Catchable.swift | 47 ++- Sources/Configuration.swift | 10 +- Sources/Deprecations.swift | 10 +- Sources/Dispatcher.swift | 475 +++++++++++++++++++++++++++ Sources/Guarantee.swift | 42 ++- Sources/Promise.swift | 27 ++ Sources/Thenable.swift | 60 ++-- 9 files changed, 608 insertions(+), 79 deletions(-) create mode 100644 Sources/Dispatcher.swift diff --git a/PromiseKit.xcodeproj/project.pbxproj b/PromiseKit.xcodeproj/project.pbxproj index 7250f0253..55bd727d6 100644 --- a/PromiseKit.xcodeproj/project.pbxproj +++ b/PromiseKit.xcodeproj/project.pbxproj @@ -78,6 +78,7 @@ 63CF6D80203CD19200EC8927 /* ThenableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63CF6D7F203CD19200EC8927 /* ThenableTests.swift */; }; 63D9B2EF203385FD0075C00B /* race.m in Sources */ = {isa = PBXBuildFile; fileRef = 63D9B2EE203385FD0075C00B /* race.m */; }; 63D9B2F120338D5D0075C00B /* Deprecations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63D9B2F020338D5D0075C00B /* Deprecations.swift */; }; + BB2524DE20D729A60010F7B0 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB2524DD20D729A60010F7B0 /* Dispatcher.swift */; }; C013F7382048E3B6006B57B1 /* MockNodeEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F7372048E3B6006B57B1 /* MockNodeEnvironment.swift */; }; C013F73A2049076A006B57B1 /* JSPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F7392049076A006B57B1 /* JSPromise.swift */; }; C013F73C20494291006B57B1 /* JSAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F73B20494291006B57B1 /* JSAdapter.swift */; }; @@ -221,6 +222,7 @@ 63CF6D7F203CD19200EC8927 /* ThenableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThenableTests.swift; sourceTree = ""; }; 63D9B2EE203385FD0075C00B /* race.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = race.m; path = Sources/race.m; sourceTree = ""; }; 63D9B2F020338D5D0075C00B /* Deprecations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Deprecations.swift; path = Sources/Deprecations.swift; sourceTree = ""; }; + BB2524DD20D729A60010F7B0 /* Dispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Dispatcher.swift; path = Sources/Dispatcher.swift; sourceTree = ""; }; C013F7372048E3B6006B57B1 /* MockNodeEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockNodeEnvironment.swift; path = "Tests/JS-A+/MockNodeEnvironment.swift"; sourceTree = ""; }; C013F7392049076A006B57B1 /* JSPromise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = JSPromise.swift; path = "Tests/JS-A+/JSPromise.swift"; sourceTree = ""; }; C013F73B20494291006B57B1 /* JSAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = JSAdapter.swift; path = "Tests/JS-A+/JSAdapter.swift"; sourceTree = ""; }; @@ -440,6 +442,7 @@ 6330B5E01F2E991200D60528 /* Configuration.swift */, 63B18AEB1F2D205C00B79E37 /* CustomStringConvertible.swift */, 63D9B2F020338D5D0075C00B /* Deprecations.swift */, + BB2524DD20D729A60010F7B0 /* Dispatcher.swift */, ); name = Sources.swift; sourceTree = ""; @@ -746,6 +749,7 @@ 63B18AEC1F2D205C00B79E37 /* CustomStringConvertible.swift in Sources */, 6330B5E11F2E991200D60528 /* Configuration.swift in Sources */, 63B912AA1F1D7B1300D49110 /* firstly.swift in Sources */, + BB2524DE20D729A60010F7B0 /* Dispatcher.swift in Sources */, 636A29211F1C1716001229C2 /* Thenable.swift in Sources */, 632FBBE31F33B273008F8FBB /* Catchable.swift in Sources */, 63B0AC851D595E6300FA21D9 /* dispatch_promise.m in Sources */, diff --git a/Sources/Box.swift b/Sources/Box.swift index 3bf7ecd7a..cf0f99af9 100644 --- a/Sources/Box.swift +++ b/Sources/Box.swift @@ -84,18 +84,14 @@ class EmptyBox: Box { } -extension Optional where Wrapped: DispatchQueue { +extension Optional: Dispatcher where Wrapped: Dispatcher { @inline(__always) - func async(flags: DispatchWorkItemFlags?, _ body: @escaping() -> Void) { + public func async(execute body: @escaping () -> Void) { switch self { case .none: body() - case .some(let q): - if let flags = flags { - q.async(flags: flags, execute: body) - } else { - q.async(execute: body) - } + case .some(let dispatcher): + dispatcher.async(execute: body) } } } diff --git a/Sources/Catchable.swift b/Sources/Catchable.swift index 41ec7277e..8a22fd2e2 100644 --- a/Sources/Catchable.swift +++ b/Sources/Catchable.swift @@ -14,14 +14,14 @@ public extension CatchMixin { 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 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](http://promisekit.org/docs/) */ @discardableResult - func `catch`(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> PMKFinalizer { + func `catch`(on: Dispatcher? = conf.D.return, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> PMKFinalizer { let finalizer = PMKFinalizer() pipe { switch $0 { @@ -29,7 +29,7 @@ public extension CatchMixin { guard policy == .allErrors || !error.isCancelled else { fallthrough } - on.async(flags: flags) { + on.async { body(error) finalizer.pending.resolve(()) } @@ -45,8 +45,8 @@ 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) { + public func finally(on: Dispatcher? = conf.D.return, _ body: @escaping () -> Void) { + pending.guarantee.done(on: on) { body() } } @@ -68,11 +68,11 @@ public extension CatchMixin { return .value(CLLocation.chicago) } - - Parameter on: The queue to which the provided closure dispatches. + - Parameter on: The dispatcher that executes the provided closure. - 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 { + func recover(on: Dispatcher? = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> U) -> Promise where U.T == T { let rp = Promise(.pending) pipe { switch $0 { @@ -80,7 +80,7 @@ public extension CatchMixin { rp.box.seal(.fulfilled(value)) case .rejected(let error): if policy == .allErrors || !error.isCancelled { - on.async(flags: flags) { + on.async { do { let rv = try body(error) guard rv !== rp else { throw PMKError.returnedSelf } @@ -101,19 +101,19 @@ public extension CatchMixin { 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 on: The dispatcher that executes the provided closure. - 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 { + func recover(on: Dispatcher? = conf.D.map, _ 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) { + on.async { body(error).pipe(to: rg.box.seal) } } @@ -134,14 +134,14 @@ public extension CatchMixin { //… } - - Parameter on: The queue to which the provided closure dispatches. + - 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: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) -> Promise { + func ensure(on: Dispatcher? = conf.D.return, _ body: @escaping () -> Void) -> Promise { let rp = Promise(.pending) pipe { result in - on.async(flags: flags) { + on.async { body() rp.box.seal(result) } @@ -163,14 +163,14 @@ public extension CatchMixin { //… } - - Parameter on: The queue to which the provided closure dispatches. + - 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: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Guarantee) -> Promise { + func ensureThen(on: Dispatcher? = conf.D.return, _ body: @escaping () -> Guarantee) -> Promise { let rp = Promise(.pending) pipe { result in - on.async(flags: flags) { + on.async { body().done { rp.box.seal(result) } @@ -180,7 +180,6 @@ public extension CatchMixin { } - /** 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. @@ -201,19 +200,19 @@ public extension CatchMixin where T == Void { 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 on: The dispatcher that executes the provided closure. - 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 { + func recover(on: Dispatcher? = conf.D.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) { + on.async { body(error) rg.box.seal(()) } @@ -227,11 +226,11 @@ public extension CatchMixin where T == Void { 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 on: The dispatcher that executes the provided closure. - 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 { + func recover(on: Dispatcher? = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> Void) -> Promise { let rg = Promise(.pending) pipe { switch $0 { @@ -239,7 +238,7 @@ public extension CatchMixin where T == Void { rg.box.seal(.fulfilled(())) case .rejected(let error): if policy == .allErrors || !error.isCancelled { - on.async(flags: flags) { + on.async { do { rg.box.seal(.fulfilled(try body(error))) } catch { diff --git a/Sources/Configuration.swift b/Sources/Configuration.swift index 4891c45e8..0e64d5787 100644 --- a/Sources/Configuration.swift +++ b/Sources/Configuration.swift @@ -2,8 +2,14 @@ import Dispatch /// PromiseKit’s configurable parameters public struct PMKConfiguration { - /// The default queues that promises handlers dispatch to - public var Q: (map: DispatchQueue?, return: DispatchQueue?) = (map: DispatchQueue.main, return: DispatchQueue.main) + /// Backward compatibility: default DispatchQueues that promise handlers dispatch to + public var Q: (map: DispatchQueue?, return: DispatchQueue?) { + get { return (map: D.map as? DispatchQueue, return: D.return as? DispatchQueue) } + set { D = (map: newValue.map, return: newValue.return) } + } + + /// The default Dispatchers that promise handlers dispatch to + public var D: (map: Dispatcher?, return: Dispatcher?) = (map: DispatchQueue.main, return: DispatchQueue.main) /// The default catch-policy for all `catch` and `resolve` public var catchPolicy = CatchPolicy.allErrorsExceptCancellation diff --git a/Sources/Deprecations.swift b/Sources/Deprecations.swift index ac4eb364b..27300a502 100644 --- a/Sources/Deprecations.swift +++ b/Sources/Deprecations.swift @@ -46,7 +46,7 @@ public extension Thenable { #if PMKFullDeprecations /// disabled due to ambiguity with the other `.flatMap` @available(*, deprecated: 6.1, message: "See: `compactMap`") - func flatMap(on: DispatchQueue? = conf.Q.map, _ transform: @escaping(T) throws -> U?) -> Promise { + func flatMap(on: DispatchQueue? = .pmkDefault, _ transform: @escaping(T) throws -> U?) -> Promise { return compactMap(on: on, transform) } #endif @@ -56,19 +56,19 @@ public extension Thenable where T: Sequence { #if PMKFullDeprecations /// disabled due to ambiguity with the other `.map` @available(*, deprecated, message: "See: `mapValues`") - func map(on: DispatchQueue? = conf.Q.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> { + func map(on: DispatchQueue? = .pmkDefault, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> { return mapValues(on: on, transform) } /// disabled due to ambiguity with the other `.flatMap` @available(*, deprecated, message: "See: `flatMapValues`") - func flatMap(on: DispatchQueue? = conf.Q.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> { + func flatMap(on: DispatchQueue? = .pmkDefault, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> { return flatMapValues(on: on, transform) } #endif @available(*, deprecated, message: "See: `filterValues`") - func filter(on: DispatchQueue? = conf.Q.map, test: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> { + func filter(on: DispatchQueue? = .pmkDefault, test: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> { return filterValues(on: on, test) } } @@ -87,7 +87,7 @@ public extension Thenable where T: Collection { public extension Thenable where T: Sequence, T.Iterator.Element: Comparable { @available(*, deprecated, message: "See: `sortedValues`") - func sorted(on: DispatchQueue? = conf.Q.map) -> Promise<[T.Iterator.Element]> { + func sorted(on: DispatchQueue? = .pmkDefault) -> Promise<[T.Iterator.Element]> { return sortedValues(on: on) } } diff --git a/Sources/Dispatcher.swift b/Sources/Dispatcher.swift new file mode 100644 index 000000000..0750f1e54 --- /dev/null +++ b/Sources/Dispatcher.swift @@ -0,0 +1,475 @@ +import Dispatch + +public protocol Dispatcher { + func async(execute work: @escaping () -> Void) +} + +public struct DispatchQueueDispatcher: Dispatcher { + + let queue: DispatchQueue + let flags: DispatchWorkItemFlags + + init(queue: DispatchQueue, flags: DispatchWorkItemFlags) { + self.queue = queue + self.flags = flags + } + + public func async(execute work: @escaping () -> Void) { + queue.async(flags: flags, execute: work) + } + +} + +extension DispatchQueue: Dispatcher { + + /// Explicit declaration required; actual function signature is not identical to protocol + public func async(execute work: @escaping () -> Void) { + async(execute: work) + } + +} + +/// Used as default parameter for backward compatibility since clients may explicitly +/// specify "nil" to turn off dispatching. We need to distinguish three cases: explicit +/// queue, explicit nil, and no value specified. Dispatchers from conf.D cannot directly +/// be used as default parameter values because they are not necessarily DispatchQueues. + +public extension DispatchQueue { + static var pmkDefault = DispatchQueue(label: "org.promisekit.sentinel") +} + +extension DispatchQueue { + + public func asDispatcher(withFlags flags: DispatchWorkItemFlags? = nil) -> Dispatcher { + if let flags = flags { + return DispatchQueueDispatcher(queue: self, flags: flags) + } + return self + } + +} + +/// This hairball disambiguates all the various combinations of explicit arguments, default +/// arguments, and configured defaults. In particular, a method that is given explicit work item +/// flags but no DispatchQueue should still work (that is, the dispatcher should use those flags) +/// as long as the configured default is actually some kind of DispatchQueue. +/// +/// TODO: should conf.D = nil turn off dispatching even if explicit dispatch arguments are given? + +fileprivate func selectDispatcher(given: DispatchQueue?, configured: Dispatcher?, flags: DispatchWorkItemFlags?) -> Dispatcher? { + guard let given = given else { + if flags != nil { + print("PromiseKit: warning: nil DispatchQueue specified, but DispatchWorkItemFlags were also supplied (ignored)") + } + return nil + } + if given !== DispatchQueue.pmkDefault { + return given.asDispatcher(withFlags: flags) + } else if let flags = flags, let configured = configured as? DispatchQueue { + return configured.asDispatcher(withFlags: flags) + } else if flags != nil && configured != nil { + print("PromiseKit: warning: DispatchWorkItemFlags flags specified, but default dispatcher is not a DispatchQueue (ignored)") + } + return configured +} + +/// Backward compatibility for DispatchQueues in public API + +public extension Guarantee { + + @discardableResult + func done(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> Void) -> Guarantee { + let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags) + return done(on: dispatcher, body) + } + + func map(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> U) -> Guarantee { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return map(on: dispatcher, body) + } + + @discardableResult + func then(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> Guarantee) -> Guarantee { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return then(on: dispatcher, body) + } + +} + +public extension Guarantee where T: Sequence { + + func thenMap(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) -> Guarantee) -> Guarantee<[U]> { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return thenMap(on: dispatcher, transform) + } + +} + +public extension Thenable { + + /** + The provided closure executes when this promise resolves. + + This allows chaining promises. The promise returned by the provided closure is resolved before the promise returned by this closure resolves. + + - Parameter on: The queue to which the provided closure dispatches. + - Parameter body: The closure that executes when this promise fulfills. It must return a promise. + - Returns: A new promise that resolves when the promise returned from the provided closure resolves. For example: + + firstly { + URLSession.shared.dataTask(.promise, with: url1) + }.then { response in + transform(data: response.data) + }.done { transformation in + //… + } + */ + func then(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> U) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return then(on: dispatcher, body) + } + + /** + The provided closure is executed when this promise is resolved. + + This is like `then` but it requires the closure to return a non-promise. + + - Parameter on: The queue to which the provided closure dispatches. + - Parameter transform: The closure that is executed when this Promise is fulfilled. It must return a non-promise. + - Returns: A new promise that is resolved with the value returned from the provided closure. For example: + + firstly { + URLSession.shared.dataTask(.promise, with: url1) + }.map { response in + response.data.length + }.done { length in + //… + } + */ + func map(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T) throws -> U) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return map(on: dispatcher, transform) + } + + /** + The provided closure is executed when this promise is resolved. + + In your closure return an `Optional`, if you return `nil` the resulting promise is rejected with `PMKError.compactMap`, otherwise the promise is fulfilled with the unwrapped value. + + firstly { + URLSession.shared.dataTask(.promise, with: url) + }.compactMap { + try JSONSerialization.jsonObject(with: $0.data) as? [String: String] + }.done { dictionary in + //… + }.catch { + // either `PMKError.compactMap` or a `JSONError` + } + */ + func compactMap(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T) throws -> U?) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return compactMap(on: dispatcher, transform) + } + + /** + The provided closure is executed when this 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 promise fulfilled as `Void`. + + firstly { + URLSession.shared.dataTask(.promise, with: url) + }.done { response in + print(response.data) + } + */ + func done(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> Void) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags) + return done(on: dispatcher, body) + } + + /** + The provided closure is executed when this 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 Promise maintains that value. + + - 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 promise that is resolved with the value that the handler is fed. For example: + + firstly { + .value(1) + }.get { foo in + print(foo, " is 1") + }.done { foo in + print(foo, " is 1") + }.done { foo in + print(foo, " is Void") + } + */ + func get(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (T) throws -> Void) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags) + return get(on: dispatcher, body) + } +} + +public extension Thenable where T: Sequence { + /** + `Promise<[T]>` => `T` -> `U` => `Promise<[U]>` + + firstly { + .value([1,2,3]) + }.mapValues { integer in + integer * 2 + }.done { + // $0 => [2,4,6] + } + */ + func mapValues(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return mapValues(on: dispatcher, transform) + } + + /** + `Promise<[T]>` => `T` -> `[U]` => `Promise<[U]>` + + firstly { + .value([1,2,3]) + }.flatMapValues { integer in + [integer, integer] + }.done { + // $0 => [1,1,2,2,3,3] + } + */ + func flatMapValues(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return flatMapValues(on: dispatcher, transform) + } + + /** + `Promise<[T]>` => `T` -> `U?` => `Promise<[U]>` + + firstly { + .value(["1","2","a","3"]) + }.compactMapValues { + Int($0) + }.done { + // $0 => [1,2,3] + } + */ + func compactMapValues(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U?) -> Promise<[U]> { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return compactMapValues(on: dispatcher, transform) + } + + /** + `Promise<[T]>` => `T` -> `Promise` => `Promise<[U]>` + + firstly { + .value([1,2,3]) + }.thenMap { integer in + .value(integer * 2) + }.done { + // $0 => [2,4,6] + } + */ + func thenMap(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T]> { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return thenMap(on: dispatcher, transform) + } + + /** + `Promise<[T]>` => `T` -> `Promise<[U]>` => `Promise<[U]>` + + firstly { + .value([1,2,3]) + }.thenFlatMap { integer in + .value([integer, integer]) + }.done { + // $0 => [1,1,2,2,3,3] + } + */ + func thenFlatMap(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T.Iterator.Element]> where U.T: Sequence { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return thenFlatMap(on: dispatcher, transform) + } + + /** + `Promise<[T]>` => `T` -> Bool => `Promise<[U]>` + + firstly { + .value([1,2,3]) + }.filterValues { + $0 > 1 + }.done { + // $0 => [2,3] + } + */ + func filterValues(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ isIncluded: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return filterValues(on: dispatcher, isIncluded) + } +} + +public extension Thenable where T: Collection { + func firstValue(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, where test: @escaping (T.Iterator.Element) -> Bool) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return firstValue(on: dispatcher, where: test) + } +} + +public extension Thenable where T: Sequence, T.Iterator.Element: Comparable { + /// - Returns: a promise fulfilled with the sorted values of this `Sequence`. + func sortedValues(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil) -> Promise<[T.Iterator.Element]> { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return sortedValues(on: dispatcher) + } +} + +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? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> PMKFinalizer { + let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags) + return `catch`(on: dispatcher, policy: policy, body) + } + + /** + 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 dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return recover(on: dispatcher, policy: policy, body) + } + + /** + 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? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Error) -> Guarantee) -> Guarantee { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return recover(on: dispatcher, body) + } + + /** + 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? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags) + return ensure(on: dispatcher, body) + } + + /** + 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? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Guarantee) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags) + return ensureThen(on: dispatcher, body) + } +} + +public extension PMKFinalizer { + /// `finally` is the same as `ensure`, but it is not chainable + public func finally(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping () -> Void) { + let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags) + return finally(on: dispatcher, body) + } +} + +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? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Error) -> Void) -> Guarantee { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return recover(on: dispatcher, body) + } + + /** + 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? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> Void) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return recover(on: dispatcher, policy: policy, body) + } +} diff --git a/Sources/Guarantee.swift b/Sources/Guarantee.swift index 1a60d57ac..888580a63 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -66,10 +66,10 @@ public class Guarantee: Thenable { public extension Guarantee { @discardableResult - func done(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> Void) -> Guarantee { + func done(on: Dispatcher? = conf.D.return, _ body: @escaping(T) -> Void) -> Guarantee { let rg = Guarantee(.pending) pipe { (value: T) in - on.async(flags: flags) { + on.async { body(value) rg.box.seal(()) } @@ -77,10 +77,10 @@ public extension Guarantee { return rg } - func map(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> U) -> Guarantee { + func map(on: Dispatcher? = conf.D.map, _ body: @escaping(T) -> U) -> Guarantee { let rg = Guarantee(.pending) pipe { value in - on.async(flags: flags) { + on.async { rg.box.seal(body(value)) } } @@ -88,10 +88,10 @@ public extension Guarantee { } @discardableResult - func then(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> Guarantee) -> Guarantee { + func then(on: Dispatcher? = conf.D.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> Guarantee) -> Guarantee { let rg = Guarantee(.pending) pipe { value in - on.async(flags: flags) { + on.async { body(value).pipe(to: rg.box.seal) } } @@ -99,7 +99,7 @@ public extension Guarantee { } public func asVoid() -> Guarantee { - return map(on: nil) { _ in } + return map { _ in } } /** @@ -128,7 +128,7 @@ public extension Guarantee { public extension Guarantee where T: Sequence { /** - `Guarantee<[T]>` => `T` -> `Guarantee` => `Guaranetee<[U]>` + `Guarantee<[T]>` => `T` -> `Guarantee` => `Guarantee<[U]>` firstly { .value([1,2,3]) @@ -138,8 +138,8 @@ public extension Guarantee where T: Sequence { // $0 => [2,4,6] } */ - func thenMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) -> Guarantee) -> Guarantee<[U]> { - return then(on: on, flags: flags) { + func thenMap(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) -> Guarantee) -> Guarantee<[U]> { + return then(on: on) { when(fulfilled: $0.map(transform)) }.recover { // if happens then is bug inside PromiseKit @@ -181,6 +181,28 @@ public extension DispatchQueue { } } +public extension Dispatcher { + /** + Asynchronously executes the provided closure on a Dispatcher. + + dispatcher.guarantee { + md5(input) + }.done { md5 in + //… + } + + - Parameter body: The closure that resolves this promise. + - Returns: A new `Guarantee` resolved by the result of the provided closure. + - Note: There is no Promise/Thenable version of this due to Swift compiler ambiguity issues. + */ + func guarantee(execute body: @escaping () -> T) -> Guarantee { + let rg = Guarantee(.pending) + async { + rg.box.seal(body()) + } + return rg + } +} #if os(Linux) import func CoreFoundation._CFIsMainThread diff --git a/Sources/Promise.swift b/Sources/Promise.swift index 4dd6f796f..62294e85b 100644 --- a/Sources/Promise.swift +++ b/Sources/Promise.swift @@ -178,6 +178,33 @@ public extension DispatchQueue { } } +public extension Dispatcher { + /** + Asynchronously executes the provided closure on a Dispatcher. + + dispatcher.promise { + try md5(input) + }.done { md5 in + //… + } + + - Parameter body: The closure that resolves this promise. + - Returns: A new `Promise` resolved by the result of the provided closure. + - Note: There is no Promise/Thenable version of this due to Swift compiler ambiguity issues. + */ + func promise(execute body: @escaping () throws -> T) -> Promise { + let promise = Promise(.pending) + async { + do { + promise.box.seal(.fulfilled(try body())) + } catch { + promise.box.seal(.rejected(error)) + } + } + return promise + } +} + /// used by our extensions to provide unambiguous functions with the same name as the original function public enum PMKNamespacer { diff --git a/Sources/Thenable.swift b/Sources/Thenable.swift index 4178d5e1f..61c86dea4 100644 --- a/Sources/Thenable.swift +++ b/Sources/Thenable.swift @@ -18,7 +18,7 @@ public extension Thenable { This allows chaining promises. The promise returned by the provided closure is resolved before the promise returned by this closure resolves. - - Parameter on: The queue to which the provided closure dispatches. + - Parameter on: The dispatcher that executes the provided closure. - Parameter body: The closure that executes when this promise fulfills. It must return a promise. - Returns: A new promise that resolves when the promise returned from the provided closure resolves. For example: @@ -30,12 +30,12 @@ public extension Thenable { //… } */ - func then(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> U) -> Promise { + func then(on: Dispatcher? = conf.D.map, _ body: @escaping(T) throws -> U) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async(flags: flags) { + on.async { do { let rv = try body(value) guard rv !== rp else { throw PMKError.returnedSelf } @@ -56,7 +56,7 @@ public extension Thenable { This is like `then` but it requires the closure to return a non-promise. - - Parameter on: The queue to which the provided closure dispatches. + - Parameter on: The dispatcher that executes the provided closure. - Parameter transform: The closure that is executed when this Promise is fulfilled. It must return a non-promise. - Returns: A new promise that is resolved with the value returned from the provided closure. For example: @@ -68,12 +68,12 @@ public extension Thenable { //… } */ - func map(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T) throws -> U) -> Promise { + func map(on: Dispatcher? = conf.D.map, _ transform: @escaping(T) throws -> U) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async(flags: flags) { + on.async { do { rp.box.seal(.fulfilled(try transform(value))) } catch { @@ -102,12 +102,12 @@ public extension Thenable { // either `PMKError.compactMap` or a `JSONError` } */ - func compactMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T) throws -> U?) -> Promise { + func compactMap(on: Dispatcher? = conf.D.map, _ transform: @escaping(T) throws -> U?) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async(flags: flags) { + on.async { do { if let rv = try transform(value) { rp.box.seal(.fulfilled(rv)) @@ -131,7 +131,7 @@ public extension Thenable { 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 on: The dispatcher that executes the provided closure. - Parameter body: The closure that is executed when this Promise is fulfilled. - Returns: A new promise fulfilled as `Void`. @@ -141,12 +141,12 @@ public extension Thenable { print(response.data) } */ - func done(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) throws -> Void) -> Promise { + func done(on: Dispatcher? = conf.Q.return, _ body: @escaping(T) throws -> Void) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async(flags: flags) { + on.async { do { try body(value) rp.box.seal(.fulfilled(())) @@ -167,7 +167,7 @@ public extension Thenable { This is like `done` but it returns the same value that the handler is fed. `get` immutably accesses the fulfilled value; the returned Promise maintains that value. - - Parameter on: The queue to which the provided closure dispatches. + - Parameter on: The dispatcher that executes the provided closure. - Parameter body: The closure that is executed when this Promise is fulfilled. - Returns: A new promise that is resolved with the value that the handler is fed. For example: @@ -181,7 +181,7 @@ public extension Thenable { print(foo, " is Void") } */ - func get(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (T) throws -> Void) -> Promise { + func get(on: Dispatcher? = conf.D.return, _ body: @escaping (T) throws -> Void) -> Promise { return map(on: on, flags: flags) { try body($0) return $0 @@ -190,7 +190,7 @@ public extension Thenable { /// - Returns: a new promise chained off this promise but with its value discarded. func asVoid() -> Promise { - return map(on: nil) { _ in } + return map { _ in } } } @@ -264,8 +264,8 @@ public extension Thenable where T: Sequence { // $0 => [2,4,6] } */ - func mapValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> { - return map(on: on, flags: flags){ try $0.map(transform) } + func mapValues(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> { + return map(on: on) { try $0.map(transform) } } /** @@ -279,8 +279,8 @@ public extension Thenable where T: Sequence { // $0 => [1,1,2,2,3,3] } */ - func flatMapValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> { - return map(on: on, flags: flags){ (foo: T) in + func flatMapValues(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> { + return map(on: on){ (foo: T) in try foo.flatMap{ try transform($0) } } } @@ -296,8 +296,8 @@ public extension Thenable where T: Sequence { // $0 => [1,2,3] } */ - func compactMapValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U?) -> Promise<[U]> { - return map(on: on, flags: flags) { foo -> [U] in + func compactMapValues(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U?) -> Promise<[U]> { + return map(on: on) { foo -> [U] in #if !swift(>=3.3) || (swift(>=4) && !swift(>=4.1)) return try foo.flatMap(transform) #else @@ -317,8 +317,8 @@ public extension Thenable where T: Sequence { // $0 => [2,4,6] } */ - func thenMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T]> { - return then(on: on, flags: flags) { + func thenMap(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T]> { + return then(on: on) { when(fulfilled: try $0.map(transform)) } } @@ -334,8 +334,8 @@ public extension Thenable where T: Sequence { // $0 => [1,1,2,2,3,3] } */ - func thenFlatMap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T.Iterator.Element]> where U.T: Sequence { - return then(on: on, flags: flags) { + func thenFlatMap(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T.Iterator.Element]> where U.T: Sequence { + return then(on: on) { when(fulfilled: try $0.map(transform)) }.map(on: nil) { $0.flatMap{ $0 } @@ -353,8 +353,8 @@ public extension Thenable where T: Sequence { // $0 => [2,3] } */ - func filterValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ isIncluded: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> { - return map(on: on, flags: flags) { + func filterValues(on: Dispatcher? = conf.D.map, _ isIncluded: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> { + return map(on: on) { $0.filter(isIncluded) } } @@ -372,8 +372,8 @@ public extension Thenable where T: Collection { } } - func firstValue(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, where test: @escaping (T.Iterator.Element) -> Bool) -> Promise { - return map(on: on, flags: flags) { + func firstValue(on: Dispatcher? = conf.D.map, where test: @escaping (T.Iterator.Element) -> Bool) -> Promise { + return map(on: on) { for x in $0 where test(x) { return x } @@ -396,7 +396,7 @@ public extension Thenable where T: Collection { public extension Thenable where T: Sequence, T.Iterator.Element: Comparable { /// - Returns: a promise fulfilled with the sorted values of this `Sequence`. - func sortedValues(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil) -> Promise<[T.Iterator.Element]> { - return map(on: on, flags: flags){ $0.sorted() } + func sortedValues(on: Dispatcher? = conf.D.map) -> Promise<[T.Iterator.Element]> { + return map(on: on){ $0.sorted() } } } From 1f4ea3996b7a8bfc416151a4773e5aa4502ce4b0 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Mon, 18 Jun 2018 00:43:50 -0700 Subject: [PATCH 2/7] Put back map(on: nil) --- Sources/Guarantee.swift | 2 +- Sources/Thenable.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Guarantee.swift b/Sources/Guarantee.swift index 888580a63..f99a837d6 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -99,7 +99,7 @@ public extension Guarantee { } public func asVoid() -> Guarantee { - return map { _ in } + return map(on: nil) { _ in } } /** diff --git a/Sources/Thenable.swift b/Sources/Thenable.swift index 61c86dea4..0c0e623a4 100644 --- a/Sources/Thenable.swift +++ b/Sources/Thenable.swift @@ -190,7 +190,7 @@ public extension Thenable { /// - Returns: a new promise chained off this promise but with its value discarded. func asVoid() -> Promise { - return map { _ in } + return map(on: nil) { _ in } } } From ffdae005d2970b5f2f782af95ad6adfdaa22c16d Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Mon, 18 Jun 2018 01:25:24 -0700 Subject: [PATCH 3/7] Rename async() method args to forestall ambiguity; compiles --- Sources/Box.swift | 6 +++--- Sources/Dispatcher.swift | 10 +++++----- Sources/Thenable.swift | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/Box.swift b/Sources/Box.swift index cf0f99af9..c0f9ebd99 100644 --- a/Sources/Box.swift +++ b/Sources/Box.swift @@ -84,14 +84,14 @@ class EmptyBox: Box { } -extension Optional: Dispatcher where Wrapped: Dispatcher { +extension Optional: Dispatcher where Wrapped == Dispatcher { @inline(__always) - public func async(execute body: @escaping () -> Void) { + public func async(_ body: @escaping () -> Void) { switch self { case .none: body() case .some(let dispatcher): - dispatcher.async(execute: body) + dispatcher.async(body) } } } diff --git a/Sources/Dispatcher.swift b/Sources/Dispatcher.swift index 0750f1e54..d5e0807b2 100644 --- a/Sources/Dispatcher.swift +++ b/Sources/Dispatcher.swift @@ -1,7 +1,7 @@ import Dispatch public protocol Dispatcher { - func async(execute work: @escaping () -> Void) + func async(_ body: @escaping () -> Void) } public struct DispatchQueueDispatcher: Dispatcher { @@ -14,8 +14,8 @@ public struct DispatchQueueDispatcher: Dispatcher { self.flags = flags } - public func async(execute work: @escaping () -> Void) { - queue.async(flags: flags, execute: work) + public func async(_ body: @escaping () -> Void) { + queue.async(flags: flags, execute: body) } } @@ -23,8 +23,8 @@ public struct DispatchQueueDispatcher: Dispatcher { extension DispatchQueue: Dispatcher { /// Explicit declaration required; actual function signature is not identical to protocol - public func async(execute work: @escaping () -> Void) { - async(execute: work) + public func async(_ body: @escaping () -> Void) { + async(execute: body) } } diff --git a/Sources/Thenable.swift b/Sources/Thenable.swift index 0c0e623a4..7932bb215 100644 --- a/Sources/Thenable.swift +++ b/Sources/Thenable.swift @@ -182,7 +182,7 @@ public extension Thenable { } */ func get(on: Dispatcher? = conf.D.return, _ body: @escaping (T) throws -> Void) -> Promise { - return map(on: on, flags: flags) { + return map(on: on) { try body($0) return $0 } From 2e1e64e762d8b7587d1e73718ce81f46cf30a6a1 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Tue, 19 Jun 2018 18:36:08 -0700 Subject: [PATCH 4/7] Working on Dispatcher tests --- PromiseKit.xcodeproj/project.pbxproj | 4 + Sources/Dispatcher.swift | 2 +- Sources/Thenable.swift | 2 +- Tests/CorePromise/DispatcherTests.swift | 105 ++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 Tests/CorePromise/DispatcherTests.swift diff --git a/PromiseKit.xcodeproj/project.pbxproj b/PromiseKit.xcodeproj/project.pbxproj index 55bd727d6..fefffd501 100644 --- a/PromiseKit.xcodeproj/project.pbxproj +++ b/PromiseKit.xcodeproj/project.pbxproj @@ -79,6 +79,7 @@ 63D9B2EF203385FD0075C00B /* race.m in Sources */ = {isa = PBXBuildFile; fileRef = 63D9B2EE203385FD0075C00B /* race.m */; }; 63D9B2F120338D5D0075C00B /* Deprecations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63D9B2F020338D5D0075C00B /* Deprecations.swift */; }; BB2524DE20D729A60010F7B0 /* Dispatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB2524DD20D729A60010F7B0 /* Dispatcher.swift */; }; + BB4AF7C520D820700008333D /* DispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB4AF7C320D819360008333D /* DispatcherTests.swift */; }; C013F7382048E3B6006B57B1 /* MockNodeEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F7372048E3B6006B57B1 /* MockNodeEnvironment.swift */; }; C013F73A2049076A006B57B1 /* JSPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F7392049076A006B57B1 /* JSPromise.swift */; }; C013F73C20494291006B57B1 /* JSAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F73B20494291006B57B1 /* JSAdapter.swift */; }; @@ -223,6 +224,7 @@ 63D9B2EE203385FD0075C00B /* race.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = race.m; path = Sources/race.m; sourceTree = ""; }; 63D9B2F020338D5D0075C00B /* Deprecations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Deprecations.swift; path = Sources/Deprecations.swift; sourceTree = ""; }; BB2524DD20D729A60010F7B0 /* Dispatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Dispatcher.swift; path = Sources/Dispatcher.swift; sourceTree = ""; }; + BB4AF7C320D819360008333D /* DispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatcherTests.swift; sourceTree = ""; }; C013F7372048E3B6006B57B1 /* MockNodeEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockNodeEnvironment.swift; path = "Tests/JS-A+/MockNodeEnvironment.swift"; sourceTree = ""; }; C013F7392049076A006B57B1 /* JSPromise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = JSPromise.swift; path = "Tests/JS-A+/JSPromise.swift"; sourceTree = ""; }; C013F73B20494291006B57B1 /* JSAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = JSAdapter.swift; path = "Tests/JS-A+/JSAdapter.swift"; sourceTree = ""; }; @@ -352,6 +354,7 @@ 635D64161D59635300BC0AF5 /* StressTests.swift */, 635D640D1D59635300BC0AF5 /* ZalgoTests.swift */, 639BF755203DF02C00FA577B /* Utilities.swift */, + BB4AF7C320D819360008333D /* DispatcherTests.swift */, ); name = Core; path = Tests/CorePromise; @@ -678,6 +681,7 @@ 635D64221D59635300BC0AF5 /* ZalgoTests.swift in Sources */, 635D64271D59635300BC0AF5 /* RaceTests.swift in Sources */, 632FBBE51F33B338008F8FBB /* CatchableTests.swift in Sources */, + BB4AF7C520D820700008333D /* DispatcherTests.swift in Sources */, 63CF6D80203CD19200EC8927 /* ThenableTests.swift in Sources */, 635D642B1D59635300BC0AF5 /* StressTests.swift in Sources */, 630A805A203CEF6800D25F23 /* WhenTests.m in Sources */, diff --git a/Sources/Dispatcher.swift b/Sources/Dispatcher.swift index d5e0807b2..bff518f39 100644 --- a/Sources/Dispatcher.swift +++ b/Sources/Dispatcher.swift @@ -369,7 +369,7 @@ public extension CatchMixin { - 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 { + func recover(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> U) -> Promise where U.T == T { let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) return recover(on: dispatcher, policy: policy, body) } diff --git a/Sources/Thenable.swift b/Sources/Thenable.swift index 7932bb215..d44ee77e1 100644 --- a/Sources/Thenable.swift +++ b/Sources/Thenable.swift @@ -141,7 +141,7 @@ public extension Thenable { print(response.data) } */ - func done(on: Dispatcher? = conf.Q.return, _ body: @escaping(T) throws -> Void) -> Promise { + func done(on: Dispatcher? = conf.D.return, _ body: @escaping(T) throws -> Void) -> Promise { let rp = Promise(.pending) pipe { switch $0 { diff --git a/Tests/CorePromise/DispatcherTests.swift b/Tests/CorePromise/DispatcherTests.swift new file mode 100644 index 000000000..64ae85178 --- /dev/null +++ b/Tests/CorePromise/DispatcherTests.swift @@ -0,0 +1,105 @@ +import PromiseKit +import XCTest + +class RecordingDispatcher: Dispatcher { + + var dispatchCount = 0 + + func async(_ body: @escaping () -> Void) { + dispatchCount += 1 + DispatchQueue.global(qos: .background).async(execute: body) + } + +} + +class DispatcherTests: XCTestCase { + + var dispatcher = RecordingDispatcher() + var dispatcherB = RecordingDispatcher() + + override func setUp() { + dispatcher = RecordingDispatcher() + dispatcherB = RecordingDispatcher() + } + + func testConfD() { + let ex = expectation(description: "conf.D") + let oldConf = PromiseKit.conf.D + PromiseKit.conf.D.map = dispatcher + PromiseKit.conf.D.return = dispatcherB + XCTAssertNil(PromiseKit.conf.Q.map) // Not representable as DispatchQueues + XCTAssertNil(PromiseKit.conf.Q.return) + Promise { seal in + seal.fulfill(42) + }.map { + $0 + 10 + }.done() { + XCTAssertEqual($0, 52) + XCTAssertEqual(self.dispatcher.dispatchCount, 1) + XCTAssertEqual(self.dispatcherB.dispatchCount, 1) + ex.fulfill() + }.cauterize() + waitForExpectations(timeout: 1) + PromiseKit.conf.D.map = DispatchQueue.main + PromiseKit.conf.Q.return = .main + XCTAssert(PromiseKit.conf.Q.map === DispatchQueue.main) + XCTAssert((PromiseKit.conf.D.return as? DispatchQueue)! === DispatchQueue.main) + PromiseKit.conf.D = oldConf + } + + func testDispatcherWithThrow() { + let ex = expectation(description: "Dispatcher with throw") + Promise { 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 testDispatchQueueBackwardCompatibility() { + let ex = expectation(description: "DispatchQueue compatibility") + let oldConf = PromiseKit.conf.D + PromiseKit.conf.D = (map: dispatcher, return: dispatcher) + Promise.value(42).map(on: .global(qos: .background), flags: .barrier) { (x: Int) -> Int in + return x + 10 + }.then(on: .main, flags: []) { + XCTAssertEqual($0, 52) + return Promise.value(50) + }.done(on: .global(qos: .userInitiated)) { + XCTAssertEqual($0, 50) + ex.fulfill() + }.cauterize() + waitForExpectations(timeout: 1) + XCTAssertEqual(self.dispatcher.dispatchCount, 0) + PromiseKit.conf.D = oldConf + } + + func testDispatcherPromiseExtension() { + let ex = expectation(description: "Dispatcher.promise") + dispatcher.promise { + return 42 + }.done(on: dispatcher) { + XCTAssertEqual($0, 42) + XCTAssertEqual(self.dispatcher.dispatchCount, 2) + ex.fulfill() + }.cauterize() + waitForExpectations(timeout: 1) + } + + func testDispatcherGuaranteeExtension() { + let ex = expectation(description: "Dispatcher.guarantee") + dispatcher.guarantee { + return 42 + }.done(on: .main) { + XCTAssertEqual($0, 42) + XCTAssertEqual(self.dispatcher.dispatchCount, 1) + ex.fulfill() + } + waitForExpectations(timeout: 1) + } + +} From c02fb969d6493399020bd817fa93ed6d734b622a Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Wed, 20 Jun 2018 12:54:41 -0700 Subject: [PATCH 5/7] Make Dispatcher arguments nonoptional --- Sources/Box.swift | 13 ------------- Sources/Catchable.swift | 30 +++++++++++++++--------------- Sources/Configuration.swift | 4 ++-- Sources/Dispatcher.swift | 28 +++++++++++++++------------- Sources/Guarantee.swift | 16 ++++++++-------- Sources/Promise.swift | 2 +- Sources/Thenable.swift | 34 +++++++++++++++++----------------- 7 files changed, 58 insertions(+), 69 deletions(-) diff --git a/Sources/Box.swift b/Sources/Box.swift index c0f9ebd99..862269f4d 100644 --- a/Sources/Box.swift +++ b/Sources/Box.swift @@ -82,16 +82,3 @@ class EmptyBox: Box { } } } - - -extension Optional: Dispatcher where Wrapped == Dispatcher { - @inline(__always) - public func async(_ body: @escaping () -> Void) { - switch self { - case .none: - body() - case .some(let dispatcher): - dispatcher.async(body) - } - } -} diff --git a/Sources/Catchable.swift b/Sources/Catchable.swift index 8a22fd2e2..83c1c33f7 100644 --- a/Sources/Catchable.swift +++ b/Sources/Catchable.swift @@ -21,7 +21,7 @@ public extension CatchMixin { - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ @discardableResult - func `catch`(on: Dispatcher? = conf.D.return, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> PMKFinalizer { + func `catch`(on: Dispatcher = conf.D.return, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) -> Void) -> PMKFinalizer { let finalizer = PMKFinalizer() pipe { switch $0 { @@ -29,7 +29,7 @@ public extension CatchMixin { guard policy == .allErrors || !error.isCancelled else { fallthrough } - on.async { + on.dispatch { body(error) finalizer.pending.resolve(()) } @@ -45,7 +45,7 @@ public class PMKFinalizer { let pending = Guarantee.pending() /// `finally` is the same as `ensure`, but it is not chainable - public func finally(on: Dispatcher? = conf.D.return, _ body: @escaping () -> Void) { + public func finally(on: Dispatcher = conf.D.return, _ body: @escaping () -> Void) { pending.guarantee.done(on: on) { body() } @@ -72,7 +72,7 @@ public extension CatchMixin { - Parameter body: The handler to execute if this promise is rejected. - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ - func recover(on: Dispatcher? = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> U) -> Promise where U.T == T { + func recover(on: Dispatcher = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> U) -> Promise where U.T == T { let rp = Promise(.pending) pipe { switch $0 { @@ -80,7 +80,7 @@ public extension CatchMixin { rp.box.seal(.fulfilled(value)) case .rejected(let error): if policy == .allErrors || !error.isCancelled { - on.async { + on.dispatch { do { let rv = try body(error) guard rv !== rp else { throw PMKError.returnedSelf } @@ -106,14 +106,14 @@ public extension CatchMixin { - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ @discardableResult - func recover(on: Dispatcher? = conf.D.map, _ body: @escaping(Error) -> Guarantee) -> Guarantee { + func recover(on: Dispatcher = conf.D.map, _ 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 { + on.dispatch { body(error).pipe(to: rg.box.seal) } } @@ -138,10 +138,10 @@ public extension CatchMixin { - 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) -> Promise { + func ensure(on: Dispatcher = conf.D.return, _ body: @escaping () -> Void) -> Promise { let rp = Promise(.pending) pipe { result in - on.async { + on.dispatch { body() rp.box.seal(result) } @@ -167,10 +167,10 @@ public extension CatchMixin { - 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 () -> Guarantee) -> Promise { + func ensureThen(on: Dispatcher = conf.D.return, _ body: @escaping () -> Guarantee) -> Promise { let rp = Promise(.pending) pipe { result in - on.async { + on.dispatch { body().done { rp.box.seal(result) } @@ -205,14 +205,14 @@ public extension CatchMixin where T == Void { - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ @discardableResult - func recover(on: Dispatcher? = conf.D.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Error) -> Void) -> Guarantee { + func recover(on: Dispatcher = conf.D.map, _ body: @escaping(Error) -> Void) -> Guarantee { let rg = Guarantee(.pending) pipe { switch $0 { case .fulfilled: rg.box.seal(()) case .rejected(let error): - on.async { + on.dispatch { body(error) rg.box.seal(()) } @@ -230,7 +230,7 @@ public extension CatchMixin where T == Void { - Parameter body: The handler to execute if this promise is rejected. - SeeAlso: [Cancellation](http://promisekit.org/docs/) */ - func recover(on: Dispatcher? = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> Void) -> Promise { + func recover(on: Dispatcher = conf.D.map, policy: CatchPolicy = conf.catchPolicy, _ body: @escaping(Error) throws -> Void) -> Promise { let rg = Promise(.pending) pipe { switch $0 { @@ -238,7 +238,7 @@ public extension CatchMixin where T == Void { rg.box.seal(.fulfilled(())) case .rejected(let error): if policy == .allErrors || !error.isCancelled { - on.async { + on.dispatch { do { rg.box.seal(.fulfilled(try body(error))) } catch { diff --git a/Sources/Configuration.swift b/Sources/Configuration.swift index 0e64d5787..65aec1351 100644 --- a/Sources/Configuration.swift +++ b/Sources/Configuration.swift @@ -5,11 +5,11 @@ public struct PMKConfiguration { /// Backward compatibility: default DispatchQueues that promise handlers dispatch to public var Q: (map: DispatchQueue?, return: DispatchQueue?) { get { return (map: D.map as? DispatchQueue, return: D.return as? DispatchQueue) } - set { D = (map: newValue.map, return: newValue.return) } + set { D = (map: newValue.map ?? CurrentThreadDispatcher(), return: newValue.return ?? CurrentThreadDispatcher()) } } /// The default Dispatchers that promise handlers dispatch to - public var D: (map: Dispatcher?, return: Dispatcher?) = (map: DispatchQueue.main, return: DispatchQueue.main) + public var D: (map: Dispatcher, return: Dispatcher) = (map: DispatchQueue.main, return: DispatchQueue.main) /// The default catch-policy for all `catch` and `resolve` public var catchPolicy = CatchPolicy.allErrorsExceptCancellation diff --git a/Sources/Dispatcher.swift b/Sources/Dispatcher.swift index bff518f39..2d7a244b8 100644 --- a/Sources/Dispatcher.swift +++ b/Sources/Dispatcher.swift @@ -1,10 +1,10 @@ import Dispatch public protocol Dispatcher { - func async(_ body: @escaping () -> Void) + func dispatch(_ body: @escaping () -> Void) } -public struct DispatchQueueDispatcher: Dispatcher { +public class DispatchQueueDispatcher: Dispatcher { let queue: DispatchQueue let flags: DispatchWorkItemFlags @@ -14,19 +14,23 @@ public struct DispatchQueueDispatcher: Dispatcher { self.flags = flags } - public func async(_ body: @escaping () -> Void) { + public func dispatch(_ body: @escaping () -> Void) { queue.async(flags: flags, execute: body) } } +public struct CurrentThreadDispatcher: Dispatcher { + public func dispatch(_ body: @escaping () -> Void) { + body() + } +} + extension DispatchQueue: Dispatcher { - /// Explicit declaration required; actual function signature is not identical to protocol - public func async(_ body: @escaping () -> Void) { + public func dispatch(_ body: @escaping () -> Void) { async(execute: body) } - } /// Used as default parameter for backward compatibility since clients may explicitly @@ -38,15 +42,13 @@ public extension DispatchQueue { static var pmkDefault = DispatchQueue(label: "org.promisekit.sentinel") } -extension DispatchQueue { - - public func asDispatcher(withFlags flags: DispatchWorkItemFlags? = nil) -> Dispatcher { +public extension DispatchQueue { + func asDispatcher(withFlags flags: DispatchWorkItemFlags? = nil) -> Dispatcher { if let flags = flags { return DispatchQueueDispatcher(queue: self, flags: flags) } return self } - } /// This hairball disambiguates all the various combinations of explicit arguments, default @@ -56,18 +58,18 @@ extension DispatchQueue { /// /// TODO: should conf.D = nil turn off dispatching even if explicit dispatch arguments are given? -fileprivate func selectDispatcher(given: DispatchQueue?, configured: Dispatcher?, flags: DispatchWorkItemFlags?) -> Dispatcher? { +fileprivate func selectDispatcher(given: DispatchQueue?, configured: Dispatcher, flags: DispatchWorkItemFlags?) -> Dispatcher { guard let given = given else { if flags != nil { print("PromiseKit: warning: nil DispatchQueue specified, but DispatchWorkItemFlags were also supplied (ignored)") } - return nil + return CurrentThreadDispatcher() } if given !== DispatchQueue.pmkDefault { return given.asDispatcher(withFlags: flags) } else if let flags = flags, let configured = configured as? DispatchQueue { return configured.asDispatcher(withFlags: flags) - } else if flags != nil && configured != nil { + } else if flags != nil { print("PromiseKit: warning: DispatchWorkItemFlags flags specified, but default dispatcher is not a DispatchQueue (ignored)") } return configured diff --git a/Sources/Guarantee.swift b/Sources/Guarantee.swift index f99a837d6..641b32148 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -66,10 +66,10 @@ public class Guarantee: Thenable { public extension Guarantee { @discardableResult - func done(on: Dispatcher? = conf.D.return, _ body: @escaping(T) -> Void) -> Guarantee { + func done(on: Dispatcher = conf.D.return, _ body: @escaping(T) -> Void) -> Guarantee { let rg = Guarantee(.pending) pipe { (value: T) in - on.async { + on.dispatch { body(value) rg.box.seal(()) } @@ -77,10 +77,10 @@ public extension Guarantee { return rg } - func map(on: Dispatcher? = conf.D.map, _ body: @escaping(T) -> U) -> Guarantee { + func map(on: Dispatcher = conf.D.map, _ body: @escaping(T) -> U) -> Guarantee { let rg = Guarantee(.pending) pipe { value in - on.async { + on.dispatch { rg.box.seal(body(value)) } } @@ -88,10 +88,10 @@ public extension Guarantee { } @discardableResult - func then(on: Dispatcher? = conf.D.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> Guarantee) -> Guarantee { + func then(on: Dispatcher = conf.D.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> Guarantee) -> Guarantee { let rg = Guarantee(.pending) pipe { value in - on.async { + on.dispatch { body(value).pipe(to: rg.box.seal) } } @@ -138,7 +138,7 @@ public extension Guarantee where T: Sequence { // $0 => [2,4,6] } */ - func thenMap(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) -> Guarantee) -> Guarantee<[U]> { + func thenMap(on: Dispatcher = conf.D.map, _ transform: @escaping(T.Iterator.Element) -> Guarantee) -> Guarantee<[U]> { return then(on: on) { when(fulfilled: $0.map(transform)) }.recover { @@ -197,7 +197,7 @@ public extension Dispatcher { */ func guarantee(execute body: @escaping () -> T) -> Guarantee { let rg = Guarantee(.pending) - async { + dispatch { rg.box.seal(body()) } return rg diff --git a/Sources/Promise.swift b/Sources/Promise.swift index 62294e85b..683024d7b 100644 --- a/Sources/Promise.swift +++ b/Sources/Promise.swift @@ -194,7 +194,7 @@ public extension Dispatcher { */ func promise(execute body: @escaping () throws -> T) -> Promise { let promise = Promise(.pending) - async { + dispatch { do { promise.box.seal(.fulfilled(try body())) } catch { diff --git a/Sources/Thenable.swift b/Sources/Thenable.swift index d44ee77e1..2c911e20f 100644 --- a/Sources/Thenable.swift +++ b/Sources/Thenable.swift @@ -30,12 +30,12 @@ public extension Thenable { //… } */ - func then(on: Dispatcher? = conf.D.map, _ body: @escaping(T) throws -> U) -> Promise { + func then(on: Dispatcher = conf.D.map, _ body: @escaping(T) throws -> U) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async { + on.dispatch { do { let rv = try body(value) guard rv !== rp else { throw PMKError.returnedSelf } @@ -68,12 +68,12 @@ public extension Thenable { //… } */ - func map(on: Dispatcher? = conf.D.map, _ transform: @escaping(T) throws -> U) -> Promise { + func map(on: Dispatcher = conf.D.map, _ transform: @escaping(T) throws -> U) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async { + on.dispatch { do { rp.box.seal(.fulfilled(try transform(value))) } catch { @@ -102,12 +102,12 @@ public extension Thenable { // either `PMKError.compactMap` or a `JSONError` } */ - func compactMap(on: Dispatcher? = conf.D.map, _ transform: @escaping(T) throws -> U?) -> Promise { + func compactMap(on: Dispatcher = conf.D.map, _ transform: @escaping(T) throws -> U?) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async { + on.dispatch { do { if let rv = try transform(value) { rp.box.seal(.fulfilled(rv)) @@ -141,12 +141,12 @@ public extension Thenable { print(response.data) } */ - func done(on: Dispatcher? = conf.D.return, _ body: @escaping(T) throws -> Void) -> Promise { + func done(on: Dispatcher = conf.D.return, _ body: @escaping(T) throws -> Void) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async { + on.dispatch { do { try body(value) rp.box.seal(.fulfilled(())) @@ -181,7 +181,7 @@ public extension Thenable { print(foo, " is Void") } */ - func get(on: Dispatcher? = conf.D.return, _ body: @escaping (T) throws -> Void) -> Promise { + func get(on: Dispatcher = conf.D.return, _ body: @escaping (T) throws -> Void) -> Promise { return map(on: on) { try body($0) return $0 @@ -264,7 +264,7 @@ public extension Thenable where T: Sequence { // $0 => [2,4,6] } */ - func mapValues(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> { + func mapValues(on: Dispatcher = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U]> { return map(on: on) { try $0.map(transform) } } @@ -279,7 +279,7 @@ public extension Thenable where T: Sequence { // $0 => [1,1,2,2,3,3] } */ - func flatMapValues(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> { + func flatMapValues(on: Dispatcher = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.Iterator.Element]> { return map(on: on){ (foo: T) in try foo.flatMap{ try transform($0) } } @@ -296,7 +296,7 @@ public extension Thenable where T: Sequence { // $0 => [1,2,3] } */ - func compactMapValues(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U?) -> Promise<[U]> { + func compactMapValues(on: Dispatcher = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U?) -> Promise<[U]> { return map(on: on) { foo -> [U] in #if !swift(>=3.3) || (swift(>=4) && !swift(>=4.1)) return try foo.flatMap(transform) @@ -317,7 +317,7 @@ public extension Thenable where T: Sequence { // $0 => [2,4,6] } */ - func thenMap(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T]> { + func thenMap(on: Dispatcher = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T]> { return then(on: on) { when(fulfilled: try $0.map(transform)) } @@ -334,7 +334,7 @@ public extension Thenable where T: Sequence { // $0 => [1,1,2,2,3,3] } */ - func thenFlatMap(on: Dispatcher? = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T.Iterator.Element]> where U.T: Sequence { + func thenFlatMap(on: Dispatcher = conf.D.map, _ transform: @escaping(T.Iterator.Element) throws -> U) -> Promise<[U.T.Iterator.Element]> where U.T: Sequence { return then(on: on) { when(fulfilled: try $0.map(transform)) }.map(on: nil) { @@ -353,7 +353,7 @@ public extension Thenable where T: Sequence { // $0 => [2,3] } */ - func filterValues(on: Dispatcher? = conf.D.map, _ isIncluded: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> { + func filterValues(on: Dispatcher = conf.D.map, _ isIncluded: @escaping (T.Iterator.Element) -> Bool) -> Promise<[T.Iterator.Element]> { return map(on: on) { $0.filter(isIncluded) } @@ -372,7 +372,7 @@ public extension Thenable where T: Collection { } } - func firstValue(on: Dispatcher? = conf.D.map, where test: @escaping (T.Iterator.Element) -> Bool) -> Promise { + func firstValue(on: Dispatcher = conf.D.map, where test: @escaping (T.Iterator.Element) -> Bool) -> Promise { return map(on: on) { for x in $0 where test(x) { return x @@ -396,7 +396,7 @@ public extension Thenable where T: Collection { public extension Thenable where T: Sequence, T.Iterator.Element: Comparable { /// - Returns: a promise fulfilled with the sorted values of this `Sequence`. - func sortedValues(on: Dispatcher? = conf.D.map) -> Promise<[T.Iterator.Element]> { + func sortedValues(on: Dispatcher = conf.D.map) -> Promise<[T.Iterator.Element]> { return map(on: on){ $0.sorted() } } } From 3b5d422113962e323827127d69fa9246c1e30c0e Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Wed, 20 Jun 2018 14:24:59 -0700 Subject: [PATCH 6/7] Finish tests --- Sources/Guarantee.swift | 2 +- Tests/CorePromise/DispatcherTests.swift | 56 +++++++++++++++++++++---- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/Sources/Guarantee.swift b/Sources/Guarantee.swift index 641b32148..0d7ad89be 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -88,7 +88,7 @@ public extension Guarantee { } @discardableResult - func then(on: Dispatcher = conf.D.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(T) -> Guarantee) -> Guarantee { + func then(on: Dispatcher = conf.D.map, _ body: @escaping(T) -> Guarantee) -> Guarantee { let rg = Guarantee(.pending) pipe { value in on.dispatch { diff --git a/Tests/CorePromise/DispatcherTests.swift b/Tests/CorePromise/DispatcherTests.swift index 64ae85178..6206ef10d 100644 --- a/Tests/CorePromise/DispatcherTests.swift +++ b/Tests/CorePromise/DispatcherTests.swift @@ -1,13 +1,23 @@ 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 async(_ body: @escaping () -> Void) { + func dispatch(_ body: @escaping () -> Void) { dispatchCount += 1 - DispatchQueue.global(qos: .background).async(execute: body) + queue.async(execute: body) } } @@ -60,22 +70,50 @@ class DispatcherTests: XCTestCase { XCTAssertEqual(self.dispatcher.dispatchCount, 2) } - func testDispatchQueueBackwardCompatibility() { + func testDispatchQueueSelection() { + let ex = expectation(description: "DispatchQueue compatibility") + let oldConf = PromiseKit.conf.D PromiseKit.conf.D = (map: dispatcher, return: dispatcher) + + DispatchQueue.global(qos: .background).setSpecific(key: queueIDKey, value: 100) + DispatchQueue.main.setSpecific(key: queueIDKey, value: 102) + dispatcher.queue.setSpecific(key: queueIDKey, value: 103) + Promise.value(42).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 - }.then(on: .main, flags: []) { - XCTAssertEqual($0, 52) + }.then(on: .main, flags: []) { (x: Int) -> Promise in + XCTAssertEqual(x, 52) + let queueID = DispatchQueue.getSpecific(key: queueIDKey) + XCTAssertNotNil(queueID) + XCTAssertEqual(queueID!, 102) return Promise.value(50) - }.done(on: .global(qos: .userInitiated)) { - XCTAssertEqual($0, 50) + }.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: .global(qos: .background)) { + XCTAssertEqual($0, 70) + let queueID = DispatchQueue.getSpecific(key: queueIDKey) + XCTAssertNotNil(queueID) + XCTAssertEqual(queueID!, 100) ex.fulfill() }.cauterize() + waitForExpectations(timeout: 1) - XCTAssertEqual(self.dispatcher.dispatchCount, 0) PromiseKit.conf.D = oldConf + } func testDispatcherPromiseExtension() { @@ -86,7 +124,7 @@ class DispatcherTests: XCTestCase { XCTAssertEqual($0, 42) XCTAssertEqual(self.dispatcher.dispatchCount, 2) ex.fulfill() - }.cauterize() + }.cauterize() waitForExpectations(timeout: 1) } From 5af97415512445a78ea6651c7380faa340a5c155 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Wed, 20 Jun 2018 15:12:01 -0700 Subject: [PATCH 7/7] Generalize DispatchQueues to Dispatcher-protocol objects --- Sources/Guarantee.swift | 2 +- Sources/Promise.swift | 2 +- Tests/CorePromise/DispatcherTests.swift | 37 +++++++++++++------------ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Sources/Guarantee.swift b/Sources/Guarantee.swift index 0d7ad89be..1e9539008 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -195,7 +195,7 @@ public extension Dispatcher { - Returns: A new `Guarantee` resolved by the result of the provided closure. - Note: There is no Promise/Thenable version of this due to Swift compiler ambiguity issues. */ - func guarantee(execute body: @escaping () -> T) -> Guarantee { + func dispatch(_: PMKNamespacer, _ body: @escaping () -> T) -> Guarantee { let rg = Guarantee(.pending) dispatch { rg.box.seal(body()) diff --git a/Sources/Promise.swift b/Sources/Promise.swift index 683024d7b..e59501ef3 100644 --- a/Sources/Promise.swift +++ b/Sources/Promise.swift @@ -192,7 +192,7 @@ public extension Dispatcher { - Returns: A new `Promise` resolved by the result of the provided closure. - Note: There is no Promise/Thenable version of this due to Swift compiler ambiguity issues. */ - func promise(execute body: @escaping () throws -> T) -> Promise { + func dispatch(_: PMKNamespacer, _ body: @escaping () throws -> T) -> Promise { let promise = Promise(.pending) dispatch { do { diff --git a/Tests/CorePromise/DispatcherTests.swift b/Tests/CorePromise/DispatcherTests.swift index 6206ef10d..817ba9d3d 100644 --- a/Tests/CorePromise/DispatcherTests.swift +++ b/Tests/CorePromise/DispatcherTests.swift @@ -77,7 +77,8 @@ class DispatcherTests: XCTestCase { let oldConf = PromiseKit.conf.D PromiseKit.conf.D = (map: dispatcher, return: dispatcher) - DispatchQueue.global(qos: .background).setSpecific(key: queueIDKey, value: 100) + 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) @@ -103,7 +104,7 @@ class DispatcherTests: XCTestCase { XCTAssertNotNil(queueID) XCTAssertEqual(queueID!, 103) return x + 10 - }.done(on: .global(qos: .background)) { + }.done(on: background) { XCTAssertEqual($0, 70) let queueID = DispatchQueue.getSpecific(key: queueIDKey) XCTAssertNotNil(queueID) @@ -116,25 +117,27 @@ class DispatcherTests: XCTestCase { } - func testDispatcherPromiseExtension() { + @available(macOS 10.10, iOS 2.0, tvOS 10.0, watchOS 2.0, *) + func testDispatcherExtensionReturnsGuarantee() { let ex = expectation(description: "Dispatcher.promise") - dispatcher.promise { - return 42 - }.done(on: dispatcher) { - XCTAssertEqual($0, 42) - XCTAssertEqual(self.dispatcher.dispatchCount, 2) + dispatcher.dispatch(.promise) { () -> Int in + XCTAssertFalse(Thread.isMainThread) + return 1 + }.done { one in + XCTAssertEqual(one, 1) ex.fulfill() - }.cauterize() + } waitForExpectations(timeout: 1) } - - func testDispatcherGuaranteeExtension() { - let ex = expectation(description: "Dispatcher.guarantee") - dispatcher.guarantee { - return 42 - }.done(on: .main) { - XCTAssertEqual($0, 42) - XCTAssertEqual(self.dispatcher.dispatchCount, 1) + + @available(macOS 10.10, iOS 2.0, tvOS 10.0, watchOS 2.0, *) + func testDispatcherExtensionCanThrowInBody() { + let ex = expectation(description: "Dispatcher.promise") + dispatcher.dispatch(.promise) { () -> Int in + throw PMKError.badInput + }.done { _ in + XCTFail() + }.catch { _ in ex.fulfill() } waitForExpectations(timeout: 1)