diff --git a/.travis.yml b/.travis.yml index 1c2d57925..e6c20cc43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,54 +1,39 @@ +addons: + # 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 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+$ + 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: 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 +47,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 +74,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 +131,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 +165,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 diff --git a/PromiseKit.xcodeproj/project.pbxproj b/PromiseKit.xcodeproj/project.pbxproj index bb625f682..3d9101e87 100644 --- a/PromiseKit.xcodeproj/project.pbxproj +++ b/PromiseKit.xcodeproj/project.pbxproj @@ -80,6 +80,8 @@ 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 */; }; + BB4AF7C520D820700008333D /* DispatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB4AF7C320D819360008333D /* DispatcherTests.swift */; }; C013F7382048E3B6006B57B1 /* MockNodeEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F7372048E3B6006B57B1 /* MockNodeEnvironment.swift */; }; C013F73A2049076A006B57B1 /* JSPromise.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F7392049076A006B57B1 /* JSPromise.swift */; }; C013F73C20494291006B57B1 /* JSAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C013F73B20494291006B57B1 /* JSAdapter.swift */; }; @@ -223,6 +225,8 @@ 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 = ""; }; + BB4AF7C320D819360008333D /* DispatcherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatcherTests.swift; sourceTree = ""; }; C013F7372048E3B6006B57B1 /* MockNodeEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MockNodeEnvironment.swift; path = "Tests/JS-A+/MockNodeEnvironment.swift"; sourceTree = ""; }; C013F7392049076A006B57B1 /* JSPromise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = JSPromise.swift; path = "Tests/JS-A+/JSPromise.swift"; sourceTree = ""; }; C013F73B20494291006B57B1 /* JSAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = JSAdapter.swift; path = "Tests/JS-A+/JSAdapter.swift"; sourceTree = ""; }; @@ -352,6 +356,7 @@ 635D640D1D59635300BC0AF5 /* ZalgoTests.swift */, 639BF755203DF02C00FA577B /* Utilities.swift */, 085B96B121A6358900E5E22F /* LoggingTests.swift */, + BB4AF7C320D819360008333D /* DispatcherTests.swift */, ); name = Core; path = Tests/CorePromise; @@ -443,6 +448,7 @@ 63B18AEB1F2D205C00B79E37 /* CustomStringConvertible.swift */, 63D9B2F020338D5D0075C00B /* Deprecations.swift */, 085B96BE21A9B37C00E5E22F /* LogEvent.swift */, + BB2524DD20D729A60010F7B0 /* Dispatcher.swift */, ); name = Sources.swift; sourceTree = ""; @@ -677,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 */, @@ -750,6 +757,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..8571999f0 100644 --- a/Sources/Box.swift +++ b/Sources/Box.swift @@ -82,20 +82,3 @@ final class EmptyBox: Box { } } } - - -extension Optional where Wrapped: DispatchQueue { - @inline(__always) - func async(flags: DispatchWorkItemFlags?, _ 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) - } - } - } -} diff --git a/Sources/Catchable.swift b/Sources/Catchable.swift index 1b640ff22..020298d9c 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.dispatch { 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.dispatch { 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.dispatch { 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.dispatch { 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.dispatch { 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, _ 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.dispatch { 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.dispatch { do { rg.box.seal(.fulfilled(try body(error))) } catch { diff --git a/Sources/Configuration.swift b/Sources/Configuration.swift index 503451266..94d0a606b 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 ?? 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) /// 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..b6407ebf5 --- /dev/null +++ b/Sources/Dispatcher.swift @@ -0,0 +1,499 @@ +import Dispatch + +public protocol Dispatcher { + func dispatch(_ body: @escaping () -> Void) +} + +public class DispatchQueueDispatcher: Dispatcher { + + let queue: DispatchQueue + let flags: DispatchWorkItemFlags + + init(queue: DispatchQueue, flags: DispatchWorkItemFlags) { + self.queue = queue + self.flags = flags + } + + 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 dispatch(_ body: @escaping () -> Void) { + async(execute: body) + } +} + +/// 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") +} + +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 +/// 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 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 { + 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 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) + } + + @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) + } + + /** + 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 { + /** + `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? = .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) + } + + /** + 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..6b771f652 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.dispatch { body(value) rg.box.seal(()) } @@ -77,17 +77,17 @@ 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 } } - 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.dispatch { 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, _ body: @escaping(T) -> Guarantee) -> Guarantee { let rg = Guarantee(.pending) pipe { value in - on.async(flags: flags) { + on.dispatch { body(value).pipe(to: rg.box.seal) } } @@ -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 dispatch(_: PMKNamespacer, _ body: @escaping () -> T) -> Guarantee { + let rg = Guarantee(.pending) + dispatch { + 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..3d4e375be 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 dispatch(_: PMKNamespacer, _ body: @escaping () throws -> T) -> Promise { + let promise = Promise(.pending) + dispatch { + 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..b3a1f340e 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.dispatch { 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.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: 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.dispatch { 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.D.return, _ body: @escaping(T) throws -> Void) -> Promise { let rp = Promise(.pending) pipe { switch $0 { case .fulfilled(let value): - on.async(flags: flags) { + on.dispatch { 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,8 +181,8 @@ public extension Thenable { print(foo, " is Void") } */ - func get(on: DispatchQueue? = conf.Q.return, flags: DispatchWorkItemFlags? = nil, _ body: @escaping (T) throws -> Void) -> Promise { - return map(on: on, flags: flags) { + func get(on: Dispatcher = conf.D.return, _ body: @escaping (T) throws -> Void) -> Promise { + return map(on: on) { try body($0) return $0 } @@ -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) } @@ -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() } } } diff --git a/Tests/CorePromise/DispatcherTests.swift b/Tests/CorePromise/DispatcherTests.swift new file mode 100644 index 000000000..55c44141d --- /dev/null +++ b/Tests/CorePromise/DispatcherTests.swift @@ -0,0 +1,147 @@ +import Dispatch +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 dispatch(_ body: @escaping () -> Void) { + dispatchCount += 1 + queue.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 testDispatchQueueSelection() { + + let ex = expectation(description: "DispatchQueue compatibility") + + let oldConf = PromiseKit.conf.D + PromiseKit.conf.D = (map: dispatcher, return: dispatcher) + + 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) + + 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: []) { (x: Int) -> Promise in + XCTAssertEqual(x, 52) + let queueID = DispatchQueue.getSpecific(key: queueIDKey) + XCTAssertNotNil(queueID) + XCTAssertEqual(queueID!, 102) + return Promise.value(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: background) { + XCTAssertEqual($0, 70) + let queueID = DispatchQueue.getSpecific(key: queueIDKey) + XCTAssertNotNil(queueID) + XCTAssertEqual(queueID!, 100) + ex.fulfill() + }.cauterize() + + waitForExpectations(timeout: 1) + PromiseKit.conf.D = oldConf + + } + + @available(macOS 10.10, iOS 2.0, tvOS 10.0, watchOS 2.0, *) + func testDispatcherExtensionReturnsGuarantee() { + let ex = expectation(description: "Dispatcher.promise") + dispatcher.dispatch(.promise) { () -> Int in + XCTAssertFalse(Thread.isMainThread) + return 1 + }.done { one in + XCTAssertEqual(one, 1) + ex.fulfill() + } + waitForExpectations(timeout: 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) + } + +}