Skip to content

Commit d72284b

Browse files
committed
Allow cancellation for network requests
1 parent 384ec38 commit d72284b

21 files changed

+275
-161
lines changed

SwiftIpfsApi.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
1F47CB9E244988B9006F251C /* CancellableRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F47CB9D244988B9006F251C /* CancellableRequest.swift */; };
11+
1F47CBA024498C67006F251C /* CancellableRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F47CB9F24498C67006F251C /* CancellableRequestTests.swift */; };
1012
AB0A3AB81BD6705B0090C97A /* SwiftIpfsApi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB0A3AAD1BD6705B0090C97A /* SwiftIpfsApi.framework */; };
1113
AB0A3ABD1BD6705B0090C97A /* SwiftIpfsApiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0A3ABC1BD6705B0090C97A /* SwiftIpfsApiTests.swift */; };
1214
AB0A3AC81BD671320090C97A /* MerkleNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB0A3AC71BD671320090C97A /* MerkleNode.swift */; };
@@ -43,6 +45,8 @@
4345
/* End PBXContainerItemProxy section */
4446

4547
/* Begin PBXFileReference section */
48+
1F47CB9D244988B9006F251C /* CancellableRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableRequest.swift; sourceTree = "<group>"; };
49+
1F47CB9F24498C67006F251C /* CancellableRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableRequestTests.swift; sourceTree = "<group>"; };
4650
AB0A3AAD1BD6705B0090C97A /* SwiftIpfsApi.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SwiftIpfsApi.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4751
AB0A3AB21BD6705B0090C97A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
4852
AB0A3AB71BD6705B0090C97A /* SwiftIpfsApiTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftIpfsApiTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -120,6 +124,7 @@
120124
AB0A3AB21BD6705B0090C97A /* Info.plist */,
121125
AB0A3AC71BD671320090C97A /* MerkleNode.swift */,
122126
ABEDF91D1BF0BE58007A1B2B /* JsonType.swift */,
127+
1F47CB9D244988B9006F251C /* CancellableRequest.swift */,
123128
AB490F8B1BF232F3005C5F57 /* Subcommands */,
124129
);
125130
path = SwiftIpfsApi;
@@ -129,6 +134,7 @@
129134
isa = PBXGroup;
130135
children = (
131136
AB0A3ABC1BD6705B0090C97A /* SwiftIpfsApiTests.swift */,
137+
1F47CB9F24498C67006F251C /* CancellableRequestTests.swift */,
132138
AB0A3ABE1BD6705B0090C97A /* Info.plist */,
133139
);
134140
path = SwiftIpfsApiTests;
@@ -302,6 +308,7 @@
302308
AB490F8A1BF232D4005C5F57 /* Refs.swift in Sources */,
303309
ABEDF91E1BF0BE58007A1B2B /* JsonType.swift in Sources */,
304310
AB490F9F1BF23516005C5F57 /* Config.swift in Sources */,
311+
1F47CB9E244988B9006F251C /* CancellableRequest.swift in Sources */,
305312
AB490F9D1BF234F1005C5F57 /* Diag.swift in Sources */,
306313
AB2C9EE01BD79F9B00BCCF01 /* HttpIo.swift in Sources */,
307314
AB490F881BF231F7005C5F57 /* Pin.swift in Sources */,
@@ -324,6 +331,7 @@
324331
isa = PBXSourcesBuildPhase;
325332
buildActionMask = 2147483647;
326333
files = (
334+
1F47CBA024498C67006F251C /* CancellableRequestTests.swift in Sources */,
327335
AB0A3ABD1BD6705B0090C97A /* SwiftIpfsApiTests.swift in Sources */,
328336
);
329337
runOnlyForDeploymentPostprocessing = 0;

