Browse Source

Add withAllRequests. (#3200)

Jon Shier 5 years ago
parent
commit
266919c512
3 changed files with 122 additions and 57 deletions
  1. 21 0
      Documentation/AdvancedUsage.md
  2. 21 4
      Source/Session.swift
  3. 80 53
      Tests/SessionTests.swift

+ 21 - 0
Documentation/AdvancedUsage.md

@@ -202,6 +202,27 @@ monitor.requestDidCompleteTaskWithError = { (request, task, error) in
 let session = Session(eventMonitors: [monitor])
 ```
 
+### Operating on All Requests
+Although use should be rare, `Session` provides the `withAllRequests` method to operate on all currently active `Request`s. This work is performed on the `Session`'s `rootQueue`, so it's important to keep it quick. If the work may take some time, creating a separate queue to process the `Set` of `Request`s should be used.
+
+```swift
+let session = ... // Some Session.
+session.withAllRequests { requests in 
+    requests.forEach { $0.suspend() }
+}
+```
+
+Additionally, `Session` offers a convenience method to cancel all `Request`s and call a completion handler when complete.
+
+```swift
+let session = ... // Some Session.
+session.cancelAllRequests(completingOn: .main) { // completingOn uses .main by default.
+    print("Cancelled all requests.")
+}
+```
+
+> Note: These actions are performed asynchronously, so requests may be created or have finished by the time it's actually run, so it should not be assumed the action will be performed on a particular set of `Request`s.
+
 ### Creating Instances From `URLSession`s
 In addition to the `convenience` initializer mentioned previously, `Session`s can be initialized directly from `URLSession`s. However, there are several requirements to keep in mind when using this initializer, so using the convenience initializer is recommended. These include:
 

+ 21 - 4
Source/Session.swift

@@ -204,7 +204,22 @@ open class Session {
         session.invalidateAndCancel()
     }
 
-    // MARK: - Cancellation
+    // MARK: - All Requests API
+
+    /// Perform an action on all active `Request`s.
+    ///
+    /// - Note: The provided `action` closure is performed asynchronously, meaning that some `Request`s may complete and
+    ///         be unavailable by time it runs. Additionally, this action is performed on the instances's `rootQueue`,
+    ///         so care should be taken that actions are fast. Once the work on the `Request`s is complete, any
+    ///         additional work should be performed on another queue.
+    ///
+    /// - Parameters:
+    ///   - action:     Closure to perform with all `Request`s.
+    public func withAllRequests(perform action: @escaping (Set<Request>) -> Void) {
+        rootQueue.async {
+            action(self.activeRequests)
+        }
+    }
 
     /// Cancel all active `Request`s, optionally calling a completion handler when complete.
     ///
@@ -216,9 +231,11 @@ open class Session {
     ///   - queue:      `DispatchQueue` on which the completion handler is run. `.main` by default.
     ///   - completion: Closure to be called when all `Request`s have been cancelled.
     public func cancelAllRequests(completingOnQueue queue: DispatchQueue = .main, completion: (() -> Void)? = nil) {
-        rootQueue.async {
-            self.activeRequests.forEach { $0.cancel() }
-            queue.async { completion?() }
+        withAllRequests { requests in
+            requests.forEach { $0.cancel() }
+            queue.async {
+                completion?()
+            }
         }
     }
 

+ 80 - 53
Tests/SessionTests.swift

@@ -1473,11 +1473,90 @@ final class SessionTestCase: BaseTestCase {
         XCTAssertEqual(request.state, .finished)
         XCTAssertEqual(response?.result.isSuccess, true)
     }
+
+    // MARK: Invalid Requests
+
+    func testThatGETRequestsWithBodyDataAreConsideredInvalid() {
+        // Given
+        let session = Session()
+        var request = URLRequest.makeHTTPBinRequest()
+        request.httpBody = Data("invalid".utf8)
+        let expect = expectation(description: "request should complete")
+        var response: DataResponse<HTTPBinResponse, AFError>?
+
+        // When
+        session.request(request).responseDecodable(of: HTTPBinResponse.self) { resp in
+            response = resp
+            expect.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual(response?.result.isFailure, true)
+        XCTAssertEqual(response?.error?.isBodyDataInGETRequest, true)
+    }
+
+    func testThatAdaptedGETRequestsWithBodyDataAreConsideredInvalid() {
+        // Given
+        struct InvalidAdapter: RequestInterceptor {
+            func adapt(_ urlRequest: URLRequest,
+                       for session: Session,
+                       completion: @escaping (Result<URLRequest, Error>) -> Void) {
+                var request = urlRequest
+                request.httpBody = Data("invalid".utf8)
+
+                completion(.success(request))
+            }
+        }
+        let session = Session(interceptor: InvalidAdapter())
+        let request = URLRequest.makeHTTPBinRequest()
+        let expect = expectation(description: "request should complete")
+        var response: DataResponse<HTTPBinResponse, AFError>?
+
+        // When
+        session.request(request).responseDecodable(of: HTTPBinResponse.self) { resp in
+            response = resp
+            expect.fulfill()
+        }
+
+        waitForExpectations(timeout: timeout)
+
+        // Then
+        XCTAssertEqual(response?.result.isFailure, true)
+        XCTAssertEqual(response?.error?.isRequestAdaptationError, true)
+        XCTAssertEqual(response?.error?.underlyingError?.asAFError?.isBodyDataInGETRequest, true)
+    }
 }
 
 // MARK: -
 
-final class SessionCancellationTestCase: BaseTestCase {
+final class SessionMassActionTestCase: BaseTestCase {
+    func testThatRequestsCanHaveMassActionsPerformed() {
+        // Given
+        let count = 10
+        let createdTasks = expectation(description: "all tasks created")
+        createdTasks.expectedFulfillmentCount = count
+        let massActions = expectation(description: "cancel all requests should be called")
+        let monitor = ClosureEventMonitor()
+        monitor.requestDidCreateTask = { _, _ in createdTasks.fulfill() }
+        let session = Session(eventMonitors: [monitor])
+        let request = URLRequest.makeHTTPBinRequest(path: "delay/1")
+        var requests: [DataRequest] = []
+
+        // When
+        requests = (0..<count).map { _ in session.request(request) }
+
+        wait(for: [createdTasks], timeout: timeout)
+
+        session.withAllRequests { $0.forEach { $0.suspend() }; massActions.fulfill() }
+
+        wait(for: [massActions], timeout: timeout)
+
+        // Then
+        XCTAssertTrue(requests.allSatisfy { $0.isSuspended })
+    }
+
     func testThatAutomaticallyResumedRequestsCanBeMassCancelled() {
         // Given
         let count = 100
@@ -1621,58 +1700,6 @@ final class SessionCancellationTestCase: BaseTestCase {
             XCTAssertTrue(session.activeRequests.isEmpty, "activeRequests should be empty but has \(session.activeRequests.count) items")
         }
     }
-
-    func testThatGETRequestsWithBodyDataAreConsideredInvalid() {
-        // Given
-        let session = Session()
-        var request = URLRequest.makeHTTPBinRequest()
-        request.httpBody = Data("invalid".utf8)
-        let expect = expectation(description: "request should complete")
-        var response: DataResponse<HTTPBinResponse, AFError>?
-
-        // When
-        session.request(request).responseDecodable(of: HTTPBinResponse.self) { resp in
-            response = resp
-            expect.fulfill()
-        }
-
-        waitForExpectations(timeout: timeout)
-
-        // Then
-        XCTAssertEqual(response?.result.isFailure, true)
-        XCTAssertEqual(response?.error?.isBodyDataInGETRequest, true)
-    }
-
-    func testThatAdaptedGETRequestsWithBodyDataAreConsideredInvalid() {
-        // Given
-        struct InvalidAdapter: RequestInterceptor {
-            func adapt(_ urlRequest: URLRequest,
-                       for session: Session,
-                       completion: @escaping (Result<URLRequest, Error>) -> Void) {
-                var request = urlRequest
-                request.httpBody = Data("invalid".utf8)
-
-                completion(.success(request))
-            }
-        }
-        let session = Session(interceptor: InvalidAdapter())
-        let request = URLRequest.makeHTTPBinRequest()
-        let expect = expectation(description: "request should complete")
-        var response: DataResponse<HTTPBinResponse, AFError>?
-
-        // When
-        session.request(request).responseDecodable(of: HTTPBinResponse.self) { resp in
-            response = resp
-            expect.fulfill()
-        }
-
-        waitForExpectations(timeout: timeout)
-
-        // Then
-        XCTAssertEqual(response?.result.isFailure, true)
-        XCTAssertEqual(response?.error?.isRequestAdaptationError, true)
-        XCTAssertEqual(response?.error?.underlyingError?.asAFError?.isBodyDataInGETRequest, true)
-    }
 }
 
 // MARK: -