From bcd797f848d59a6911893c59de520f4dce48e8c7 Mon Sep 17 00:00:00 2001 From: dougzilla32 Date: Sun, 7 Oct 2018 15:14:56 +0900 Subject: [PATCH 1/3] 'Cancel' for PromiseKit -- provides the ability to cancel promises and promise chains --- Cartfile | 3 ++- Cartfile.resolved | 2 +- Sources/HMAcessoryBrowser+Promise.swift | 12 ++++++++- Sources/HMHomeManager+Promise.swift | 18 ++++++++++++- Sources/Utils.swift | 9 +++++-- Tests/HMAccessoryBrowserTests.swift | 34 ++++++++++++++++++++++++- 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/Cartfile b/Cartfile index 2bfea98..c517d21 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1,2 @@ -github "mxcl/PromiseKit" ~> 6.0 +#github "mxcl/PromiseKit" ~> 6.0 +github "dougzilla32/PromiseKit" "CoreCancel" diff --git a/Cartfile.resolved b/Cartfile.resolved index 8d4fefc..80a4000 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1 +1 @@ -github "mxcl/PromiseKit" "6.3.4" +github "dougzilla32/PromiseKit" "087b3cf470890ff9ea841212e2f3e285fecf3988" diff --git a/Sources/HMAcessoryBrowser+Promise.swift b/Sources/HMAcessoryBrowser+Promise.swift index 06dec36..816c1dd 100644 --- a/Sources/HMAcessoryBrowser+Promise.swift +++ b/Sources/HMAcessoryBrowser+Promise.swift @@ -24,6 +24,7 @@ public class HMPromiseAccessoryBrowser { private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDelegate { let browser = HMAccessoryBrowser() let scanInterval: ScanInterval + var timer: CancellablePromise? init(scanInterval: ScanInterval) { self.scanInterval = scanInterval @@ -40,7 +41,7 @@ private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDeleg } if let timeout = timeout { - after(seconds: timeout) + self.timer = cancellable(after(seconds: timeout)) .done { [weak self] () -> Void in guard let _self = self else { return } _self.reject(HMPromiseAccessoryBrowserError.noAccessoryFound) @@ -60,6 +61,7 @@ private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDeleg override func cancel() { browser.stopSearchingForNewAccessories() + timer?.cancel() super.cancel() } @@ -74,3 +76,11 @@ private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDeleg } #endif + +//////////////////////////////////////////////////////////// Cancellable wrapper + +extension HMPromiseAccessoryBrowser { + public func cancellableStart(scanInterval: ScanInterval) -> CancellablePromise<[HMAccessory]> { + return cancellable(start(scanInterval: scanInterval)) + } +} diff --git a/Sources/HMHomeManager+Promise.swift b/Sources/HMHomeManager+Promise.swift index a1d67fa..0318832 100644 --- a/Sources/HMHomeManager+Promise.swift +++ b/Sources/HMHomeManager+Promise.swift @@ -43,18 +43,34 @@ extension HMHomeManager { internal class HMHomeManagerProxy: PromiseProxy<[HMHome]>, HMHomeManagerDelegate { fileprivate let manager: HMHomeManager + private var task: DispatchWorkItem! override init() { self.manager = HMHomeManager() super.init() self.manager.delegate = self - DispatchQueue.main.asyncAfter(deadline: .now() + 20.0) { [weak self] in + self.task = DispatchWorkItem { [weak self] in self?.reject(HomeKitError.permissionDeined) } + DispatchQueue.main.asyncAfter(deadline: .now() + 20.0, execute: self.task) } func homeManagerDidUpdateHomes(_ manager: HMHomeManager) { fulfill(manager.homes) } + + override func cancel() { + self.task.cancel() + super.cancel() + } +} + +//////////////////////////////////////////////////////////// Cancellable wrapper + +@available(iOS 8.0, tvOS 10.0, *) +extension HMHomeManager { + public func cancellableHomes() -> CancellablePromise<[HMHome]> { + return cancellable(homes()) + } } diff --git a/Sources/Utils.swift b/Sources/Utils.swift index f114789..cffba90 100644 --- a/Sources/Utils.swift +++ b/Sources/Utils.swift @@ -3,7 +3,9 @@ import PromiseKit /** Commonly used functionality when promisifying a delegate pattern */ -internal class PromiseProxy: NSObject { +internal class PromiseProxy: NSObject, CancellableTask { + var isCancelled = false + internal let (promise, seal) = Promise.pending(); private var retainCycle: PromiseProxy? @@ -13,7 +15,9 @@ internal class PromiseProxy: NSObject { // Create a retain cycle self.retainCycle = self // And ensure we break it when the promise is resolved - _ = promise.ensure { self.retainCycle = nil } + _ = promise.ensure { self.retainCycle = nil ; self.promise.setCancellableTask(nil) } + + promise.setCancellableTask(self) } /// These functions ensure we only resolve the promise once @@ -28,6 +32,7 @@ internal class PromiseProxy: NSObject { /// Cancel helper internal func cancel() { + isCancelled = true self.reject(PMKError.cancelled) } } diff --git a/Tests/HMAccessoryBrowserTests.swift b/Tests/HMAccessoryBrowserTests.swift index b6f555f..5318fb2 100644 --- a/Tests/HMAccessoryBrowserTests.swift +++ b/Tests/HMAccessoryBrowserTests.swift @@ -46,7 +46,7 @@ extension HMAccessoryBrowser { @objc func pmk_startSearchingForNewAccessories() { after(.milliseconds(100)) .done { swag in - self.delegate!.accessoryBrowser?(self, didFindNewAccessory: MockAccessory()) + self.delegate?.accessoryBrowser?(self, didFindNewAccessory: MockAccessory()) } } } @@ -81,4 +81,36 @@ func swizzle(_ foo: AnyClass, _ from: Selector, isClassMethod: Bool = false, bod method_exchangeImplementations(swizzledMethod, originalMethod) } +//////////////////////////////////////////////////////////// Cancellation + +extension HMAccessoryBrowserTests { + + func testCancelBrowserScanReturningFirst() { + swizzle(HMAccessoryBrowser.self, #selector(HMAccessoryBrowser.startSearchingForNewAccessories)) { + let ex = expectation(description: "") + + cancellable(HMPromiseAccessoryBrowser().start(scanInterval: .returnFirst(timeout: 0.5))) + .done { accessories in + XCTAssertEqual(accessories.count, 1) + XCTFail() + }.catch(policy: .allErrors) { + $0.isCancelled ? ex.fulfill() : XCTFail() + }.cancel() + + waitForExpectations(timeout: 1, handler: nil) + } + } + + func testCancelBrowserScanReturningTimeout() { + let ex = expectation(description: "") + + cancellable(HMPromiseAccessoryBrowser().start(scanInterval: .returnFirst(timeout: 0.5))) + .catch(policy: .allErrors) { + $0.isCancelled ? ex.fulfill() : XCTFail() + }.cancel() + + waitForExpectations(timeout: 1, handler: nil) + } +} + #endif From 16d415429b0f0a46235f4f1ecd3bb451fca82148 Mon Sep 17 00:00:00 2001 From: dougzilla32 Date: Tue, 9 Oct 2018 16:03:36 +0900 Subject: [PATCH 2/3] 'Cancel' for PromiseKit -- fix HomeKit CI build (was missing the "#ifdef !os(tvOS) && !os(watchOS)") --- Sources/HMAcessoryBrowser+Promise.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/HMAcessoryBrowser+Promise.swift b/Sources/HMAcessoryBrowser+Promise.swift index 816c1dd..fe33df4 100644 --- a/Sources/HMAcessoryBrowser+Promise.swift +++ b/Sources/HMAcessoryBrowser+Promise.swift @@ -75,8 +75,6 @@ private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDeleg } } -#endif - //////////////////////////////////////////////////////////// Cancellable wrapper extension HMPromiseAccessoryBrowser { @@ -84,3 +82,5 @@ extension HMPromiseAccessoryBrowser { return cancellable(start(scanInterval: scanInterval)) } } + +#endif From 4b5a7fbb25bd7f93871b347ac164891e029b6e2f Mon Sep 17 00:00:00 2001 From: dougzilla32 Date: Tue, 16 Oct 2018 11:53:03 +0900 Subject: [PATCH 3/3] 'Cancel' for PromiseKit -- remove cancellable wrappers (they are unnecessary) --- Sources/HMAcessoryBrowser+Promise.swift | 10 ++-------- Sources/HMHomeManager+Promise.swift | 11 ++--------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/Sources/HMAcessoryBrowser+Promise.swift b/Sources/HMAcessoryBrowser+Promise.swift index fe33df4..207e903 100644 --- a/Sources/HMAcessoryBrowser+Promise.swift +++ b/Sources/HMAcessoryBrowser+Promise.swift @@ -11,6 +11,8 @@ public enum HMPromiseAccessoryBrowserError: Error { public class HMPromiseAccessoryBrowser { private var proxy: BrowserProxy? + /// - Note: cancelling this promise will cancel the underlying task + /// - SeeAlso: [Cancellation](http://promisekit.org/docs/) public func start(scanInterval: ScanInterval) -> Promise<[HMAccessory]> { proxy = BrowserProxy(scanInterval: scanInterval) return proxy!.promise @@ -75,12 +77,4 @@ private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDeleg } } -//////////////////////////////////////////////////////////// Cancellable wrapper - -extension HMPromiseAccessoryBrowser { - public func cancellableStart(scanInterval: ScanInterval) -> CancellablePromise<[HMAccessory]> { - return cancellable(start(scanInterval: scanInterval)) - } -} - #endif diff --git a/Sources/HMHomeManager+Promise.swift b/Sources/HMHomeManager+Promise.swift index 0318832..fff4cbb 100644 --- a/Sources/HMHomeManager+Promise.swift +++ b/Sources/HMHomeManager+Promise.swift @@ -9,6 +9,8 @@ public enum HomeKitError: Error { @available(iOS 8.0, tvOS 10.0, *) extension HMHomeManager { + /// - Note: cancelling this promise will cancel the underlying task + /// - SeeAlso: [Cancellation](http://promisekit.org/docs/) public func homes() -> Promise<[HMHome]> { return HMHomeManagerProxy().promise } @@ -65,12 +67,3 @@ internal class HMHomeManagerProxy: PromiseProxy<[HMHome]>, HMHomeManagerDelegate super.cancel() } } - -//////////////////////////////////////////////////////////// Cancellable wrapper - -@available(iOS 8.0, tvOS 10.0, *) -extension HMHomeManager { - public func cancellableHomes() -> CancellablePromise<[HMHome]> { - return cancellable(homes()) - } -}