Skip to content

Commit 194f051

Browse files
committed
'Cancel' for PromiseKit --
* Eliminate duplicate code in the cancellable extensions * When using the 'cancellable' function with the extensions, ensure that the underlying task (if any) is always cancelled * When using the 'cancellable' function, call 'reject' on the underlying promise (wherever possible) if 'cancel' is invoked * Create cancellable wrappers for the extensions only for the cases where there is an underlying task that supports cancellation
1 parent 6c1b4e7 commit 194f051

File tree

5 files changed

+97
-180
lines changed

5 files changed

+97
-180
lines changed

Sources/NSNotificationCenter+Promise.swift

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,30 +27,12 @@ extension NotificationCenter {
2727
#else
2828
let id = addObserver(forName: name, object: object, queue: nil, using: fulfill)
2929
#endif
30+
promise.setCancellableTask(ObserverTask { self.removeObserver(id) })
3031
promise.done { _ in self.removeObserver(id) }
3132
return promise
3233
}
3334
}
3435

35-
//////////////////////////////////////////////////////////// Cancellation
36-
37-
extension NotificationCenter {
38-
/// Observe the named notification once
39-
public func cancellableObserve(once name: Notification.Name, object: Any? = nil) -> CancellablePromise<Notification> {
40-
let (promise, resolver) = CancellablePromise<Notification>.pending()
41-
#if os(Linux) && ((swift(>=4.0) && !swift(>=4.0.1)) || (swift(>=3.0) && !swift(>=3.2.1)))
42-
let id = addObserver(forName: name, object: object, queue: nil, usingBlock: resolver.fulfill)
43-
#else
44-
let id = addObserver(forName: name, object: object, queue: nil, using: resolver.fulfill)
45-
#endif
46-
47-
promise.appendCancellableTask(task: ObserverTask { self.removeObserver(id) }, reject: resolver.reject)
48-
49-
_ = promise.ensure { self.removeObserver(id) }
50-
return promise
51-
}
52-
}
53-
5436
class ObserverTask: CancellableTask {
5537
let cancelBlock: () -> Void
5638

@@ -65,3 +47,12 @@ class ObserverTask: CancellableTask {
6547

6648
var isCancelled = false
6749
}
50+
51+
//////////////////////////////////////////////////////////// Cancellable wrapper
52+
53+
extension NotificationCenter {
54+
/// Observe the named notification once
55+
public func cancellableObserve(once name: Notification.Name, object: Any? = nil) -> CancellablePromise<Notification> {
56+
return cancellable(observe(once: name, object: object))
57+
}
58+
}

Sources/NSObject+Promise.swift

Lines changed: 20 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -29,62 +29,13 @@ extension NSObject {
2929
}
3030
}
3131

