Skip to content

Commit

Permalink
'Cancel' for PromiseKit -- provides the ability to cancel promises an…
Browse files Browse the repository at this point in the history
…d promise chains
  • Loading branch information
dougzilla32 committed Oct 7, 2018
1 parent e56adc9 commit bcd797f
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 7 deletions.
3 changes: 2 additions & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github "mxcl/PromiseKit" ~> 6.0
#github "mxcl/PromiseKit" ~> 6.0
github "dougzilla32/PromiseKit" "CoreCancel"
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1 +1 @@
github "mxcl/PromiseKit" "6.3.4"
github "dougzilla32/PromiseKit" "087b3cf470890ff9ea841212e2f3e285fecf3988"
12 changes: 11 additions & 1 deletion Sources/HMAcessoryBrowser+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class HMPromiseAccessoryBrowser {
private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDelegate {
let browser = HMAccessoryBrowser()
let scanInterval: ScanInterval
var timer: CancellablePromise<Void>?

init(scanInterval: ScanInterval) {
self.scanInterval = scanInterval
Expand All @@ -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)
Expand All @@ -60,6 +61,7 @@ private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDeleg

override func cancel() {
browser.stopSearchingForNewAccessories()
timer?.cancel()
super.cancel()
}

Expand All @@ -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))
}
}
18 changes: 17 additions & 1 deletion Sources/HMHomeManager+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
9 changes: 7 additions & 2 deletions Sources/Utils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import PromiseKit
/**
Commonly used functionality when promisifying a delegate pattern
*/
internal class PromiseProxy<T>: NSObject {
internal class PromiseProxy<T>: NSObject, CancellableTask {
var isCancelled = false

internal let (promise, seal) = Promise<T>.pending();

private var retainCycle: PromiseProxy?
Expand All @@ -13,7 +15,9 @@ internal class PromiseProxy<T>: 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
Expand All @@ -28,6 +32,7 @@ internal class PromiseProxy<T>: NSObject {

/// Cancel helper
internal func cancel() {
isCancelled = true
self.reject(PMKError.cancelled)
}
}
Expand Down
34 changes: 33 additions & 1 deletion Tests/HMAccessoryBrowserTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
Expand Down Expand Up @@ -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

0 comments on commit bcd797f

Please sign in to comment.