Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

'Cancel' for PromiseKit #9

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github "mxcl/PromiseKit" ~> 6.3
#github "mxcl/PromiseKit" ~> 6.3
github "dougzilla32/PromiseKit" "CoreCancel"
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "AliSoftware/OHHTTPStubs" "6.1.0"
github "mxcl/PromiseKit" "6.3.4"
github "dougzilla32/PromiseKit" "087b3cf470890ff9ea841212e2f3e285fecf3988"
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import PackageDescription
let package = Package(
name: "PMKFoundation",
dependencies: [
.Package(url: "https://github.com/mxcl/PromiseKit.git", majorVersion: 6)
// Switch this back before integrating:
// .Package(url: "https://github.com/mxcl/PromiseKit.git", majorVersion: 6)
.Package(url: "https://github.com/dougzilla32/PromiseKitCoreCancel.git", majorVersion: 6)
],
swiftLanguageVersions: [3, 4],
exclude: [
Expand Down
18 changes: 18 additions & 0 deletions Sources/NSNotificationCenter+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,32 @@ import PromiseKit
*/
extension NotificationCenter {
/// Observe the named notification once
/// - Note: cancelling this guarantee will cancel the underlying task
/// - SeeAlso: [Cancellation](http://promisekit.org/docs/)
public func observe(once name: Notification.Name, object: Any? = nil) -> Guarantee<Notification> {
let (promise, fulfill) = Guarantee<Notification>.pending()
#if os(Linux) && ((swift(>=4.0) && !swift(>=4.0.1)) || (swift(>=3.0) && !swift(>=3.2.1)))
let id = addObserver(forName: name, object: object, queue: nil, usingBlock: fulfill)
#else
let id = addObserver(forName: name, object: object, queue: nil, using: fulfill)
#endif
promise.setCancellableTask(ObserverTask { self.removeObserver(id) })
promise.done { _ in self.removeObserver(id) }
return promise
}
}

class ObserverTask: CancellableTask {
let cancelBlock: () -> Void

init(cancelBlock: @escaping () -> Void) {
self.cancelBlock = cancelBlock
}

func cancel() {
cancelBlock()
isCancelled = true
}

var isCancelled = false
}
23 changes: 22 additions & 1 deletion Sources/NSObject+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,27 @@ extension NSObject {
- Returns: A promise that resolves when the provided keyPath changes.
- Warning: *Important* The promise must not outlive the object under observation.
- SeeAlso: Apple’s KVO documentation.
- Note: cancelling this promise will cancel the underlying task
- SeeAlso: [Cancellation](http://promisekit.org/docs/)
*/
public func observe(_: PMKNamespacer, keyPath: String) -> Guarantee<Any?> {
return Guarantee { KVOProxy(observee: self, keyPath: keyPath, resolve: $0) }
}
}

private class KVOProxy: NSObject {
private class KVOProxy: NSObject, CancellableTask {
var retainCycle: KVOProxy?
let fulfill: (Any?) -> Void
let observeeObject: NSObject
let observeeKeyPath: String
var observing: Bool

@discardableResult
init(observee: NSObject, keyPath: String, resolve: @escaping (Any?) -> Void) {
fulfill = resolve
observeeObject = observee
observeeKeyPath = keyPath
observing = true
super.init()
observee.addObserver(self, forKeyPath: keyPath, options: NSKeyValueObservingOptions.new, context: pointer)
retainCycle = self
Expand All @@ -47,10 +55,23 @@ private class KVOProxy: NSObject {
fulfill(change[NSKeyValueChangeKey.newKey])
if let object = object as? NSObject, let keyPath = keyPath {
object.removeObserver(self, forKeyPath: keyPath)
observing = false
}
}
}

func cancel() {
if !isCancelled {
if observing {
observeeObject.removeObserver(self, forKeyPath: observeeKeyPath)
observing = false
}
isCancelled = true
}
}

var isCancelled = false

private lazy var pointer: UnsafeMutableRawPointer = {
return Unmanaged<KVOProxy>.passUnretained(self).toOpaque()
}()
Expand Down
75 changes: 68 additions & 7 deletions Sources/NSURLSession+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,26 +76,68 @@ extension URLSession {
- Returns: A promise that represents the URL request.
- SeeAlso: [OMGHTTPURLRQ]
- Remark: We deliberately don’t provide a `URLRequestConvertible` for `String` because in our experience, you should be explicit with this error path to make good apps.

- Note: cancelling this promise will cancel the underlying task
- SeeAlso: [Cancellation](http://promisekit.org/docs/)

[OMGHTTPURLRQ]: https://github.com/mxcl/OMGHTTPURLRQ
*/
public func dataTask(_: PMKNamespacer, with convertible: URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> {
return Promise { dataTask(with: convertible.pmkRequest, completionHandler: adapter($0)).resume() }
var task: URLSessionTask!
var reject: ((Error) -> Void)!

let promise = Promise<(data: Data, response: URLResponse)> {
reject = $0.reject
task = self.dataTask(with: convertible.pmkRequest, completionHandler: adapter($0))
task.resume()
}

promise.setCancellableTask(task, reject: reject)
return promise
}

/// - Note: cancelling this promise will cancel the underlying task
/// - SeeAlso: [Cancellation](http://promisekit.org/docs/)
public func uploadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, from data: Data) -> Promise<(data: Data, response: URLResponse)> {
return Promise { uploadTask(with: convertible.pmkRequest, from: data, completionHandler: adapter($0)).resume() }
var task: URLSessionTask!
var reject: ((Error) -> Void)!

let promise = Promise<(data: Data, response: URLResponse)> {
reject = $0.reject
task = self.uploadTask(with: convertible.pmkRequest, from: data, completionHandler: adapter($0))
task.resume()
}

promise.setCancellableTask(task, reject: reject)
return promise
}

/// - Note: cancelling this promise will cancel the underlying task
/// - SeeAlso: [Cancellation](http://promisekit.org/docs/)
public func uploadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, fromFile file: URL) -> Promise<(data: Data, response: URLResponse)> {
return Promise { uploadTask(with: convertible.pmkRequest, fromFile: file, completionHandler: adapter($0)).resume() }
var task: URLSessionTask!
var reject: ((Error) -> Void)!

let promise = Promise<(data: Data, response: URLResponse)> {
reject = $0.reject
task = self.uploadTask(with: convertible.pmkRequest, fromFile: file, completionHandler: adapter($0))
task.resume()
}

promise.setCancellableTask(task, reject: reject)
return promise
}

/// - Remark: we force a `to` parameter because Apple deletes the downloaded file immediately after the underyling completion handler returns.
/// - Note: we do not create the destination directory for you, because we move the file with FileManager.moveItem which changes it behavior depending on the directory status of the URL you provide. So create your own directory first!
/// - Note: cancelling this promise will cancel the underlying task
/// - SeeAlso: [Cancellation](http://promisekit.org/docs/)
public func downloadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, to saveLocation: URL) -> Promise<(saveLocation: URL, response: URLResponse)> {
return Promise { seal in
downloadTask(with: convertible.pmkRequest, completionHandler: { tmp, rsp, err in
var task: URLSessionTask!
var reject: ((Error) -> Void)!

let promise = Promise<(saveLocation: URL, response: URLResponse)> { seal in
reject = seal.reject
task = self.downloadTask(with: convertible.pmkRequest, completionHandler: { tmp, rsp, err in
if let error = err {
seal.reject(error)
} else if let rsp = rsp, let tmp = tmp {
Expand All @@ -108,8 +150,12 @@ extension URLSession {
} else {
seal.reject(PMKError.invalidCallingConvention)
}
}).resume()
})
task.resume()
}

promise.setCancellableTask(task, reject: reject)
return promise
}
}

Expand Down Expand Up @@ -237,3 +283,18 @@ public extension Promise where T == (data: Data, response: URLResponse) {
}
}
#endif

extension URLSessionTask: CancellableTask {
/// `true` if the URLSessionTask was successfully cancelled, `false` otherwise
public var isCancelled: Bool {
return state == .canceling || (error as NSError?)?.code == NSURLErrorCancelled
}
}

#if swift(>=3.1)
public extension CancellablePromise where T == (data: Data, response: URLResponse) {
func validate() -> CancellablePromise<T> {
return cancellable(promise.validate())
}
}
#endif
16 changes: 15 additions & 1 deletion Sources/Process+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ extension Process {
}.then { stdout in
print(str)
}
- Note: cancelling this promise will cancel the underlying task
- SeeAlso: [Cancellation](http://promisekit.org/docs/)
*/
public func launch(_: PMKNamespacer) -> Promise<(out: Pipe, err: Pipe)> {
let (stdout, stderr) = (Pipe(), Pipe())
Expand Down Expand Up @@ -67,7 +69,7 @@ extension Process {
}
}

return Promise { seal in
return Promise<(out: Pipe, err: Pipe)>(cancellableTask: self) { seal in
q.async {
self.waitUntilExit()

Expand Down Expand Up @@ -143,4 +145,16 @@ extension Process {
}
}

extension Process: CancellableTask {
/// Sends an interrupt signal to the process
public func cancel() {
interrupt()
}

/// `true` if the Process was successfully interrupted, `false` otherwise
public var isCancelled: Bool {
return !isRunning
}
}

#endif
22 changes: 22 additions & 0 deletions Sources/afterlife.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import PromiseKit
/**
- Returns: A promise that resolves when the provided object deallocates
- Important: The promise is not guarenteed to resolve immediately when the provided object is deallocated. So you cannot write code that depends on exact timing.
- Note: cancelling this guarantee will cancel the underlying task
- SeeAlso: [Cancellation](http://promisekit.org/docs/)
*/
public func after(life object: NSObject) -> Guarantee<Void> {
var reaper = objc_getAssociatedObject(object, &handle) as? GrimReaper
if reaper == nil {
reaper = GrimReaper()
objc_setAssociatedObject(object, &handle, reaper, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
reaper!.promise.setCancellableTask(CancellableReaperTask(object: object))
}
return reaper!.promise
}
Expand All @@ -24,3 +27,22 @@ private class GrimReaper: NSObject {
}
let (promise, fulfill) = Guarantee<Void>.pending()
}

private class CancellableReaperTask: CancellableTask {
weak var object: NSObject?

var isCancelled = false

init(object: NSObject) {
self.object = object
}

func cancel() {
if !isCancelled {
if let obj = object {
objc_setAssociatedObject(obj, &handle, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
isCancelled = true
}
}
}
19 changes: 19 additions & 0 deletions Tests/TestNSNotificationCenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,22 @@ class NSNotificationCenterTests: XCTestCase {
}

private let PMKTestNotification = Notification.Name("PMKTestNotification")

//////////////////////////////////////////////////////////// Cancellation

extension NSNotificationCenterTests {
func testCancel() {
let ex = expectation(description: "")
let userInfo = ["a": 1]

cancellable(NotificationCenter.default.observe(once: PMKTestNotification)).done { value in
XCTFail()
}.catch(policy: .allErrors) {
$0.isCancelled ? ex.fulfill() : XCTFail()
}.cancel()

NotificationCenter.default.post(name: PMKTestNotification, object: nil, userInfo: userInfo)

waitForExpectations(timeout: 1)
}
}
Loading