From f8b0eadd302607ba4be97a846ca44f7968e3f28d Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Sun, 17 Jun 2018 23:39:52 -0700 Subject: [PATCH 01/16] 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 bb625f682..aa532b751 100644 --- a/PromiseKit.xcodeproj/project.pbxproj +++ b/PromiseKit.xcodeproj/project.pbxproj @@ -80,6 +80,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 */; }; @@ -223,6 +224,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 = ""; }; @@ -443,6 +445,7 @@ 63B18AEB1F2D205C00B79E37 /* CustomStringConvertible.swift */, 63D9B2F020338D5D0075C00B /* Deprecations.swift */, 085B96BE21A9B37C00E5E22F /* LogEvent.swift */, + BB2524DD20D729A60010F7B0 /* Dispatcher.swift */, ); name = Sources.swift; sourceTree = ""; @@ -750,6 +753,7 @@ 085B96BF21A9B37C00E5E22F /* LogEvent.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 a0a80152b..f17caf5ae 100644 --- a/Sources/Box.swift +++ b/Sources/Box.swift @@ -84,18 +84,14 @@ final 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 1b640ff22..12491108d 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 503451266..b53c810be 100644 --- a/Sources/Configuration.swift +++ b/Sources/Configuration.swift @@ -8,8 +8,14 @@ import Dispatch We would like it to be, but sadly `Swift` does not expose `dispatch_once` et al. which is what we used to use in order to make the configuration immutable once first used. */ 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 597e97ad2..d5482c732 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -66,10 +66,10 @@ public final 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(()) } @@ -84,10 +84,10 @@ public extension Guarantee { } } - 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)) } } @@ -95,10 +95,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) } } @@ -106,7 +106,7 @@ public extension Guarantee { } public func asVoid() -> Guarantee { - return map(on: nil) { _ in } + return map { _ in } } /** @@ -135,7 +135,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]) @@ -145,8 +145,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 @@ -188,6 +188,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 da9f6aa2b..ca5357b88 100644 --- a/Sources/Promise.swift +++ b/Sources/Promise.swift @@ -168,6 +168,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 776237207..c37e4db5b 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 @@ -212,7 +212,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 } } } @@ -286,8 +286,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) } } /** @@ -301,8 +301,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) } } } @@ -318,8 +318,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 @@ -339,8 +339,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)) } } @@ -356,8 +356,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 } @@ -375,8 +375,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) } } @@ -394,8 +394,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 } @@ -418,7 +418,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 4ba57edddcccb183146d813462d145ed4f60b79e Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Mon, 18 Jun 2018 00:43:50 -0700 Subject: [PATCH 02/16] 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 d5482c732..fc7c16da2 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -106,7 +106,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 c37e4db5b..a56f53f19 100644 --- a/Sources/Thenable.swift +++ b/Sources/Thenable.swift @@ -212,7 +212,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 262b4dd5784da097849a1e6b99b6bfd9bdb5b613 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Mon, 18 Jun 2018 01:25:24 -0700 Subject: [PATCH 03/16] 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 f17caf5ae..fc8b3e4e0 100644 --- a/Sources/Box.swift +++ b/Sources/Box.swift @@ -84,14 +84,14 @@ final 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 a56f53f19..b944a3896 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 044fe7841716fd7312f52c1a2ca5878e57d8ee56 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Tue, 19 Jun 2018 18:36:08 -0700 Subject: [PATCH 04/16] 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 aa532b751..3d9101e87 100644 --- a/PromiseKit.xcodeproj/project.pbxproj +++ b/PromiseKit.xcodeproj/project.pbxproj @@ -81,6 +81,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 */; }; @@ -225,6 +226,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 = ""; }; @@ -354,6 +356,7 @@ 635D640D1D59635300BC0AF5 /* ZalgoTests.swift */, 639BF755203DF02C00FA577B /* Utilities.swift */, 085B96B121A6358900E5E22F /* LoggingTests.swift */, + BB4AF7C320D819360008333D /* DispatcherTests.swift */, ); name = Core; path = Tests/CorePromise; @@ -680,6 +683,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 b944a3896..7516d6535 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 0a027c8b0ce18a9efa9cce5d1e6406bcdda36238 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Wed, 20 Jun 2018 12:54:41 -0700 Subject: [PATCH 05/16] 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 fc8b3e4e0..8571999f0 100644 --- a/Sources/Box.swift +++ b/Sources/Box.swift @@ -82,16 +82,3 @@ final 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 12491108d..020298d9c 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 b53c810be..94d0a606b 100644 --- a/Sources/Configuration.swift +++ b/Sources/Configuration.swift @@ -11,11 +11,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 fc7c16da2..fd6ee689a 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -66,10 +66,10 @@ public final 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(()) } @@ -84,10 +84,10 @@ public extension Guarantee { } } - 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)) } } @@ -95,10 +95,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) } } @@ -145,7 +145,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 { @@ -204,7 +204,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 ca5357b88..1daf1192b 100644 --- a/Sources/Promise.swift +++ b/Sources/Promise.swift @@ -184,7 +184,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 7516d6535..0cb935bdf 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 @@ -286,7 +286,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) } } @@ -301,7 +301,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) } } @@ -318,7 +318,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) @@ -339,7 +339,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)) } @@ -356,7 +356,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) { @@ -375,7 +375,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) } @@ -394,7 +394,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 @@ -418,7 +418,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 855476acbe3e77fb7b4feb7109cd7ac957984c0a Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Wed, 20 Jun 2018 14:24:59 -0700 Subject: [PATCH 06/16] 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 fd6ee689a..5f387aee7 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -95,7 +95,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 9e17e7d6ae21bc8d94a71a4d06aea32f29355ee8 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Wed, 20 Jun 2018 15:12:01 -0700 Subject: [PATCH 07/16] 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 5f387aee7..6518e577a 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -202,7 +202,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 1daf1192b..3d4e375be 100644 --- a/Sources/Promise.swift +++ b/Sources/Promise.swift @@ -182,7 +182,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) From 686a1e63a4d5000016029989bb44aae8727ecb74 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Wed, 8 Aug 2018 15:59:54 -0700 Subject: [PATCH 08/16] Port Thenable.tap and Guarantee.get to Dispatcher objects --- Sources/Dispatcher.swift | 24 +++++++++++++++++++++++- Sources/Guarantee.swift | 4 ++-- Sources/Thenable.swift | 6 +++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Sources/Dispatcher.swift b/Sources/Dispatcher.swift index 2d7a244b8..b6407ebf5 100644 --- a/Sources/Dispatcher.swift +++ b/Sources/Dispatcher.swift @@ -85,6 +85,11 @@ public extension Guarantee { return done(on: dispatcher, body) } + func get(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (T) -> Void) -> Guarantee { + let dispatcher = selectDispatcher(given: on, configured: conf.D.return, flags: flags) + return get(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) @@ -213,11 +218,28 @@ public extension Thenable { }.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) } + + /** + The provided closure is executed with promise result. + + This is like `get` but provides the Result of the Promise so you can inspect the value of the chain at this point without causing any side effects. + + - Parameter on: The queue to which the provided closure dispatches. + - Parameter body: The closure that is executed with Result of Promise. + - Returns: A new promise that is resolved with the result that the handler is fed. For example: + + promise.tap{ print($0) }.then{ /*…*/ } + */ + func tap(on: DispatchQueue? = .pmkDefault, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Result) -> Void) -> Promise { + let dispatcher = selectDispatcher(given: on, configured: conf.D.map, flags: flags) + return tap(on: dispatcher, body) + } + } public extension Thenable where T: Sequence { diff --git a/Sources/Guarantee.swift b/Sources/Guarantee.swift index 6518e577a..6b771f652 100644 --- a/Sources/Guarantee.swift +++ b/Sources/Guarantee.swift @@ -77,8 +77,8 @@ public extension Guarantee { return rg } - func get(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (T) -> Void) -> Guarantee { - return map(on: on, flags: flags) { + func get(on: Dispatcher = conf.D.return, _ body: @escaping (T) -> Void) -> Guarantee { + return map(on: on) { body($0) return $0 } diff --git a/Sources/Thenable.swift b/Sources/Thenable.swift index 0cb935bdf..b3a1f340e 100644 --- a/Sources/Thenable.swift +++ b/Sources/Thenable.swift @@ -193,16 +193,16 @@ public extension Thenable { This is like `get` but provides the Result of the Promise so you can inspect the value of the chain at this point without causing any side effects. - - 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 with Result of Promise. - Returns: A new promise that is resolved with the result that the handler is fed. For example: promise.tap{ print($0) }.then{ /*…*/ } */ - func tap(on: DispatchQueue? = conf.Q.map, flags: DispatchWorkItemFlags? = nil, _ body: @escaping(Result) -> Void) -> Promise { + func tap(on: Dispatcher = conf.D.map, _ body: @escaping(Result) -> Void) -> Promise { return Promise { seal in pipe { result in - on.async(flags: flags) { + on.dispatch { body(result) seal.resolve(result) } From 83345a4d10509e2fc0b037d6f2a7e2a30e0327d4 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Fri, 14 Dec 2018 21:45:43 -0800 Subject: [PATCH 09/16] Add explicit import for Dispatch in tests (needed for Travis builds) --- Tests/CorePromise/DispatcherTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/CorePromise/DispatcherTests.swift b/Tests/CorePromise/DispatcherTests.swift index 817ba9d3d..55c44141d 100644 --- a/Tests/CorePromise/DispatcherTests.swift +++ b/Tests/CorePromise/DispatcherTests.swift @@ -1,3 +1,4 @@ +import Dispatch import PromiseKit import XCTest From f78eaf14102f9d415ede473dac8f955a8d77f6e9 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Sun, 16 Dec 2018 14:53:35 -0800 Subject: [PATCH 10/16] Travis-build all branches in Travis TRAVIS_BRANCHES env variable --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1c2d57925..24cd001ba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,10 @@ stages: - name: lint - if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ + if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ OR branch =~ concat("^(",env(TRAVIS_BRANCHES),")$") - name: compile - if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ + if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ OR branch =~ concat("^(",env(TRAVIS_BRANCHES),")$") - name: test - if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ + if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ OR branch =~ concat("^(",env(TRAVIS_BRANCHES),")$") - name: deploy if: branch =~ ^\d+\.\d+\.\d+$ jobs: From f2eadfc040ea8767018a3727001b45956ee3fed2 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Sun, 16 Dec 2018 14:55:19 -0800 Subject: [PATCH 11/16] v7 Travis: Swift 3 only on latest compiler, sync Linux test/build --- .travis.yml | 132 ++++++++++++++++++++++++---------------------------- 1 file changed, 61 insertions(+), 71 deletions(-) diff --git a/.travis.yml b/.travis.yml index 24cd001ba..7ac877c03 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,48 +7,22 @@ stages: if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ OR branch =~ concat("^(",env(TRAVIS_BRANCHES),")$") - name: deploy if: branch =~ ^\d+\.\d+\.\d+$ + jobs: include: - - &carthage - stage: compile - osx_image: xcode8.3 - name: Carthage / Xcode 8.3 - os: osx - language: objective-c - script: carthage build --no-skip-current --configuration Release - - <<: *carthage - osx_image: xcode9.2 - name: Carthage / Xcode 9.2 - - <<: *carthage - osx_image: xcode9.4 - name: Carthage / Xcode 9.4 - - <<: *carthage - osx_image: xcode10.1 - name: Carthage / Xcode 10.0 + # Lint stage - &pod stage: lint - osx_image: xcode8.3 - env: SWIFT=3.1 - name: pod lib lint --swift-version=3.1 + osx_image: xcode10.1 + env: SWIFT=3.4 + name: pod lib lint --swift-version=3.4 os: osx cache: cocoapods language: objective-c before_install: mv .github/PromiseKit.podspec . install: gem install cocoapods --prerelease --version 1.6.0.beta.2 script: pod lib lint --subspec=PromiseKit/CorePromise --fail-fast --swift-version=$SWIFT - - <<: *pod - osx_image: xcode9.2 - env: SWIFT=3.2 - name: pod lib lint --swift-version=3.2 - - <<: *pod - osx_image: xcode9.4 - env: SWIFT=3.3 - name: pod lib lint --swift-version=3.3 - - <<: *pod - osx_image: xcode10.1 - env: SWIFT=3.4 - name: pod lib lint --swift-version=3.4 - <<: *pod osx_image: xcode9.2 env: SWIFT=4.0 @@ -62,10 +36,25 @@ jobs: env: SWIFT=4.2 name: pod lib lint --swift-version=4.2 + # Compile stage - Carthage macOS builds + - &carthage + stage: compile + osx_image: xcode10.1 + name: Carthage / Xcode 10.1 (Swift 3.4) + env: SWIFT_VERSION=3.4 + os: osx + language: objective-c + script: carthage build --no-skip-current --configuration Release + - <<: *carthage + osx_image: xcode10.1 + env: SWIFT_VERSION=4.2 + name: Carthage / Xcode 10.1 (Swift 4.2) + + # Compile and test stages - Linux builds - &linux stage: compile - env: SWIFT_BUILD_VERSION=3 SWIFT_VERSION=4.0.3 - name: Linux / Swift 3.2 + env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.2.1 + name: Linux / Swift 4.2 (compile) os: linux dist: trusty sudo: required @@ -74,29 +63,50 @@ jobs: install: swift build -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION script: "true" - <<: *linux - env: SWIFT_BUILD_VERSION=3 SWIFT_VERSION=4.1.2 - name: Linux / Swift 3.3 + name: Linux / Swift 4.2 (test) + stage: test + script: swift test -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION - <<: *linux - env: SWIFT_BUILD_VERSION=3 SWIFT_VERSION=4.2.1 - name: Linux / Swift 3.4 + name: Linux / Swift 4.1 (compile) + env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.1.2 + - <<: *linux + stage: test + name: Linux / Swift 4.1 (test) + env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.1.2 + script: swift test -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION - <<: *linux + name: Linux / Swift 4.0 (compile) env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.0.3 - name: Linux / Swift 4.0 + - <<: *linux stage: test - script: swift test -Xswiftc -swift-version -Xswiftc 4 + name: Linux / Swift 4.0 (test) + env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.0.3 + script: swift test -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION - <<: *linux - env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.1.2 - name: Linux / Swift 4.1 + name: Linux / Swift 3 on tools 4.2.1 (compile) + env: SWIFT_BUILD_VERSION=3 SWIFT_VERSION=4.2.1 - <<: *linux - env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.2.1 - name: Linux / Swift 4.2 stage: test - script: swift test -Xswiftc -swift-version -Xswiftc 4 + name: Linux / Swift 3 on tools 4.2.1 (test) + env: SWIFT_BUILD_VERSION=3 SWIFT_VERSION=4.2.1 + script: swift test -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION + # Compile stage - SwiftPM builds + - &swiftpm + stage: compile + name: SwiftPM / macOS / Xcode 9.4 + os: osx + osx_image: xcode9.4 + script: swift build + - <<: *swiftpm + osx_image: xcode10.1 + name: SwiftPM / macOS / Xcode 10.1 + + # Test stage - macOS, iOS, and tvOS - &test stage: test - osx_image: xcode8.3 - name: macOS / Xcode 8.3 + osx_image: xcode10.1 + name: macOS / Xcode 10.1 (Swift 3) os: osx language: objective-c env: DST='platform=OS X,arch=x86_64' @@ -110,31 +120,20 @@ jobs: after_success: bash <(curl -s https://codecov.io/bash) - <<: *test - name: macOS / Xcode 9.2 - osx_image: xcode9.2 - script: - xcodebuild -scheme PromiseKit -destination="$DST" -enableCodeCoverage YES SWIFT_VERSION=4 test | xcpretty - - <<: *test - name: macOS / Xcode 9.4 - osx_image: xcode9.4 - script: - xcodebuild -scheme PromiseKit -destination="$DST" -enableCodeCoverage YES SWIFT_VERSION=4 test | xcpretty - - <<: *test - name: macOS / Xcode 10.0 + name: macOS / Xcode 10.1 (Swift 4) osx_image: xcode10.1 script: xcodebuild -scheme PromiseKit -destination="$DST" -enableCodeCoverage YES SWIFT_VERSION=4 test | xcpretty - - <<: *test - name: iOS / Xcode 10.0 + name: iOS / Xcode 10.1 osx_image: xcode10.1 env: DST='OS=12.0,name=iPhone SE' - - <<: *test - name: tvOS / Xcode 10.0 + name: tvOS / Xcode 10.1 env: DST='OS=12.0,name=Apple TV' osx_image: xcode10.1 + # Test stage - Promises/A+ - stage: test name: Promises/A+ (via WebKit JavaScript Bridge) language: objective-c @@ -155,16 +154,7 @@ jobs: - Tests/JS-A+/build - Tests/JS-A+/node_modules - - &swiftpm - stage: compile - name: SwiftPM / macOS / Xcode 9.4 - os: osx - osx_image: xcode9.4 - script: swift build - - <<: *swiftpm - osx_image: xcode10.1 - name: SwiftPM / macOS / Xcode 10.0 - + # Deployment stage - stage: deploy script: | set -eo pipefail From 0b936fd6446792e312bbd46997bba0f925f9de19 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Sun, 16 Dec 2018 14:52:44 -0800 Subject: [PATCH 12/16] Save artifacts (env must define ARTIFACTS_* for KEY, SECRET, REGION, BUCKET) --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7ac877c03..365bf8d0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,12 @@ +addons: + artifacts: + # To collect artifacts, define ARTIFACTS_KEY, ARTIFACTS_SECRET, ARTIFACTS_REGION, and + # ARTIFACTS_BUCKET in the Travis environment. (AWS S3 is currently the only option.) + paths: + # Collect build directory contents and logs if configured to do so in the environment, + # but skip the Promises/A+ products because there are a lot. Also omit some binaries. + - $(find /var/folders -name '*carthage-xcodebuild*' -print 2>/dev/null | tr "\n" ":") + - $(if [ `git ls-files -o | grep build.js | wc -l` -eq 0 ]; then git ls-files -o | grep -v x86_64- | tr "\n" ":" ; fi) stages: - name: lint if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ OR branch =~ concat("^(",env(TRAVIS_BRANCHES),")$") From 0da820ab852e4df40c541d6773705ce0a79b3bb2 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Mon, 17 Dec 2018 20:52:25 -0800 Subject: [PATCH 13/16] Disable artifact collection by default --- .travis.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 365bf8d0e..e6c20cc43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ addons: - artifacts: - # To collect artifacts, define ARTIFACTS_KEY, ARTIFACTS_SECRET, ARTIFACTS_REGION, and - # ARTIFACTS_BUCKET in the Travis environment. (AWS S3 is currently the only option.) + # To enable build artifact collection on S3, change "artifacts_disabled" below to "artifacts". + # Then define ARTIFACTS_KEY, ARTIFACTS_SECRET, ARTIFACTS_REGION, and ARTIFACTS_BUCKET in the + # Travis environment. If you enable artifacts here but do not define the configuration + # parameters, some jobs will fail. + artifacts_disabled: paths: # Collect build directory contents and logs if configured to do so in the environment, - # but skip the Promises/A+ products because there are a lot. Also omit some binaries. + # but skip the Promises/A+ products because there are a lot. Also omit some larger binaries. - $(find /var/folders -name '*carthage-xcodebuild*' -print 2>/dev/null | tr "\n" ":") - $(if [ `git ls-files -o | grep build.js | wc -l` -eq 0 ]; then git ls-files -o | grep -v x86_64- | tr "\n" ":" ; fi) stages: From c47cf7b5f9075eb1b7adb70555101a22ada69521 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Sat, 5 Jan 2019 16:01:02 -0800 Subject: [PATCH 14/16] Reorganize .travis.yml for better diffability --- .travis.yml | 80 +++++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/.travis.yml b/.travis.yml index e6c20cc43..16b9a9604 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ addons: # but skip the Promises/A+ products because there are a lot. Also omit some larger binaries. - $(find /var/folders -name '*carthage-xcodebuild*' -print 2>/dev/null | tr "\n" ":") - $(if [ `git ls-files -o | grep build.js | wc -l` -eq 0 ]; then git ls-files -o | grep -v x86_64- | tr "\n" ":" ; fi) + stages: - name: lint if: branch = master OR branch =~ ^\d+\.\d+\.\d+$ OR branch =~ concat("^(",env(TRAVIS_BRANCHES),")$") @@ -22,6 +23,20 @@ stages: jobs: include: + # Compile stage - Carthage macOS builds + - &carthage + stage: compile + osx_image: xcode10.1 + name: Carthage / Xcode 10.1 (Swift 3.4) + env: SWIFT_VERSION=3.4 + os: osx + language: objective-c + script: carthage build --no-skip-current --configuration Release + - <<: *carthage + osx_image: xcode10.1 + env: SWIFT_VERSION=4.2 + name: Carthage / Xcode 10.1 (Swift 4.2) + # Lint stage - &pod stage: lint @@ -47,25 +62,11 @@ jobs: env: SWIFT=4.2 name: pod lib lint --swift-version=4.2 - # Compile stage - Carthage macOS builds - - &carthage - stage: compile - osx_image: xcode10.1 - name: Carthage / Xcode 10.1 (Swift 3.4) - env: SWIFT_VERSION=3.4 - os: osx - language: objective-c - script: carthage build --no-skip-current --configuration Release - - <<: *carthage - osx_image: xcode10.1 - env: SWIFT_VERSION=4.2 - name: Carthage / Xcode 10.1 (Swift 4.2) - # Compile and test stages - Linux builds - &linux stage: compile - env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.2.1 - name: Linux / Swift 4.2 (compile) + env: SWIFT_BUILD_VERSION=3 SWIFT_VERSION=4.2.1 + name: Linux / Swift 3 on tools 4.2.1 (compile) os: linux dist: trusty sudo: required @@ -74,44 +75,34 @@ jobs: install: swift build -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION script: "true" - <<: *linux - name: Linux / Swift 4.2 (test) + env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.0.3 + name: Linux / Swift 4.0 (test) stage: test - script: swift test -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION + script: swift test -Xswiftc -swift-version -Xswiftc 4 - <<: *linux - name: Linux / Swift 4.1 (compile) env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.1.2 + name: Linux / Swift 4.1 (compile) + - <<: *linux + env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.2.1 + name: Linux / Swift 4.2 (test) + stage: test + script: swift test -Xswiftc -swift-version -Xswiftc 4 - <<: *linux stage: test name: Linux / Swift 4.1 (test) env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.1.2 - script: swift test -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION + script: swift test -Xswiftc -swift-version -Xswiftc 4 - <<: *linux name: Linux / Swift 4.0 (compile) env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.0.3 - <<: *linux - stage: test - name: Linux / Swift 4.0 (test) - env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.0.3 - script: swift test -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION - - <<: *linux - name: Linux / Swift 3 on tools 4.2.1 (compile) - env: SWIFT_BUILD_VERSION=3 SWIFT_VERSION=4.2.1 + env: SWIFT_BUILD_VERSION=4 SWIFT_VERSION=4.2.1 + name: Linux / Swift 4.2 (compile) - <<: *linux stage: test name: Linux / Swift 3 on tools 4.2.1 (test) env: SWIFT_BUILD_VERSION=3 SWIFT_VERSION=4.2.1 - script: swift test -Xswiftc -swift-version -Xswiftc $SWIFT_BUILD_VERSION - - # Compile stage - SwiftPM builds - - &swiftpm - stage: compile - name: SwiftPM / macOS / Xcode 9.4 - os: osx - osx_image: xcode9.4 - script: swift build - - <<: *swiftpm - osx_image: xcode10.1 - name: SwiftPM / macOS / Xcode 10.1 + script: swift test -Xswiftc -swift-version -Xswiftc 3 # Test stage - macOS, iOS, and tvOS - &test @@ -165,6 +156,17 @@ jobs: - Tests/JS-A+/build - Tests/JS-A+/node_modules + # Compile stage - SwiftPM builds + - &swiftpm + stage: compile + name: SwiftPM / macOS / Xcode 9.4 + os: osx + osx_image: xcode9.4 + script: swift build + - <<: *swiftpm + osx_image: xcode10.1 + name: SwiftPM / macOS / Xcode 10.1 + # Deployment stage - stage: deploy script: | From e18c78c9d2dfae08a66502e62c685f75abce1919 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Sun, 3 Feb 2019 21:05:58 -0800 Subject: [PATCH 15/16] rm redundant public modifier --- Sources/Dispatcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Dispatcher.swift b/Sources/Dispatcher.swift index b6407ebf5..e2e0e0a18 100644 --- a/Sources/Dispatcher.swift +++ b/Sources/Dispatcher.swift @@ -460,7 +460,7 @@ public extension CatchMixin { 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) { + 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) } From 5c52dcdbfced015b691d68699e70a3020f560830 Mon Sep 17 00:00:00 2001 From: Garth Snyder Date: Sun, 3 Feb 2019 21:26:04 -0800 Subject: [PATCH 16/16] Add TODO note to move log messages into LogError enum if they're kept --- Sources/Dispatcher.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Dispatcher.swift b/Sources/Dispatcher.swift index e2e0e0a18..abfe72e25 100644 --- a/Sources/Dispatcher.swift +++ b/Sources/Dispatcher.swift @@ -57,6 +57,7 @@ public extension DispatchQueue { /// 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? +/// TODO: Move log prints into LogError enum if they are kept fileprivate func selectDispatcher(given: DispatchQueue?, configured: Dispatcher, flags: DispatchWorkItemFlags?) -> Dispatcher { guard let given = given else {