32-
private class KVOProxy: NSObject {
32+
private class KVOProxy: NSObject, CancellableTask {
3333
var retainCycle: KVOProxy?
3434
let fulfill: (Any?) -> Void
35-
36-
@discardableResult
37-
init(observee: NSObject, keyPath: String, resolve: @escaping (Any?) -> Void) {
38-
fulfill = resolve
39-
super.init()
40-
observee.addObserver(self, forKeyPath: keyPath, options: NSKeyValueObservingOptions.new, context: pointer)
41-
retainCycle = self
42-
}
43-
44-
fileprivate override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
45-
if let change = change, context == pointer {
46-
defer { retainCycle = nil }
47-
fulfill(change[NSKeyValueChangeKey.newKey])
48-
if let object = object as? NSObject, let keyPath = keyPath {
49-
object.removeObserver(self, forKeyPath: keyPath)
50-
}
51-
}
52-
}
53-
54-
private lazy var pointer: UnsafeMutableRawPointer = {
55-
return Unmanaged<KVOProxy>.passUnretained(self).toOpaque()
56-
}()
57-
}
58-
59-
//////////////////////////////////////////////////////////// Cancellation
60-
61-
extension NSObject {
62-
/**
63-
- Returns: A promise that resolves when the provided keyPath changes, or when the promise is cancelled.
64-
- Warning: *Important* The promise must not outlive the object under observation.
65-
- SeeAlso: Apple’s KVO documentation.
66-
*/
67-
public func cancellableObserve(_: PMKNamespacer, keyPath: String) -> CancellablePromise<Any?> {
68-
var task: CancellableTask!
69-
var reject: ((Error) -> Void)!
70-
71-
let promise = CancellablePromise<Any?> { seal in
72-
reject = seal.reject
73-
task = CancellableKVOProxy(observee: self, keyPath: keyPath, resolve: seal.fulfill)
74-
}
75-
76-
promise.appendCancellableTask(task: task, reject: reject)
77-
return promise
78-
}
79-
}
80-
81-
private class CancellableKVOProxy: NSObject, CancellableTask {
82-
var retainCycle: CancellableKVOProxy?
83-
let fulfill: (Any?) -> Void
8435
let observeeObject: NSObject
8536
let observeeKeyPath: String
8637
var observing: Bool
87-
38+
8839
@discardableResult
8940
init(observee: NSObject, keyPath: String, resolve: @escaping (Any?) -> Void) {
9041
fulfill = resolve
@@ -95,18 +46,18 @@ private class CancellableKVOProxy: NSObject, CancellableTask {
9546
observee.addObserver(self, forKeyPath: keyPath, options: NSKeyValueObservingOptions.new, context: pointer)
9647
retainCycle = self
9748
}
98-
99-
fileprivate override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
49+
50+
fileprivate override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
10051
if let change = change, context == pointer {
10152
defer { retainCycle = nil }
10253
fulfill(change[NSKeyValueChangeKey.newKey])
103-
if let object = object as? NSObject, let keyPath = keyPath, observing {
54+
if let object = object as? NSObject, let keyPath = keyPath {
10455
object.removeObserver(self, forKeyPath: keyPath)
10556
observing = false
10657
}
10758
}
10859
}
109-
60+
11061
func cancel() {
11162
if !isCancelled {
11263
if observing {
@@ -120,6 +71,19 @@ private class CancellableKVOProxy: NSObject, CancellableTask {
12071
var isCancelled = false
12172

12273
private lazy var pointer: UnsafeMutableRawPointer = {
123-
return Unmanaged<CancellableKVOProxy>.passUnretained(self).toOpaque()
74+
return Unmanaged<KVOProxy>.passUnretained(self).toOpaque()
12475
}()
12576
}
77+
78+
//////////////////////////////////////////////////////////// Cancellable wrapper
79+
80+
extension NSObject {
81+
/**
82+
- Returns: A promise that resolves when the provided keyPath changes, or when the promise is cancelled.
83+
- Warning: *Important* The promise must not outlive the object under observation.
84+
- SeeAlso: Apple’s KVO documentation.
85+
*/
86+
public func cancellableObserve(_: PMKNamespacer, keyPath: String) -> CancellablePromise<Any?> {
87+
return cancellable(observe(.promise, keyPath: keyPath))
88+
}
89+
}

Sources/NSURLSession+Promise.swift

Lines changed: 51 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -80,22 +80,56 @@ extension URLSession {
8080
[OMGHTTPURLRQ]: https://github.com/mxcl/OMGHTTPURLRQ
8181
*/
8282
public func dataTask(_: PMKNamespacer, with convertible: URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> {
83-
return Promise { dataTask(with: convertible.pmkRequest, completionHandler: adapter($0)).resume() }
83+
var task: URLSessionTask!
84+
var reject: ((Error) -> Void)!
85+
86+
let promise = Promise<(data: Data, response: URLResponse)> {
87+
reject = $0.reject
88+
task = self.dataTask(with: convertible.pmkRequest, completionHandler: adapter($0))
89+
task.resume()
90+
}
91+
92+
promise.setCancellableTask(task, reject: reject)
93+
return promise
8494
}
8595

8696
public func uploadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, from data: Data) -> Promise<(data: Data, response: URLResponse)> {
87-
return Promise { uploadTask(with: convertible.pmkRequest, from: data, completionHandler: adapter($0)).resume() }
97+
var task: URLSessionTask!
98+
var reject: ((Error) -> Void)!
99+
100+
let promise = Promise<(data: Data, response: URLResponse)> {
101+
reject = $0.reject
102+
task = self.uploadTask(with: convertible.pmkRequest, from: data, completionHandler: adapter($0))
103+
task.resume()
104+
}
105+
106+
promise.setCancellableTask(task, reject: reject)
107+
return promise
88108
}
89109

90110
public func uploadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, fromFile file: URL) -> Promise<(data: Data, response: URLResponse)> {
91-
return Promise { uploadTask(with: convertible.pmkRequest, fromFile: file, completionHandler: adapter($0)).resume() }
111+
var task: URLSessionTask!
112+
var reject: ((Error) -> Void)!
113+
114+
let promise = Promise<(data: Data, response: URLResponse)> {
115+
reject = $0.reject
116+
task = self.uploadTask(with: convertible.pmkRequest, fromFile: file, completionHandler: adapter($0))
117+
task.resume()
118+
}
119+
120+
promise.setCancellableTask(task, reject: reject)
121+
return promise
92122
}
93123

94124
/// - Remark: we force a `to` parameter because Apple deletes the downloaded file immediately after the underyling completion handler returns.
95125
/// - 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!
96126
public func downloadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, to saveLocation: URL) -> Promise<(saveLocation: URL, response: URLResponse)> {
97-
return Promise { seal in
98-
downloadTask(with: convertible.pmkRequest, completionHandler: { tmp, rsp, err in
127+
var task: URLSessionTask!
128+
var reject: ((Error) -> Void)!
129+
130+
let promise = Promise<(saveLocation: URL, response: URLResponse)> { seal in
131+
reject = seal.reject
132+
task = self.downloadTask(with: convertible.pmkRequest, completionHandler: { tmp, rsp, err in
99133
if let error = err {
100134
seal.reject(error)
101135
} else if let rsp = rsp, let tmp = tmp {
@@ -108,8 +142,12 @@ extension URLSession {
108142
} else {
109143
seal.reject(PMKError.invalidCallingConvention)
110144
}
111-
}).resume()
145+
})
146+
task.resume()
112147
}
148+
149+
promise.setCancellableTask(task, reject: reject)
150+
return promise
113151
}
114152
}
115153

@@ -238,15 +276,15 @@ public extension Promise where T == (data: Data, response: URLResponse) {
238276
}
239277
#endif
240278

241-
//////////////////////////////////////////////////////////// Cancellation
242-
243279
extension URLSessionTask: CancellableTask {
244280
/// `true` if the URLSessionTask was successfully cancelled, `false` otherwise
245281
public var isCancelled: Bool {
246282
return state == .canceling
247283
}
248284
}
249285

286+
//////////////////////////////////////////////////////////// Cancellable wrappers
287+
250288
extension URLSession {
251289
/**
252290
Example usage with explicit cancel context:
@@ -331,93 +369,32 @@ extension URLSession {
331369
[OMGHTTPURLRQ]: https://github.com/mxcl/OMGHTTPURLRQ
332370
*/
333371
public func cancellableDataTask(_: PMKNamespacer, with convertible: URLRequestConvertible) -> CancellablePromise<(data: Data, response: URLResponse)> {
334-
var task: URLSessionTask!
335-
var reject: ((Error) -> Void)!
336-
337-
let promise = CancellablePromise<(data: Data, response: URLResponse)> {
338-
reject = $0.reject
339-
task = self.dataTask(with: convertible.pmkRequest, completionHandler: adapter($0))
340-
task.resume()
341-
}
342-
343-
promise.appendCancellableTask(task: task, reject: reject)
344-
return promise
372+
return cancellable(dataTask(.promise, with: convertible))
345373
}
346374

347375
/// Wraps the (Data?, URLResponse?, Error?) response from URLSession.uploadTask(with:from:) as CancellablePromise<(Data,URLResponse)>
348376
public func cancellableUploadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, from data: Data) -> CancellablePromise<(data: Data, response: URLResponse)> {
349-
var task: URLSessionTask!
350-
var reject: ((Error) -> Void)!
351-
352-
let promise = CancellablePromise<(data: Data, response: URLResponse)> {
353-
reject = $0.reject
354-
task = self.uploadTask(with: convertible.pmkRequest, from: data, completionHandler: adapter($0))
355-
task.resume()
356-
}
357-
358-
promise.appendCancellableTask(task: task, reject: reject)
359-
return promise
377+
return cancellable(uploadTask(.promise, with: convertible, from: data))
360378
}
361379

362380
/// Wraps the (Data?, URLResponse?, Error?) response from URLSession.uploadTask(with:fromFile:) as CancellablePromise<(Data,URLResponse)>
363381
public func cancellableUploadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, fromFile file: URL) -> CancellablePromise<(data: Data, response: URLResponse)> {
364-
var task: URLSessionTask!
365-
var reject: ((Error) -> Void)!
366-
367-
let promise = CancellablePromise<(data: Data, response: URLResponse)> {
368-
reject = $0.reject
369-
task = self.uploadTask(with: convertible.pmkRequest, fromFile: file, completionHandler: adapter($0))
370-
task.resume()
371-
}
372-
373-
promise.appendCancellableTask(task: task, reject: reject)
374-
return promise
382+
return cancellable(uploadTask(.promise, with: convertible, fromFile: file))
375383
}
376384

377385
/**
378386
Wraps the URLSesstionDownloadTask response from URLSession.downloadTask(with:) as CancellablePromise<(URL,URLResponse)>
379387
- Remark: we force a `to` parameter because Apple deletes the downloaded file immediately after the underyling completion handler returns.
380388
*/
381389
public func cancellableDownloadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, to saveLocation: URL) -> CancellablePromise<(saveLocation: URL, response: URLResponse)> {
382-
var task: URLSessionTask!
383-
var reject: ((Error) -> Void)!
384-
385-
let promise = CancellablePromise<(saveLocation: URL, response: URLResponse)> { seal in
386-
reject = seal.reject
387-
task = self.downloadTask(with: convertible.pmkRequest, completionHandler: { tmp, rsp, err in
388-
if let error = err {
389-
seal.reject(error)
390-
} else if let rsp = rsp, let tmp = tmp {
391-
do {
392-
try FileManager.default.moveItem(at: tmp, to: saveLocation)
393-
seal.fulfill((saveLocation, rsp))
394-
} catch {
395-
seal.reject(error)
396-
}
397-
} else {
398-
seal.reject(PMKError.invalidCallingConvention)
399-
}
400-
})
401-
task.resume()
402-
}
403-
404-
promise.appendCancellableTask(task: task, reject: reject)
405-
return promise
390+
return cancellable(downloadTask(.promise, with: convertible, to: saveLocation))
406391
}
407392
}
408393

409394
#if swift(>=3.1)
410395
public extension CancellablePromise where T == (data: Data, response: URLResponse) {
411396
func validate() -> CancellablePromise<T> {
412-
return map {
413-
guard let response = $0.response as? HTTPURLResponse else { return $0 }
414-
switch response.statusCode {
415-
case 200..<300:
416-
return $0
417-
case let code:
418-
throw PMKHTTPError.badStatusCode(code, $0.data, response)
419-
}
420-
}
397+
return cancellable(promise.validate())
421398
}
422399
}
423400
#endif

Sources/Process+Promise.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ extension Process {
6767
}
6868
}
6969

70-
return Promise { seal in
70+
return Promise(cancellableTask: self) { seal in
7171
q.async {
7272
self.waitUntilExit()
7373

@@ -143,8 +143,6 @@ extension Process {
143143
}
144144
}
145145

146-
//////////////////////////////////////////////////////////// Cancellation
147-
148146
extension Process: CancellableTask {
149147
/// Sends an interrupt signal to the process
150148
public func cancel() {
@@ -157,6 +155,8 @@ extension Process: CancellableTask {
157155
}
158156
}
159157

158+
//////////////////////////////////////////////////////////// Cancellable wrapper
159+
160160
extension Process {
161161
/**
162162
Launches the receiver and resolves when it exits, or when the promise is cancelled.
@@ -175,7 +175,7 @@ extension Process {
175175
context.cancel()
176176
*/
177177
public func cancellableLaunch(_: PMKNamespacer) -> CancellablePromise<(out: Pipe, err: Pipe)> {
178-
return CancellablePromise(task: self, self.launch(.promise))
178+
return cancellable(launch(.promise))
179179
}
180180
}
181181

0 commit comments

Comments
 (0)