SwiftIpfsApi/CancellableRequest.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// CancellableRequest.swift
3+
// SwiftIpfsApi
4+
//
5+
// Created by Marcel Voß on 17.04.20.
6+
// Copyright © 2020 Teo Sartori. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// A protocol that represents any network request that can be cancelled during execution.
12+
public protocol CancellableRequest {
13+
func cancel()
14+
}
15+
16+
/// A concrete type conforming to the `CancellableRequest` protocol that can be used for abstracting inner implementation
17+
/// details of the networking layer away and not leaking this kind of information to any integrators.
18+
struct CancellableDataTask: CancellableRequest {
19+
private let request: URLSessionDataTask
20+
21+
init(request: URLSessionDataTask) {
22+
self.request = request
23+
}
24+
25+
func cancel() {
26+
request.cancel()
27+
}
28+
}

SwiftIpfsApi/HttpIo.swift

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ enum HttpIoError : Error {
1717

1818
public struct HttpIo : NetworkIo {
1919

20-
public func receiveFrom(_ source: String, completionHandler: @escaping (Data) throws -> Void) throws {
20+
public func receiveFrom(_ source: String, completionHandler: @escaping (Data) throws -> Void) throws -> CancellableRequest {
2121
guard let url = URL(string: source) else { throw HttpIoError.urlError("Invalid URL") }
2222
print("HttpIo receiveFrom url is \(url)")
23+
2324
let task = URLSession.shared.dataTask(with: url) {
2425
(data: Data?, response: URLResponse?, error: Error?) in
2526

@@ -35,14 +36,16 @@ public struct HttpIo : NetworkIo {
3536
print("Error ", error, "in completionHandler passed to fetchData ")
3637
}
3738
}
38-
39+
3940
task.resume()
41+
42+
return CancellableDataTask(request: task)
4043
}
4144

4245

4346
public func streamFrom( _ source: String,
4447
updateHandler: @escaping (Data, URLSessionDataTask) throws -> Bool,
45-
completionHandler: @escaping (AnyObject) throws -> Void) throws {
48+
completionHandler: @escaping (AnyObject) throws -> Void) throws -> CancellableRequest {
4649

4750
guard let url = URL(string: source) else { throw HttpIoError.urlError("Invalid URL") }
4851
let config = URLSessionConfiguration.default
@@ -51,23 +54,24 @@ public struct HttpIo : NetworkIo {
5154
let task = session.dataTask(with: url)
5255

5356
task.resume()
57+
return CancellableDataTask(request: task)
5458
}
5559

56-
public func sendTo(_ target: String, content: Data, completionHandler: @escaping (Data) -> Void) throws {
60+
public func sendTo(_ target: String, content: Data, completionHandler: @escaping (Data) -> Void) throws -> CancellableRequest {
5761

5862
var multipart = try Multipart(targetUrl: target, encoding: .utf8)
5963
multipart = try Multipart.addFilePart(multipart, fileName: nil , fileData: content)
60-
Multipart.finishMultipart(multipart, completionHandler: completionHandler)
64+
return Multipart.finishMultipart(multipart, completionHandler: completionHandler)
6165
}
6266

6367

64-
public func sendTo(_ target: String, filePath: String, completionHandler: @escaping (Data) -> Void) throws {
68+
public func sendTo(_ target: String, filePath: String, completionHandler: @escaping (Data) -> Void) throws -> CancellableRequest {
6569

6670
var multipart = try Multipart(targetUrl: target, encoding: .utf8)
6771

6872
multipart = try handle(oldMultipart: multipart, files: [filePath])
6973

70-
Multipart.finishMultipart(multipart, completionHandler: completionHandler)
74+
return Multipart.finishMultipart(multipart, completionHandler: completionHandler)
7175

7276
}
7377

SwiftIpfsApi/IpfsApi.swift

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ extension IpfsApiClient {
5252

5353
func fetchStreamJson( _ path: String,
5454
updateHandler: @escaping (Data, URLSessionDataTask) throws -> Bool,
55-
completionHandler: @escaping (AnyObject) throws -> Void) throws {
55+
completionHandler: @escaping (AnyObject) throws -> Void) throws -> CancellableRequest {
5656
/// We need to use the passed in completionHandler
5757
try net.streamFrom(baseUrl + path, updateHandler: updateHandler, completionHandler: completionHandler)
5858
}
@@ -69,9 +69,10 @@ extension IpfsApiClient {
6969
// }
7070
// stream.close()
7171

72-
73-
func fetchJson(_ path: String, completionHandler: @escaping (JsonType) throws -> Void) throws {
74-
try fetchData(path) {
72+
73+
@discardableResult
74+
func fetchJson(_ path: String, completionHandler: @escaping (JsonType) throws -> Void) throws -> CancellableRequest {
75+
return try fetchData(path) {
7576
(data: Data) in
7677

7778
/// If there was no data fetched pass an empty dictionary and return.
@@ -99,13 +100,14 @@ extension IpfsApiClient {
99100
try completionHandler(JsonType.parse(json as AnyObject))
100101
}
101102
}
102-
103-
func fetchData(_ path: String, completionHandler: @escaping (Data) throws -> Void) throws {
104-
103+
104+
@discardableResult
105+
func fetchData(_ path: String, completionHandler: @escaping (Data) throws -> Void) throws -> CancellableRequest {
105106
try net.receiveFrom(baseUrl + path, completionHandler: completionHandler)
106107
}
107-
108-
func fetchBytes(_ path: String, completionHandler: @escaping ([UInt8]) throws -> Void) throws {
108+
109+
@discardableResult
110+
func fetchBytes(_ path: String, completionHandler: @escaping ([UInt8]) throws -> Void) throws -> CancellableRequest {
109111
try fetchData(path) {
110112
(data: Data) in
111113

@@ -239,9 +241,9 @@ public class IpfsApi : IpfsApiClient {
239241

240242

241243
/// base commands
242-
243-
public func add(_ filePath: String, completionHandler: @escaping ([MerkleNode]) -> Void) throws {
244-
244+
245+
@discardableResult
246+
public func add(_ filePath: String, completionHandler: @escaping ([MerkleNode]) -> Void) throws -> CancellableRequest {
245247
try net.sendTo(baseUrl+"add?s", filePath: filePath) {
246248
data in
247249
do {
@@ -265,9 +267,9 @@ public class IpfsApi : IpfsApiClient {
265267
}
266268

267269
// Store binary data
268-
269-
public func add(_ fileData: Data, completionHandler: @escaping ([MerkleNode]) -> Void) throws {
270-
270+
271+
@discardableResult
272+
public func add(_ fileData: Data, completionHandler: @escaping ([MerkleNode]) -> Void) throws -> CancellableRequest {
271273
try net.sendTo(baseUrl+"add?stream-channels=true", content: fileData) {
272274
data in
273275
do {
@@ -291,9 +293,9 @@ public class IpfsApi : IpfsApiClient {
291293
}
292294
}
293295
}
294-
295-
public func ls(_ hash: Multihash, completionHandler: @escaping ([MerkleNode]) -> Void) throws {
296-
296+
297+
@discardableResult
298+
public func ls(_ hash: Multihash, completionHandler: @escaping ([MerkleNode]) -> Void) throws -> CancellableRequest {
297299
try fetchJson("ls/\(b58String(hash))") {
298300
json in
299301

@@ -310,17 +312,18 @@ public class IpfsApi : IpfsApiClient {
310312
}
311313
}
312314

313-
public func cat(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws {
315+
@discardableResult
316+
public func cat(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws -> CancellableRequest {
314317
try fetchBytes("cat/\(b58String(hash))", completionHandler: completionHandler)
315318
}
316-
317-
public func get(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws {
319+
320+
@discardableResult
321+
public func get(_ hash: Multihash, completionHandler: @escaping ([UInt8]) -> Void) throws -> CancellableRequest {
318322
try self.cat(hash, completionHandler: completionHandler)
319323
}
320324

321-
322-
public func refs(_ hash: Multihash, recursive: Bool, completionHandler: @escaping ([Multihash]) -> Void) throws {
323-
325+
@discardableResult
326+
public func refs(_ hash: Multihash, recursive: Bool, completionHandler: @escaping ([Multihash]) -> Void) throws -> CancellableRequest {
324327
try fetchJson("refs?arg=" + b58String(hash) + "&r=\(recursive)") {
325328
result in
326329
guard let results = result.array else { throw IpfsApiError.unexpectedReturnType }
@@ -338,20 +341,23 @@ public class IpfsApi : IpfsApiClient {
338341
}
339342
}
340343

341-
public func resolve(_ scheme: String, hash: Multihash, recursive: Bool, completionHandler: @escaping (JsonType) -> Void) throws {
344+
@discardableResult
345+
public func resolve(_ scheme: String, hash: Multihash, recursive: Bool, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {
342346
try fetchJson("resolve?arg=/\(scheme)/\(b58String(hash))&r=\(recursive)", completionHandler: completionHandler)
343347
}
344-
345-
public func dns(_ domain: String, completionHandler: @escaping (String) -> Void) throws {
348+
349+
@discardableResult
350+
public func dns(_ domain: String, completionHandler: @escaping (String) -> Void) throws -> CancellableRequest {
346351
try fetchJson("dns?arg=" + domain) {
347352
result in
348353

349354
guard let path = result.object?[IpfsCmdString.Path.rawValue]?.string else { throw IpfsApiError.resultMissingData("No Path found") }
350355
completionHandler(path)
351356
}
352357
}
353-
354-
public func mount(_ ipfsRootPath: String = "/ipfs", ipnsRootPath: String = "/ipns", completionHandler: @escaping (JsonType) -> Void) throws {
358+
359+
@discardableResult
360+
public func mount(_ ipfsRootPath: String = "/ipfs", ipnsRootPath: String = "/ipns", completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {
355361

356362
let fileManager = FileManager.default
357363

@@ -363,25 +369,27 @@ public class IpfsApi : IpfsApiClient {
363369
try fileManager.createDirectory(atPath: ipnsRootPath, withIntermediateDirectories: false, attributes: nil)
364370
}
365371

366-
try fetchJson("mount?arg=" + ipfsRootPath + "&arg=" + ipnsRootPath, completionHandler: completionHandler)
372+
return try fetchJson("mount?arg=" + ipfsRootPath + "&arg=" + ipnsRootPath, completionHandler: completionHandler)
367373
}
368374

369375
/** ping is a tool to test sending data to other nodes.
370376
It finds nodes via the routing system, send pings, wait for pongs,
371377
and prints out round- trip latency information. */
372-
public func ping(_ target: String, completionHandler: @escaping (JsonType) -> Void) throws {
378+
@discardableResult
379+
public func ping(_ target: String, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {
373380
try fetchJson("ping/" + target, completionHandler: completionHandler)
374381
}
375382

376-
377-
public func id(_ target: String? = nil, completionHandler: @escaping (JsonType) -> Void) throws {
383+
@discardableResult
384+
public func id(_ target: String? = nil, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {
378385
var request = "id"
379386
if target != nil { request += "/\(target!)" }
380387

381-
try fetchJson(request, completionHandler: completionHandler)
388+
return try fetchJson(request, completionHandler: completionHandler)
382389
}
383-
384-
public func version(_ completionHandler: @escaping (String) -> Void) throws {
390+
391+
@discardableResult
392+
public func version(_ completionHandler: @escaping (String) -> Void) throws -> CancellableRequest {
385393
try fetchJson("version") {
386394
json in
387395
let version = json.object?[IpfsCmdString.Version.rawValue]?.string ?? ""
@@ -390,19 +398,21 @@ public class IpfsApi : IpfsApiClient {
390398
}
391399

392400
/** List all available commands. */
393-
public func commands(_ showOptions: Bool = false, completionHandler: @escaping (JsonType) -> Void) throws {
401+
@discardableResult
402+
public func commands(_ showOptions: Bool = false, completionHandler: @escaping (JsonType) -> Void) throws -> CancellableRequest {
394403

395404
var request = "commands" //+ (showOptions ? "?flags=true&" : "")
396405
if showOptions { request += "?flags=true&" }
397406

398-
try fetchJson(request, completionHandler: completionHandler)
407+
return try fetchJson(request, completionHandler: completionHandler)
399408
}
400409

401410
/** This method should take both a completion handler and an update handler.
402411
Since the log tail won't stop until interrupted, the update handler
403412
should return false when it wants the updates to stop.
404413
*/
405-
public func log(_ updateHandler: (Data) throws -> Bool, completionHandler: @escaping ([[String : AnyObject]]) -> Void) throws {
414+
@discardableResult
415+
public func log(_ updateHandler: (Data) throws -> Bool, completionHandler: @escaping ([[String : AnyObject]]) -> Void) throws -> CancellableRequest {
406416

407417
/// Two test closures to be passed to the fetchStreamJson as parameters.
408418
let comp = { (result: AnyObject) -> Void in
@@ -426,16 +436,17 @@ public class IpfsApi : IpfsApiClient {
426436
return true
427437
}
428438

429-
try fetchStreamJson("log/tail", updateHandler: update, completionHandler: comp)
439+
return try fetchStreamJson("log/tail", updateHandler: update, completionHandler: comp)
430440
}
431441
}
432442

433443

434444

435445
/** Show or edit the list of bootstrap peers */
436446
extension IpfsApiClient {
437-
438-
public func bootstrap(_ completionHandler: @escaping ([Multiaddr]) -> Void) throws {
447+
448+
@discardableResult
449+
public func bootstrap(_ completionHandler: @escaping ([Multiaddr]) -> Void) throws -> CancellableRequest {
439450
try bootstrap.list(completionHandler)
440451
}
441452
}

SwiftIpfsApi/Multipart.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ extension Multipart {
141141
return oldMultipart
142142
}
143143

144-
public static func finishMultipart(_ multipart: Multipart, completionHandler: @escaping (Data) -> Void) {
144+
public static func finishMultipart(_ multipart: Multipart, completionHandler: @escaping (Data) -> Void) -> CancellableRequest {
145145

146146
let outString = "--" + multipart.boundary + "--" + lineFeed
147147

@@ -167,5 +167,7 @@ extension Multipart {
167167
}
168168

169169
task.resume()
170+
171+
return CancellableDataTask(request: task)
170172
}
171173
}

SwiftIpfsApi/NetworkIo.swift

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
import Foundation
1212

1313
public protocol NetworkIo {
14-
15-
func receiveFrom(_ source: String, completionHandler: @escaping (Data) throws -> Void) throws
1614

17-
func streamFrom(_ source: String, updateHandler: @escaping (Data, URLSessionDataTask) throws -> Bool, completionHandler: @escaping (AnyObject) throws -> Void) throws
18-
19-
func sendTo(_ target: String, content: Data, completionHandler: @escaping (Data) -> Void) throws
15+
func receiveFrom(_ source: String, completionHandler: @escaping (Data) throws -> Void) throws -> CancellableRequest
16+
17+
func streamFrom(_ source: String,
18+
updateHandler: @escaping (Data, URLSessionDataTask) throws -> Bool,
19+
completionHandler: @escaping (AnyObject) throws -> Void) throws -> CancellableRequest
20+
21+
func sendTo(_ target: String, content: Data, completionHandler: @escaping (Data) -> Void) throws -> CancellableRequest
2022

2123
/// If we want to send location addressed content
22-
func sendTo(_ target: String, filePath: String, completionHandler: @escaping (Data) -> Void) throws
24+
func sendTo(_ target: String, filePath: String, completionHandler: @escaping (Data) -> Void) throws -> CancellableRequest
2325
}

0 commit comments

Comments
 (0)