@@ -22,6 +22,18 @@ extension HTTPAPIResponse where Body == Data {
2222
2323extension URLSession {
2424
25+ /// Create a background URLSession instance that can be used in the `perform(request:...)` async function.
26+ ///
27+ /// The `perform(request:...)` async function can be used in all non-background URLSession instances without any
28+ /// extra work. However, there is a requirement to make the function works with with background URLSession instances.
29+ /// That is the URLSession must have a delegate of `BackgroundURLSessionDelegate` type.
30+ static func backgroundSession( configuration: URLSessionConfiguration ) -> URLSession {
31+ assert ( configuration. identifier != nil )
32+ // Pass `delegateQueue: nil` to get a serial queue, which is required to ensure thread safe access to
33+ // `WordPressKitSessionDelegate` instances.
34+ return URLSession ( configuration: configuration, delegate: BackgroundURLSessionDelegate ( ) , delegateQueue: nil )
35+ }
36+
2537 /// Send a HTTP request and return its response as a `WordPressAPIResult` instance.
2638 ///
2739 /// ## Progress Tracking and Cancellation
@@ -49,6 +61,10 @@ extension URLSession {
4961 fulfilling parentProgress: Progress ? = nil ,
5062 errorType: E . Type = E . self
5163 ) async -> WordPressAPIResult < HTTPAPIResponse < Data > , E > {
64+ if configuration. identifier != nil {
65+ assert ( delegate is BackgroundURLSessionDelegate , " Unexpected URLSession delegate type. See the `backgroundSession(configuration:)` " )
66+ }
67+
5268 if let parentProgress {
5369 assert ( parentProgress. completedUnitCount == 0 && parentProgress. totalUnitCount > 0 , " Invalid parent progress " )
5470 assert ( parentProgress. cancellationHandler == nil , " The progress instance's cancellationHandler property must be nil " )
@@ -97,23 +113,50 @@ extension URLSession {
97113 ) throws -> URLSessionTask {
98114 var request = try builder. build ( encodeBody: false )
99115
116+ // This additional `callCompletionFromDelegate` is added so that we can test `BackgroundURLSessionDelegate`
117+ // in unit tests. Background URLSession doesn't work on unit tests, we have to create a non-background URLSession
118+ // which has a `BackgroundURLSessionDelegate` delegate in order to test `BackgroundURLSessionDelegate`.
119+ //
120+ // In reality, `callCompletionFromDelegate` and `isBackgroundSession` have the same value.
121+ let callCompletionFromDelegate = delegate is BackgroundURLSessionDelegate
100122 let isBackgroundSession = configuration. identifier != nil
123+ let task : URLSessionTask
101124 let body = try builder. encodeMultipartForm ( request: & request, forceWriteToFile: isBackgroundSession)
102125 ?? builder. encodeXMLRPC ( request: & request, forceWriteToFile: isBackgroundSession)
103126 if let body {
104127 // Use special `URLSession.uploadTask` API for multipart POST requests.
105- return body. map (
128+ task = body. map (
106129 left: {
107- uploadTask ( with: request, from: $0, completionHandler: completion)
130+ if callCompletionFromDelegate {
131+ return uploadTask ( with: request, from: $0)
132+ } else {
133+ return uploadTask ( with: request, from: $0, completionHandler: completion)
134+ }
108135 } ,
109136 right: {
110- uploadTask ( with: request, fromFile: $0, completionHandler: completion)
137+ if callCompletionFromDelegate {
138+ return uploadTask ( with: request, fromFile: $0)
139+ } else {
140+ return uploadTask ( with: request, fromFile: $0, completionHandler: completion)
141+ }
111142 }
112143 )
113144 } else {
114145 // Use `URLSession.dataTask` for all other request
115- return dataTask ( with: request, completionHandler: completion)
146+ if callCompletionFromDelegate {
147+ task = dataTask ( with: request)
148+ } else {
149+ task = dataTask ( with: request, completionHandler: completion)
150+ }
151+ }
152+
153+ if callCompletionFromDelegate {
154+ assert ( delegate is BackgroundURLSessionDelegate , " Unexpected URLSession delegate type. See the `backgroundSession(configuration:)` " )
155+
156+ set ( completion: completion, forTaskWithIdentifier: task. taskIdentifier)
116157 }
158+
159+ return task
117160 }
118161
119162 private static func parseResponse< E: LocalizedError > (
@@ -205,3 +248,76 @@ extension Progress {
205248 }
206249 }
207250}
251+
252+ // MARK: - Background URL Session Support
253+
254+ private final class SessionTaskData {
255+ var responseBody = Data ( )
256+ var completion : ( ( Data ? , URLResponse ? , Error ? ) -> Void ) ?
257+ }
258+
259+ class BackgroundURLSessionDelegate : NSObject , URLSessionDataDelegate {
260+
261+ private var taskData = [ Int: SessionTaskData] ( )
262+
263+ func urlSession( _ session: URLSession , dataTask: URLSessionDataTask , didReceive data: Data ) {
264+ session. recieved ( data, forTaskWithIdentifier: dataTask. taskIdentifier)
265+ }
266+
267+ func urlSession( _ session: URLSession , task: URLSessionTask , didCompleteWithError error: Error ? ) {
268+ session. completed ( with: error, response: task. response, forTaskWithIdentifier: task. taskIdentifier)
269+ }
270+
271+ }
272+
273+ private extension URLSession {
274+
275+ static var taskDataKey = 0
276+
277+ // A map from `URLSessionTask` identifier to in-memory data of the given task.
278+ //
279+ // This property is in `URLSession` not `BackgroundURLSessionDelegate` because task id (the key) is unique within
280+ // the context of a `URLSession` instance. And in theory `BackgroundURLSessionDelegate` can be used by multiple
281+ // `URLSession` instances.
282+ var taskData : [ Int : SessionTaskData ] {
283+ get {
284+ objc_getAssociatedObject ( self , & URLSession. taskDataKey) as? [ Int : SessionTaskData ] ?? [ : ]
285+ }
286+ set {
287+ objc_setAssociatedObject ( self , & URLSession. taskDataKey, newValue, . OBJC_ASSOCIATION_RETAIN)
288+ }
289+ }
290+
291+ func updateData( forTaskWithIdentifier taskID: Int , using closure: ( SessionTaskData ) -> Void ) {
292+ let task = self . taskData [ taskID] ?? SessionTaskData ( )
293+ closure ( task)
294+ self . taskData [ taskID] = task
295+ }
296+
297+ func set( completion: @escaping ( Data ? , URLResponse ? , Error ? ) -> Void , forTaskWithIdentifier taskID: Int ) {
298+ updateData ( forTaskWithIdentifier: taskID) {
299+ $0. completion = completion
300+ }
301+ }
302+
303+ func recieved( _ data: Data , forTaskWithIdentifier taskID: Int ) {
304+ updateData ( forTaskWithIdentifier: taskID) { task in
305+ task. responseBody. append ( data)
306+ }
307+ }
308+
309+ func completed( with error: Error ? , response: URLResponse ? , forTaskWithIdentifier taskID: Int ) {
310+ guard let task = taskData [ taskID] else {
311+ return
312+ }
313+
314+ if let error {
315+ task. completion ? ( nil , response, error)
316+ } else {
317+ task. completion ? ( task. responseBody, response, nil )
318+ }
319+
320+ self . taskData. removeValue ( forKey: taskID)
321+ }
322+
323+ }
0 commit comments