From 0c3780efc9e4fb2c087a304319bc5937d0fb5586 Mon Sep 17 00:00:00 2001 From: Jon Shier Date: Mon, 13 Dec 2021 17:28:25 -0500 Subject: [PATCH] Swift Concurrency Support (#3463) * Add Concurrency support. * Remove misleading Protected convenience APIs. * Safer NSLog usage. * Project updates. * Disable thread sanitizer during development. * Update tests for new APIs. * Add concurrency tester. * Fix concurrency runner. * 15.2 simulator for concurrency tests. * Disable thread sanitizer on iOS as well. * Update inline docs, add stream cancellation. * Add documentation. * Document async value handlers. * Remove TODO. * Add doc comment. * Target the concurrent queue for the streams. * Rename finishObservers to finishHandlers. --- .github/workflows/ci.yml | 15 + Alamofire.xcodeproj/project.pbxproj | 22 +- .../xcschemes/Alamofire iOS.xcscheme | 1 - .../xcschemes/Alamofire macOS.xcscheme | 1 - Documentation/AdvancedUsage.md | 124 ++++ Example/iOS Example.xcodeproj/project.pbxproj | 17 +- Source/Concurrency.swift | 698 ++++++++++++++++++ Source/HTTPHeaders.swift | 4 +- Source/Protected.swift | 40 - Source/Request.swift | 19 +- Source/ResponseSerialization.swift | 6 + Tests/BaseTestCase.swift | 16 +- Tests/ConcurrencyTests.swift | 526 +++++++++++++ Tests/NSLoggingEventMonitor.swift | 86 +-- 14 files changed, 1482 insertions(+), 93 deletions(-) create mode 100644 Source/Concurrency.swift create mode 100644 Tests/ConcurrencyTests.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb8326d94..d1eda0a2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,21 @@ jobs: run: arch -arch arm64e brew install alamofire/alamofire/firewalk || arch -arch arm64e brew upgrade alamofire/alamofire/firewalk && arch -arch x86_64 firewalk & - name: iOS - ${{ matrix.destination }} run: set -o pipefail && arch -arch arm64e env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "${{ matrix.destination }}" clean test | xcpretty + iOS_Concurrency: + name: Test Swift Concurrency + runs-on: macOS-11 + env: + DEVELOPER_DIR: /Applications/Xcode_13.2.app/Contents/Developer + timeout-minutes: 10 + strategy: + matrix: + destination: ["OS=15.2,name=iPhone 13 Pro"] + steps: + - uses: actions/checkout@v2 + - name: Install Firewalk + run: brew install alamofire/alamofire/firewalk || brew upgrade alamofire/alamofire/firewalk && firewalk & + - name: iOS - ${{ matrix.destination }} + run: set -o pipefail && env NSUnbufferedIO=YES xcodebuild -project "Alamofire.xcodeproj" -scheme "Alamofire iOS" -destination "${{ matrix.destination }}" -enableThreadSanitizer NO clean test | xcpretty tvOS: name: Test tvOS runs-on: firebreak diff --git a/Alamofire.xcodeproj/project.pbxproj b/Alamofire.xcodeproj/project.pbxproj index f3e5d1a25..8c7d43c0c 100644 --- a/Alamofire.xcodeproj/project.pbxproj +++ b/Alamofire.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -187,6 +187,13 @@ 319917BA209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; }; 319917BB209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; }; 319917BC209CE53A00103A19 /* OperationQueue+Alamofire.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319917B8209CE53A00103A19 /* OperationQueue+Alamofire.swift */; }; + 31B3DE3B25C11CEA00760641 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE3A25C11CEA00760641 /* Concurrency.swift */; }; + 31B3DE3C25C11CEA00760641 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE3A25C11CEA00760641 /* Concurrency.swift */; }; + 31B3DE3D25C11CEA00760641 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE3A25C11CEA00760641 /* Concurrency.swift */; }; + 31B3DE3E25C11CEA00760641 /* Concurrency.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE3A25C11CEA00760641 /* Concurrency.swift */; }; + 31B3DE4F25C120D800760641 /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */; }; + 31B3DE5025C120D800760641 /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */; }; + 31B3DE5125C120D800760641 /* ConcurrencyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */; }; 31B51E8C2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; }; 31B51E8D2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; }; 31B51E8E2434FECB005356DB /* RequestModifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */; }; @@ -479,6 +486,8 @@ 319ECEA425EC96E8001C38CA /* config.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = config.yml; sourceTree = ""; }; 319ECEA525EC9710001C38CA /* FEATURE_REQUEST.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = FEATURE_REQUEST.md; sourceTree = ""; }; 31B2CA9521AA25CD005B371A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + 31B3DE3A25C11CEA00760641 /* Concurrency.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Concurrency.swift; sourceTree = ""; }; + 31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConcurrencyTests.swift; sourceTree = ""; }; 31B51E8B2434FECB005356DB /* RequestModifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestModifierTests.swift; sourceTree = ""; }; 31BADE4D2439A8D1007D2AB9 /* CombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CombineTests.swift; sourceTree = ""; }; 31D83FCD20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLConvertible+URLRequestConvertible.swift"; sourceTree = ""; }; @@ -697,6 +706,7 @@ 4CFD6B132201338E00FFB5E3 /* CachedResponseHandlerTests.swift */, 4C341BB91B1A865A00C1B34D /* CacheTests.swift */, 31BADE4D2439A8D1007D2AB9 /* CombineTests.swift */, + 31B3DE4E25C120D800760641 /* ConcurrencyTests.swift */, 3113D46A21878227001CCD21 /* HTTPHeadersTests.swift */, 4C3238E61B3604DB00FE04AE /* MultipartFormDataTests.swift */, 4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */, @@ -882,6 +892,7 @@ 4C67D1352454B12A00CBA725 /* AuthenticationInterceptor.swift */, 4C4466EA21F8F5D800AC9703 /* CachedResponseHandler.swift */, 318DD40E2439780500963291 /* Combine.swift */, + 31B3DE3A25C11CEA00760641 /* Concurrency.swift */, 3111CE8720A77843008315E2 /* EventMonitor.swift */, 4C23EB421B327C5B0090E0BC /* MultipartFormData.swift */, 311B198F20B0D3B40036823B /* MultipartUpload.swift */, @@ -1262,7 +1273,7 @@ }; }; buildConfigurationList = F8111E2D19A95C8B0040E7D1 /* Build configuration list for PBXProject "Alamofire" */; - compatibilityVersion = "Xcode 11.4"; + compatibilityVersion = "Xcode 12.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -1520,6 +1531,7 @@ 3191B5771F5F53A6003960A8 /* Protected.swift in Sources */, 3199179A209CDA7F00103A19 /* Response.swift in Sources */, 31D83FD020D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, + 31B3DE3D25C11CEA00760641 /* Concurrency.swift in Sources */, 319917A7209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4CF627131BA7CBF60011A099 /* Validation.swift in Sources */, 31F5085F20B50DC400FE2A0C /* URLSessionConfiguration+Alamofire.swift in Sources */, @@ -1590,6 +1602,7 @@ 31C2B0EC20B271060089BA7C /* CacheTests.swift in Sources */, 3111CE9120A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, 31BADE502439A8D1007D2AB9 /* CombineTests.swift in Sources */, + 31B3DE5125C120D800760641 /* ConcurrencyTests.swift in Sources */, 31762DCC247738FA0025C704 /* LeaksTests.swift in Sources */, 31425AC3241F098000EE3CCC /* InternalRequestTests.swift in Sources */, 4CF627171BA7CC240011A099 /* ParameterEncodingTests.swift in Sources */, @@ -1604,6 +1617,7 @@ 3191B5761F5F53A6003960A8 /* Protected.swift in Sources */, 4CDE2C471AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */, 31991799209CDA7F00103A19 /* Response.swift in Sources */, + 31B3DE3C25C11CEA00760641 /* Concurrency.swift in Sources */, 31D83FCF20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, 319917A6209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4C1DC8551B68908E00476DE3 /* AFError.swift in Sources */, @@ -1646,6 +1660,7 @@ E4202FD01B667AA100C997FB /* ParameterEncoding.swift in Sources */, 3191B5781F5F53A6003960A8 /* Protected.swift in Sources */, 3199179B209CDA7F00103A19 /* Response.swift in Sources */, + 31B3DE3E25C11CEA00760641 /* Concurrency.swift in Sources */, 31D83FD120D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, 319917A8209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4CEC605A1B745C9100E684F4 /* AFError.swift in Sources */, @@ -1688,6 +1703,7 @@ 3191B5751F5F53A6003960A8 /* Protected.swift in Sources */, 4CDE2C461AF89FF300BABAE5 /* ResponseSerialization.swift in Sources */, 31991798209CDA7F00103A19 /* Response.swift in Sources */, + 31B3DE3B25C11CEA00760641 /* Concurrency.swift in Sources */, 31D83FCE20D5C29300D93E47 /* URLConvertible+URLRequestConvertible.swift in Sources */, 319917A5209CDAC400103A19 /* RequestTaskMap.swift in Sources */, 4C1DC8541B68908E00476DE3 /* AFError.swift in Sources */, @@ -1758,6 +1774,7 @@ 31C2B0EA20B271040089BA7C /* CacheTests.swift in Sources */, 3111CE8F20A7EC26008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, 31BADE4E2439A8D1007D2AB9 /* CombineTests.swift in Sources */, + 31B3DE4F25C120D800760641 /* ConcurrencyTests.swift in Sources */, 31762DCA247738FA0025C704 /* LeaksTests.swift in Sources */, 31425AC1241F098000EE3CCC /* InternalRequestTests.swift in Sources */, F8111E6119A9674D0040E7D1 /* ParameterEncodingTests.swift in Sources */, @@ -1800,6 +1817,7 @@ 31C2B0EB20B271050089BA7C /* CacheTests.swift in Sources */, 3111CE9020A7EC27008315E2 /* NetworkReachabilityManagerTests.swift in Sources */, 31BADE4F2439A8D1007D2AB9 /* CombineTests.swift in Sources */, + 31B3DE5025C120D800760641 /* ConcurrencyTests.swift in Sources */, 31762DCB247738FA0025C704 /* LeaksTests.swift in Sources */, 31425AC2241F098000EE3CCC /* InternalRequestTests.swift in Sources */, 4C256A541B096C770065714F /* BaseTestCase.swift in Sources */, diff --git a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme index c610b5c58..bb1c991f2 100644 --- a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme +++ b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire iOS.xcscheme @@ -41,7 +41,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "NO" - enableThreadSanitizer = "YES" codeCoverageEnabled = "YES" onlyGenerateCoverageForSpecifiedTargets = "YES"> diff --git a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme index bd16c1475..563d255f0 100644 --- a/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme +++ b/Alamofire.xcodeproj/xcshareddata/xcschemes/Alamofire macOS.xcscheme @@ -42,7 +42,6 @@ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "NO" enableASanStackUseAfterReturn = "YES" - enableThreadSanitizer = "YES" codeCoverageEnabled = "YES" onlyGenerateCoverageForSpecifiedTargets = "YES"> diff --git a/Documentation/AdvancedUsage.md b/Documentation/AdvancedUsage.md index 334b013d0..437972c69 100644 --- a/Documentation/AdvancedUsage.md +++ b/Documentation/AdvancedUsage.md @@ -1211,6 +1211,130 @@ Like most `DownloadRequest`'s response handlers, `DownloadResponsePublisher` rea #### `DataStreamPublisher` `DataStreamPublisher` is a `Publisher` for `DataStreamRequest`s. Like `DataStreamRequest` itself, and unlike Alamofire's other `Publisher`s, `DataStreamPublisher` can return multiple values serialized from `Data` received from the network, as well as a final completion event. For more information on how `DataStreamRequest` works, please see our [detailed usage documentation](https://github.com/Alamofire/Alamofire/blob/master/Documentation/Usage.md#streaming-data-from-a-server). +## Using Alamofire with Swift Concurrency +Swift's concurrency features, released in Swift 5.5, provide fundamental asynchronous building blocks in the language, including `async`-`await` syntax, `Task`s, and actors. Alamofire provides extensions allowing the use of common Alamofire APIs with Swift's concurrency features. + +> Alamofire's concurrency support requires Swift 5.5.2 or Xcode 13.2. These examples also include the use of static protocol values added in Alamofire 5.5 for Swift 5.5. + +Alamofire's concurrency support works by vending various `*Task` types, like `DataTask`, `DownloadTask`, and `DataStreamTask`. These types work similarly to Alamofire's existing response handlers and convert the standard completion handlers into `async` properties which can be `await`ed. For example, `DataRequest` (and `UploadRequest`, which inherits from `DataRequest`) can provide a `DataTask` used to `await` any of the asynchronous values: + +```swift +let value = try await AF.request(...).serializingDecodable(TestResponse.self).value +``` + +This code synchronously produces a `DataTask` value which can be used to `await` any part of the resulting `DataResponse`. Each `DataTask` can be used to `await` any of these properties as many times as needed. For example: + +```swift +let dataTask = AF.request(...).serializingDecodable(TestResponse.self) +// Later... +let response = await task.response // Returns full DataResponse +// Elsewhere... +let result = await task.result // Returns Result +// And... +let value = try await task.value // Returns the TestResponse or throws the AFError +``` + +Like all Swift Concurrency APIs, these `await`able properties can be used to `await` multiple requests issued in parallel. For example: + +```swift +async let first = AF.request(...).serializingDecodable(TestResponse.self).response +async let second = AF.request(...).serializingString().response +async let third = AF.request(...).serializingData().response + +// Later... + +// Produces (DataResponse, DataResponse, DataResponse) +// when all requests are complete. +let responses = await (first, second, third) +``` + +Alamofire's concurrency APIs can also be used with other builtin concurrency constructs like `Task` and `TaskGroup`. + +### `DownloadRequest` Support + +Like `DataRequest`, `DownloadRequest` vends its own `DownloadTask` value which can be used to `await` the completion of the request. Like the existing response handlers, the `DownloadTask` will read the downloaded `Data` from disk, so if the `Data` is very large it's best to simply get the `URL` and read the `Data` in a way that won't read it all into memory at once. + +```swift +let url = try await AF.download(...).serializingURL().value +``` + +#### Automatic Cancellation + +By default, `DataTask` and `DownloadTask` values do not cancel the underlying request when an enclosing concurrent context is cancelled. This means that request will complete even if the enclosing context is explicitly cancelled. For example: + +```swift +let request = AF.request(...) // Creates the DataRequest. +let task = Task { // Produces a `Task, Never> value. + await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).response +} + +// Later... + +task.cancel() // task is cancelled, but the DataRequest created inside it is not. +print(task.isCancelled) // true +print(request.isCancelled) // false +``` + +If automatic cancellation is desired, it can be configured when creating the `DataTask` or `DownloadTask`. For example: + +```swift +let request = AF.request(...) // Creates the DataRequest. +let task = Task { // Produces a `Task, Never> value. + await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).response +} + +// Later... + +task.cancel() // task is cancelled. +print(task.isCancelled) // true +print(request.isCancelled) // true +``` + +This automatic cancellation only takes affect when one of the asynchronous properties is `await`ed. + +### `DataStreamRequest` Support + +`DataStreamRequest`, unlike the other request types, does not read a single value and complete. Instead, it continuously streams `Data` from the server to be processed through a handler. With Swift Concurrency, this callback API has been replaced with `StreamOf` values vended by `DataStreamTask`. `StreamOf` conforms to `AsyncSequence`, allowing the use of `for await` syntax to observe values as they're received by the stream. Unlike `DataTask` and `DownloadTask`, `DataStreamTask` doesn't vend asynchronous properties itself. Instead, it vends the streams that can be observed. + +```swift +let streamTask = AF.dataStreamRequest(...).streamTask() + +// Later... + +for await data in streamTask.streamingData() { + // Streams Stream values. a.k.a StreamOf> +} +``` + +This loop only ends when the `DataStreamRequest` completes, either through the server closing the connection or the `DataStreamRequest` being cancelled. If the loop is ended early by `break`ing out of it, the `DataStreamRequest` is canceled and no further values can be received. If the use of multiple observers without automatically cancellation is desired, you can pass `false` for the `automaticallyCancelling` parameter. + +```swift +let streamTask = AF.dataStreamRequest(...).streamTask() + +// Later... + +for await data in streamTask.streamingData(automaticallyCancelling: false) { + // Streams Stream values. a.k.a StreamOf> + if condition { break } // Stream ends but underlying `DataStreamRequest` is not cancelled and keeps receiving data. +} +``` + +One observer setting `automaticallyCancelling` to `false` does not affect other from the same `DataStreamRequest`, so if any other observer exits the request will still be cancelled. + +### Value Handlers + +Alamofire provides various handlers for internal values which are produced asynchronously, such as `Progress` values, `URLRequest`s and `URLSessionTask`s, as well as cURL descriptions of the request each time a new request is issued. Alamofire's concurrency support now exposes these handlers as `StreamOf` values that can be used to asynchronously observe the received values. For instance, if you wanted to print each cURL description produced by a request: + +```swift +let request = AF.request(...) + +// Later... + +for await description in request.cURLDescriptions() { + print(description) +} +``` + ## Network Reachability The `NetworkReachabilityManager` listens for changes in the reachability of hosts and addresses for both Cellular and WiFi network interfaces. diff --git a/Example/iOS Example.xcodeproj/project.pbxproj b/Example/iOS Example.xcodeproj/project.pbxproj index 52f8ada08..a4ca8c1e1 100644 --- a/Example/iOS Example.xcodeproj/project.pbxproj +++ b/Example/iOS Example.xcodeproj/project.pbxproj @@ -18,6 +18,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 31AB09122733010700986A70 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 31E4765F1C55DD5900968569 /* Alamofire.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 31293065263E17D600473CEA; + remoteInfo = "Alamofire watchOS Tests"; + }; 31E476691C55DD5900968569 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 31E4765F1C55DD5900968569 /* Alamofire.xcodeproj */; @@ -124,6 +131,7 @@ 31E476721C55DD5900968569 /* Alamofire.framework */, 31E476741C55DD5900968569 /* Alamofire tvOS Tests.xctest */, 31E476761C55DD5900968569 /* Alamofire.framework */, + 31AB09132733010700986A70 /* Alamofire watchOS Tests.xctest */, ); name = Products; sourceTree = ""; @@ -208,7 +216,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0720; - LastUpgradeCheck = 1250; + LastUpgradeCheck = 1320; ORGANIZATIONNAME = Alamofire; TargetAttributes = { F8111E0419A951050040E7D1 = { @@ -242,6 +250,13 @@ /* End PBXProject section */ /* Begin PBXReferenceProxy section */ + 31AB09132733010700986A70 /* Alamofire watchOS Tests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = "Alamofire watchOS Tests.xctest"; + remoteRef = 31AB09122733010700986A70 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; 31E4766A1C55DD5900968569 /* Alamofire.framework */ = { isa = PBXReferenceProxy; fileType = wrapper.framework; diff --git a/Source/Concurrency.swift b/Source/Concurrency.swift new file mode 100644 index 000000000..24843358e --- /dev/null +++ b/Source/Concurrency.swift @@ -0,0 +1,698 @@ +// +// Concurrency.swift +// +// Copyright (c) 2021 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if compiler(>=5.5.2) && canImport(_Concurrency) + +import Foundation + +// MARK: - Request Event Streams + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension Request { + /// Creates a `StreamOf` for the instance's upload progress. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func uploadProgress(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + uploadProgress(queue: .singleEventQueue) { progress in + continuation.yield(progress) + } + } + } + + /// Creates a `StreamOf` for the instance's download progress. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func downloadProgress(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + downloadProgress(queue: .singleEventQueue) { progress in + continuation.yield(progress) + } + } + } + + /// Creates a `StreamOf` for the `URLRequest`s produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func urlRequests(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + onURLRequestCreation(on: .singleEventQueue) { request in + continuation.yield(request) + } + } + } + + /// Creates a `StreamOf` for the `URLSessionTask`s produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func urlSessionTasks(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + onURLSessionTaskCreation(on: .singleEventQueue) { task in + continuation.yield(task) + } + } + } + + /// Creates a `StreamOf` for the cURL descriptions produced for the instance. + /// + /// - Parameter bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `StreamOf`. + public func cURLDescriptions(bufferingPolicy: StreamOf.BufferingPolicy = .unbounded) -> StreamOf { + stream(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + cURLDescription(on: .singleEventQueue) { description in + continuation.yield(description) + } + } + } + + private func stream(of type: T.Type = T.self, + bufferingPolicy: StreamOf.BufferingPolicy = .unbounded, + yielder: @escaping (StreamOf.Continuation) -> Void) -> StreamOf { + StreamOf(bufferingPolicy: bufferingPolicy) { [unowned self] continuation in + yielder(continuation) + // Must come after serializers run in order to catch retry progress. + onFinish { + continuation.finish() + } + } + } +} + +// MARK: - DataTask + +/// Value used to `await` a `DataResponse` and associated values. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DataTask { + /// `DataResponse` produced by the `DataRequest` and its response handler. + public var response: DataResponse { + get async { + if shouldAutomaticallyCancel { + return await withTaskCancellationHandler { + self.cancel() + } operation: { + await task.value + } + } else { + return await task.value + } + } + } + + /// `Result` of any response serialization performed for the `response`. + public var result: Result { + get async { await response.result } + } + + /// `Value` returned by the `response`. + public var value: Value { + get async throws { + try await result.get() + } + } + + private let request: DataRequest + private let task: Task, Never> + private let shouldAutomaticallyCancel: Bool + + fileprivate init(request: DataRequest, task: Task, Never>, shouldAutomaticallyCancel: Bool) { + self.request = request + self.task = task + self.shouldAutomaticallyCancel = shouldAutomaticallyCancel + } + + /// Cancel the underlying `DataRequest` and `Task`. + public func cancel() { + task.cancel() + } + + /// Resume the underlying `DataRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DataRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DataRequest { + /// Creates a `DataTask` to `await` a `Data` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion. + /// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization of a `Decodable` value. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingDecodable(_ type: Value.Type = Value.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization of a `String` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case + /// the encoding will be determined from the server response, falling back to the + /// default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DataTask`. + public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DataTask { + serializingResponse(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DataTask` to `await` serialization using the provided `ResponseSerializer` instance. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DataTask`. + public func serializingResponse(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DataTask { + dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + /// Creates a `DataTask` to `await` serialization using the provided `DataResponseSerializerProtocol` instance. + /// + /// - Parameters: + /// - serializer: `DataResponseSerializerProtocol` responsible for serializing the request, + /// response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DataTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DataTask`. + public func serializingResponse(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DataTask { + dataTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + private func dataTask(automaticallyCancelling shouldAutomaticallyCancel: Bool, + forResponse onResponse: @escaping (@escaping (DataResponse) -> Void) -> Void) + -> DataTask { + let task = Task { + await withTaskCancellationHandler { + self.cancel() + } operation: { + await withCheckedContinuation { continuation in + onResponse { + continuation.resume(returning: $0) + } + } + } + } + + return DataTask(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel) + } +} + +// MARK: - DownloadTask + +/// Value used to `await` a `DownloadResponse` and associated values. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DownloadTask { + /// `DownloadResponse` produced by the `DownloadRequest` and its response handler. + public var response: DownloadResponse { + get async { + if shouldAutomaticallyCancel { + return await withTaskCancellationHandler { + self.cancel() + } operation: { + await task.value + } + } else { + return await task.value + } + } + } + + /// `Result` of any response serialization performed for the `response`. + public var result: Result { + get async { await response.result } + } + + /// `Value` returned by the `response`. + public var value: Value { + get async throws { + try await result.get() + } + } + + private let task: Task, Never> + private let request: DownloadRequest + private let shouldAutomaticallyCancel: Bool + + fileprivate init(request: DownloadRequest, task: Task, Never>, shouldAutomaticallyCancel: Bool) { + self.request = request + self.task = task + self.shouldAutomaticallyCancel = shouldAutomaticallyCancel + } + + /// Cancel the underlying `DownloadRequest` and `Task`. + public func cancel() { + task.cancel() + } + + /// Resume the underlying `DownloadRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DownloadRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DownloadRequest { + /// Creates a `DownloadTask` to `await` a `Data` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before completion. + /// - emptyResponseCodes: HTTP response codes for which empty responses are allowed. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DataResponseSerializer.defaultDataPreprocessor, + emptyResponseCodes: Set = DataResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DataResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: DataResponseSerializer(dataPreprocessor: dataPreprocessor, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization of a `Decodable` value. + /// + /// - Note: This serializer reads the entire response into memory before parsing. + /// + /// - Parameters: + /// - type: `Decodable` type to decode from response data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the serializer. + /// `PassthroughPreprocessor()` by default. + /// - decoder: `DataDecoder` to use to decode the response. `JSONDecoder()` by default. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDecodable(_ type: Value.Type = Value.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = DecodableResponseSerializer.defaultDataPreprocessor, + decoder: DataDecoder = JSONDecoder(), + emptyResponseCodes: Set = DecodableResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = DecodableResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor, + decoder: decoder, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization of the downloaded file's `URL` on disk. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownloadedFileURL() -> DownloadTask { + serializingDownload(using: URLResponseSerializer()) + } + + /// Creates a `DownloadTask` to `await` serialization of a `String` value. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// - dataPreprocessor: `DataPreprocessor` which processes the received `Data` before calling the + /// serializer. `PassthroughPreprocessor()` by default. + /// - encoding: `String.Encoding` to use during serialization. Defaults to `nil`, in which case + /// the encoding will be determined from the server response, falling back to the + /// default HTTP character set, `ISO-8859-1`. + /// - emptyResponseCodes: HTTP status codes for which empty responses are always valid. `[204, 205]` by default. + /// - emptyRequestMethods: `HTTPMethod`s for which empty responses are always valid. `[.head]` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingString(automaticallyCancelling shouldAutomaticallyCancel: Bool = false, + dataPreprocessor: DataPreprocessor = StringResponseSerializer.defaultDataPreprocessor, + encoding: String.Encoding? = nil, + emptyResponseCodes: Set = StringResponseSerializer.defaultEmptyResponseCodes, + emptyRequestMethods: Set = StringResponseSerializer.defaultEmptyRequestMethods) -> DownloadTask { + serializingDownload(using: StringResponseSerializer(dataPreprocessor: dataPreprocessor, + encoding: encoding, + emptyResponseCodes: emptyResponseCodes, + emptyRequestMethods: emptyRequestMethods), + automaticallyCancelling: shouldAutomaticallyCancel) + } + + /// Creates a `DownloadTask` to `await` serialization using the provided `ResponseSerializer` instance. + /// + /// - Parameters: + /// - serializer: `ResponseSerializer` responsible for serializing the request, response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownload(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DownloadTask { + downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + /// Creates a `DownloadTask` to `await` serialization using the provided `DownloadResponseSerializerProtocol` + /// instance. + /// + /// - Parameters: + /// - serializer: `DownloadResponseSerializerProtocol` responsible for serializing the request, + /// response, and data. + /// - shouldAutomaticallyCancel: `Bool` determining whether or not the request should be cancelled when the + /// enclosing async context is cancelled. Only applies to `DownloadTask`'s async + /// properties. `false` by default. + /// + /// - Returns: The `DownloadTask`. + public func serializingDownload(using serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = false) + -> DownloadTask { + downloadTask(automaticallyCancelling: shouldAutomaticallyCancel) { + self.response(queue: .singleEventQueue, + responseSerializer: serializer, + completionHandler: $0) + } + } + + private func downloadTask(automaticallyCancelling shouldAutomaticallyCancel: Bool, + forResponse onResponse: @escaping (@escaping (DownloadResponse) -> Void) -> Void) + -> DownloadTask { + let task = Task { + await withTaskCancellationHandler { + self.cancel() + } operation: { + await withCheckedContinuation { continuation in + onResponse { + continuation.resume(returning: $0) + } + } + } + } + + return DownloadTask(request: self, task: task, shouldAutomaticallyCancel: shouldAutomaticallyCancel) + } +} + +// MARK: - DataStreamTask + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct DataStreamTask { + // Type of created streams. + public typealias Stream = StreamOf> + + private let request: DataStreamRequest + + fileprivate init(request: DataStreamRequest) { + self.request = request + } + + /// Creates a `Stream` of `Data` values from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingData(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream.BufferingPolicy = .unbounded) -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStream(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream) + } + } + + /// Creates a `Stream` of `UTF-8` `String`s from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: ` BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// - Returns: + public func streamingStrings(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, bufferingPolicy: Stream.BufferingPolicy = .unbounded) -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStreamString(on: .streamCompletionQueue(forRequestID: request.id), stream: onStream) + } + } + + /// Creates a `Stream` of `Decodable` values from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - type: `Decodable` type to be serialized from stream payloads. + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingDecodables(_ type: T.Type = T.self, + automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded) + -> Stream where T: Decodable { + streamingResponses(serializedUsing: DecodableStreamSerializer(), + automaticallyCancelling: shouldAutomaticallyCancel, + bufferingPolicy: bufferingPolicy) + } + + /// Creates a `Stream` of values using the provided `DataStreamSerializer` from the underlying `DataStreamRequest`. + /// + /// - Parameters: + /// - serializer: `DataStreamSerializer` to use to serialize incoming `Data`. + /// - shouldAutomaticallyCancel: `Bool` indicating whether the underlying `DataStreamRequest` should be canceled + /// which observation of the stream stops. `true` by default. + /// - bufferingPolicy: `BufferingPolicy` that determines the stream's buffering behavior.`.unbounded` by default. + /// + /// - Returns: The `Stream`. + public func streamingResponses(serializedUsing serializer: Serializer, + automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded) + -> Stream { + createStream(automaticallyCancelling: shouldAutomaticallyCancel, bufferingPolicy: bufferingPolicy) { onStream in + self.request.responseStream(using: serializer, + on: .streamCompletionQueue(forRequestID: request.id), + stream: onStream) + } + } + + private func createStream(automaticallyCancelling shouldAutomaticallyCancel: Bool = true, + bufferingPolicy: Stream.BufferingPolicy = .unbounded, + forResponse onResponse: @escaping (@escaping (DataStreamRequest.Stream) -> Void) -> Void) + -> Stream { + StreamOf(bufferingPolicy: bufferingPolicy) { + guard shouldAutomaticallyCancel, + request.isInitialized || request.isResumed || request.isSuspended else { return } + + cancel() + } builder: { continuation in + onResponse { stream in + continuation.yield(stream) + if case .complete = stream.event { + continuation.finish() + } + } + } + } + + /// Cancel the underlying `DataStreamRequest`. + public func cancel() { + request.cancel() + } + + /// Resume the underlying `DataStreamRequest`. + public func resume() { + request.resume() + } + + /// Suspend the underlying `DataStreamRequest`. + public func suspend() { + request.suspend() + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension DataStreamRequest { + /// Creates a `DataStreamTask` used to `await` streams of serialized values. + /// + /// - Returns: The `DataStreamTask`. + public func streamTask() -> DataStreamTask { + DataStreamTask(request: self) + } +} + +extension DispatchQueue { + fileprivate static let singleEventQueue = DispatchQueue(label: "org.alamofire.concurrencySingleEventQueue", + attributes: .concurrent) + + fileprivate static func streamCompletionQueue(forRequestID id: UUID) -> DispatchQueue { + DispatchQueue(label: "org.alamofire.concurrencyStreamCompletionQueue-\(id)", target: .singleEventQueue) + } +} + +/// An asynchronous sequence generated from an underlying `AsyncStream`. Only produced by Alamofire. +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +public struct StreamOf: AsyncSequence { + public typealias AsyncIterator = Iterator + public typealias BufferingPolicy = AsyncStream.Continuation.BufferingPolicy + fileprivate typealias Continuation = AsyncStream.Continuation + + private let bufferingPolicy: BufferingPolicy + private let onTermination: (() -> Void)? + private let builder: (Continuation) -> Void + + fileprivate init(bufferingPolicy: BufferingPolicy = .unbounded, + onTermination: (() -> Void)? = nil, + builder: @escaping (Continuation) -> Void) { + self.bufferingPolicy = bufferingPolicy + self.onTermination = onTermination + self.builder = builder + } + + public func makeAsyncIterator() -> Iterator { + var continuation: AsyncStream.Continuation? + let stream = AsyncStream { innerContinuation in + continuation = innerContinuation + builder(innerContinuation) + } + + return Iterator(iterator: stream.makeAsyncIterator()) { + continuation?.finish() + self.onTermination?() + } + } + + public struct Iterator: AsyncIteratorProtocol { + private final class Token { + private let onDeinit: () -> Void + + init(onDeinit: @escaping () -> Void) { + self.onDeinit = onDeinit + } + + deinit { + onDeinit() + } + } + + private var iterator: AsyncStream.AsyncIterator + private let token: Token + + init(iterator: AsyncStream.AsyncIterator, onCancellation: @escaping () -> Void) { + self.iterator = iterator + token = Token(onDeinit: onCancellation) + } + + public mutating func next() async -> Element? { + await iterator.next() + } + } +} + +#endif diff --git a/Source/HTTPHeaders.swift b/Source/HTTPHeaders.swift index 17c4784c7..cdbdbc6b5 100644 --- a/Source/HTTPHeaders.swift +++ b/Source/HTTPHeaders.swift @@ -360,9 +360,7 @@ extension HTTPHeader { /// `preferredLanguages`. /// /// See the [Accept-Language HTTP header documentation](https://tools.ietf.org/html/rfc7231#section-5.3.5). - public static let defaultAcceptLanguage: HTTPHeader = { - .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded()) - }() + public static let defaultAcceptLanguage: HTTPHeader = .acceptLanguage(Locale.preferredLanguages.prefix(6).qualityEncoded()) /// Returns Alamofire's default `User-Agent` header. /// diff --git a/Source/Protected.swift b/Source/Protected.swift index bb414386c..2c056fa58 100644 --- a/Source/Protected.swift +++ b/Source/Protected.swift @@ -136,46 +136,6 @@ final class Protected { } } -extension Protected where T: RangeReplaceableCollection { - /// Adds a new element to the end of this protected collection. - /// - /// - Parameter newElement: The `Element` to append. - func append(_ newElement: T.Element) { - write { (ward: inout T) in - ward.append(newElement) - } - } - - /// Adds the elements of a sequence to the end of this protected collection. - /// - /// - Parameter newElements: The `Sequence` to append. - func append(contentsOf newElements: S) where S.Element == T.Element { - write { (ward: inout T) in - ward.append(contentsOf: newElements) - } - } - - /// Add the elements of a collection to the end of the protected collection. - /// - /// - Parameter newElements: The `Collection` to append. - func append(contentsOf newElements: C) where C.Element == T.Element { - write { (ward: inout T) in - ward.append(contentsOf: newElements) - } - } -} - -extension Protected where T == Data? { - /// Adds the contents of a `Data` value to the end of the protected `Data`. - /// - /// - Parameter data: The `Data` to be appended. - func append(_ data: Data) { - write { (ward: inout T) in - ward?.append(data) - } - } -} - extension Protected where T == Request.MutableState { /// Attempts to transition to the passed `State`. /// diff --git a/Source/Request.swift b/Source/Request.swift index 572b6d30c..fdbdf11d0 100644 --- a/Source/Request.swift +++ b/Source/Request.swift @@ -120,6 +120,8 @@ public class Request { /// Whether the instance has had `finish()` called and is running the serializers. Should be replaced with a /// representation in the state machine in the future. var isFinishing = false + /// Actions to run when requests are finished. Use for concurrency support. + var finishHandlers: [() -> Void] = [] } /// Protected `MutableState` value that provides thread-safe access to state values. @@ -920,10 +922,25 @@ public class Request { // MARK: Cleanup + /// Adds a `finishHandler` closure to be called when the request completes. + /// + /// - Parameter closure: Closure to be called when the request finishes. + func onFinish(perform finishHandler: @escaping () -> Void) { + guard !isFinished else { finishHandler(); return } + + $mutableState.write { state in + state.finishHandlers.append(finishHandler) + } + } + /// Final cleanup step executed when the instance finishes response serialization. func cleanup() { delegate?.cleanup(after: self) - // No-op: override in subclass + let handlers = $mutableState.finishHandlers + handlers.forEach { $0() } + $mutableState.write { state in + state.finishHandlers.removeAll() + } } } diff --git a/Source/ResponseSerialization.swift b/Source/ResponseSerialization.swift index 3432d9420..30973642d 100644 --- a/Source/ResponseSerialization.swift +++ b/Source/ResponseSerialization.swift @@ -1110,11 +1110,17 @@ public struct DecodableStreamSerializer: DataStreamSerializer { /// `DataStreamSerializer` which performs no serialization on incoming `Data`. public struct PassthroughStreamSerializer: DataStreamSerializer { + /// Creates an instance. + public init() {} + public func serialize(_ data: Data) throws -> Data { data } } /// `DataStreamSerializer` which serializes incoming stream `Data` into `UTF8`-decoded `String` values. public struct StringStreamSerializer: DataStreamSerializer { + /// Creates an instance. + public init() {} + public func serialize(_ data: Data) throws -> String { String(decoding: data, as: UTF8.self) } diff --git a/Tests/BaseTestCase.swift b/Tests/BaseTestCase.swift index fe54ffc50..1eeafe7a7 100644 --- a/Tests/BaseTestCase.swift +++ b/Tests/BaseTestCase.swift @@ -37,13 +37,21 @@ class BaseTestCase: XCTestCase { testDirectoryURL.appendingPathComponent(UUID().uuidString) } + private var session: Session? + override func setUp() { + FileManager.createDirectory(at: testDirectoryURL) + super.setUp() + } + override func tearDown() { + session = nil FileManager.removeAllItemsInsideDirectory(at: testDirectoryURL) - FileManager.createDirectory(at: testDirectoryURL) clearCredentials() clearCookies() + + super.tearDown() } func clearCookies(for storage: HTTPCookieStorage = .shared) { @@ -63,6 +71,12 @@ class BaseTestCase: XCTestCase { return bundle.url(forResource: fileName, withExtension: ext)! } + func stored(_ session: Session) -> Session { + self.session = session + + return session + } + /// Runs assertions on a particular `DispatchQueue`. /// /// - Parameters: diff --git a/Tests/ConcurrencyTests.swift b/Tests/ConcurrencyTests.swift new file mode 100644 index 000000000..656142615 --- /dev/null +++ b/Tests/ConcurrencyTests.swift @@ -0,0 +1,526 @@ +// +// ConcurrencyTests.swift +// +// Copyright (c) 2021 Alamofire Software Foundation (http://alamofire.org/) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +// + +#if compiler(>=5.5.2) && canImport(_Concurrency) + +import Alamofire +import XCTest + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +final class DataRequestConcurrencyTests: BaseTestCase { + func testThatDataTaskSerializesResponseUsingSerializer() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.request(.get) + .serializingResponse(using: .data) + .value + + // Then + XCTAssertNotNil(value) + } + + func testThatDataTaskSerializesDecodable() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.request(.get).serializingDecodable(TestResponse.self).value + + // Then + XCTAssertNotNil(value) + } + + func testThatDataTaskSerializesString() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.request(.get).serializingString().value + + // Then + XCTAssertNotNil(value) + } + + func testThatDataTaskSerializesData() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.request(.get).serializingData().value + + // Then + XCTAssertNotNil(value) + } + + func testThatDataTaskProducesResult() async { + // Given + let session = stored(Session()) + + // When + let result = await session.request(.get).serializingDecodable(TestResponse.self).result + + // Then + XCTAssertNotNil(result.success) + } + + func testThatDataTaskProducesValue() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.request(.get).serializingDecodable(TestResponse.self).value + + // Then + XCTAssertNotNil(value) + } + + func testThatDataTaskProperlySupportsConcurrentRequests() async { + // Given + let session = stored(Session()) + + // When + async let first = session.request(.get).serializingDecodable(TestResponse.self).response + async let second = session.request(.get).serializingDecodable(TestResponse.self).response + async let third = session.request(.get).serializingDecodable(TestResponse.self).response + + // Then + let responses = await [first, second, third] + XCTAssertEqual(responses.count, 3) + XCTAssertTrue(responses.allSatisfy(\.result.isSuccess)) + } + + func testThatDataTaskCancellationCancelsRequest() async { + // Given + let session = stored(Session()) + let request = session.request(.get) + let task = request.serializingDecodable(TestResponse.self) + + // When + task.cancel() + let response = await task.response + + // Then + XCTAssertTrue(response.error?.isExplicitlyCancelledError == true) + XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.") + } + + func testThatDataTaskIsAutomaticallyCancelledInTaskWhenEnabled() async { + // Given + let session = stored(Session()) + let request = session.request(.get) + + // When + let task = Task { + await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).result + } + + task.cancel() + let result = await task.value + + // Then + XCTAssertTrue(result.failure?.isExplicitlyCancelledError == true) + XCTAssertTrue(task.isCancelled, "Task should be cancelled.") + XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.") + } + + func testThatDataTaskIsAutomaticallyCancelledInTaskGroupWhenEnabled() async { + // Given + let session = stored(Session()) + let request = session.request(.get) + + // When + let task = Task { + await withTaskGroup(of: Result.self) { group -> Result in + group.addTask { + await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).result + } + + return await group.first(where: { _ in true })! + } + } + + task.cancel() + let result = await task.value + + // Then + XCTAssertTrue(result.failure?.isExplicitlyCancelledError == true) + XCTAssertTrue(task.isCancelled, "Task should be cancelled.") + XCTAssertTrue(request.isCancelled, "Underlying DataRequest should be cancelled.") + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +final class DownloadConcurrencyTests: BaseTestCase { + func testThatDownloadTaskSerializesResponseFromSerializer() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.download(.get) + .serializingDownload(using: .data) + .value + + // Then + XCTAssertNotNil(value) + } + + func testThatDownloadTaskSerializesDecodable() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.download(.get).serializingDecodable(TestResponse.self).value + + // Then + XCTAssertNotNil(value) + } + + func testThatDownloadTaskSerializesString() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.download(.get).serializingString().value + + // Then + XCTAssertNotNil(value) + } + + func testThatDownloadTaskSerializesData() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.download(.get).serializingData().value + + // Then + XCTAssertNotNil(value) + } + + func testThatDownloadTaskSerializesURL() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.download(.get).serializingDownloadedFileURL().value + + // Then + XCTAssertNotNil(value) + } + + func testThatDownloadTaskProducesResult() async { + // Given + let session = stored(Session()) + + // When + let result = await session.download(.get).serializingDecodable(TestResponse.self).result + + // Then + XCTAssertNotNil(result.success) + } + + func testThatDownloadTaskProducesValue() async throws { + // Given + let session = stored(Session()) + + // When + let value = try await session.download(.get).serializingDecodable(TestResponse.self).value + + // Then + XCTAssertNotNil(value) + } + + func testThatDownloadTaskProperlySupportsConcurrentRequests() async { + // Given + let session = stored(Session()) + + // When + async let first = session.download(.get).serializingDecodable(TestResponse.self).response + async let second = session.download(.get).serializingDecodable(TestResponse.self).response + async let third = session.download(.get).serializingDecodable(TestResponse.self).response + + // Then + let responses = await [first, second, third] + XCTAssertEqual(responses.count, 3) + XCTAssertTrue(responses.allSatisfy(\.result.isSuccess)) + } + + func testThatDownloadTaskCancelsRequest() async { + // Given + let session = stored(Session()) + let request = session.download(.get) + let task = request.serializingDecodable(TestResponse.self) + + // When + task.cancel() + let response = await task.response + + // Then + XCTAssertTrue(response.error?.isExplicitlyCancelledError == true) + } + + func testThatDownloadTaskIsAutomaticallyCancelledInTaskWhenEnabled() async { + // Given + let session = stored(Session()) + let request = session.download(.get) + + // When + let task = Task { + await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).result + } + + task.cancel() + let result = await task.value + + // Then + XCTAssertTrue(result.failure?.isExplicitlyCancelledError == true) + XCTAssertTrue(task.isCancelled, "Task should be cancelled.") + XCTAssertTrue(request.isCancelled, "Underlying DownloadRequest should be cancelled.") + } + + func testThatDownloadTaskIsAutomaticallyCancelledInTaskGroupWhenEnabled() async { + // Given + let session = stored(Session()) + let request = session.download(.get) + + // When + let task = Task { + await withTaskGroup(of: Result.self) { group -> Result in + group.addTask { + await request.serializingDecodable(TestResponse.self, automaticallyCancelling: true).result + } + + return await group.first(where: { _ in true })! + } + } + + task.cancel() + let result = await task.value + + // Then + XCTAssertTrue(result.failure?.isExplicitlyCancelledError == true) + XCTAssertTrue(task.isCancelled, "Task should be cancelled.") + XCTAssertTrue(request.isCancelled, "Underlying DownloadRequest should be cancelled.") + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +final class DataStreamConcurrencyTests: BaseTestCase { + func testThatDataStreamTaskCanStreamData() async { + // Given + let session = stored(Session()) + + // When + let task = session.streamRequest(.payloads(2)).streamTask() + var datas: [Data] = [] + + for await data in task.streamingData().compactMap(\.value) { + datas.append(data) + } + + // Then + XCTAssertEqual(datas.count, 2) + } + + func testThatDataStreamTaskCanStreamStrings() async { + // Given + let session = stored(Session()) + + // When + let task = session.streamRequest(.payloads(2)).streamTask() + var strings: [String] = [] + + for await string in task.streamingStrings().compactMap(\.value) { + strings.append(string) + } + + // Then + XCTAssertEqual(strings.count, 2) + } + + func testThatDataStreamTaskCanStreamDecodable() async { + // Given + let session = stored(Session()) + + // When + let task = session.streamRequest(.payloads(2)).streamTask() + let stream = task.streamingResponses(serializedUsing: DecodableStreamSerializer()) + var responses: [TestResponse] = [] + + for await response in stream.compactMap(\.value) { + responses.append(response) + } + + // Then + XCTAssertEqual(responses.count, 2) + } + + func testThatDataStreamTaskCanBeDirectlyCancelled() async { + // Given + let session = stored(Session()) + + // When + let expectedPayloads = 10 + let request = session.streamRequest(.payloads(expectedPayloads)) + let task = request.streamTask() + var datas: [Data] = [] + + for await data in task.streamingData().compactMap(\.value) { + datas.append(data) + if datas.count == 1 { + task.cancel() + } + } + + // Then + XCTAssertTrue(request.isCancelled) + XCTAssertTrue(datas.count == 1) + } + + func testThatDataStreamTaskIsCancelledByCancellingIteration() async { + // Given + let session = stored(Session()) + + // When + let expectedPayloads = 10 + let request = session.streamRequest(.payloads(expectedPayloads)) + let task = request.streamTask() + var datas: [Data] = [] + + for await data in task.streamingData().compactMap(\.value) { + datas.append(data) + if datas.count == 1 { + break + } + } + + // Then + XCTAssertTrue(request.isCancelled) + XCTAssertTrue(datas.count == 1) + } + + func testThatDataStreamTaskCanBeImplicitlyCancelled() async { + // Given + let session = stored(Session()) + + // When + let expectedPayloads = 10 + let request = session.streamRequest(.payloads(expectedPayloads)) + let task = Task<[Data], Never> { + var datas: [Data] = [] + + for await data in request.streamTask().streamingData().compactMap(\.value) { + datas.append(data) + } + + return datas + } + task.cancel() + let datas: [Data] = await task.value + + // Then + XCTAssertTrue(request.isCancelled) + XCTAssertTrue(datas.isEmpty) + } + + func testThatDataStreamTaskCanBeCancelledAfterStreamTurnsOffAutomaticCancellation() async { + // Given + let session = stored(Session()) + + // When + let expectedPayloads = 10 + let request = session.streamRequest(.payloads(expectedPayloads)) + let task = Task<[Data], Never> { + var datas: [Data] = [] + let streamTask = request.streamTask() + + for await data in streamTask.streamingData(automaticallyCancelling: false).compactMap(\.value) { + datas.append(data) + break + } + + for await data in streamTask.streamingData().compactMap(\.value) { + datas.append(data) + break + } + + return datas + } + let datas: [Data] = await task.value + + // Then + XCTAssertTrue(request.isCancelled) + XCTAssertTrue(datas.count == 2) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +final class ClosureAPIConcurrencyTests: BaseTestCase { + func testThatDownloadProgressStreamReturnsProgress() async { + // Given + let session = stored(Session()) + + // When + let request = session.request(.get) + async let uploadProgress = request.uploadProgress().collect() + async let downloadProgress = request.downloadProgress().collect() + async let requests = request.urlRequests().collect() + async let tasks = request.urlSessionTasks().collect() + async let descriptions = request.cURLDescriptions().collect() + async let response = request.serializingDecodable(TestResponse.self).response + + let values: (uploadProgresses: [Progress], + downloadProgresses: [Progress], + requests: [URLRequest], + tasks: [URLSessionTask], + descriptions: [String], + response: AFDataResponse) + values = await(uploadProgress, downloadProgress, requests, tasks, descriptions, response) + + // Then + XCTAssertTrue(values.uploadProgresses.isEmpty) + XCTAssertNotNil(values.downloadProgresses.last) + XCTAssertTrue(values.downloadProgresses.last?.isFinished == true) + XCTAssertNotNil(values.requests.last) + XCTAssertNotNil(values.tasks.last) + XCTAssertNotNil(values.descriptions.last) + XCTAssertTrue(values.response.result.isSuccess) + } +} + +@available(macOS 10.15, iOS 13, tvOS 13, watchOS 6, *) +extension AsyncSequence { + func collect() async rethrows -> [Element] { + var elements: [Element] = [] + for try await element in self { + elements.append(element) + } + + return elements + } +} + +#endif diff --git a/Tests/NSLoggingEventMonitor.swift b/Tests/NSLoggingEventMonitor.swift index 6e8a2aa3e..680a48939 100644 --- a/Tests/NSLoggingEventMonitor.swift +++ b/Tests/NSLoggingEventMonitor.swift @@ -31,11 +31,11 @@ public final class NSLoggingEventMonitor: EventMonitor { public init() {} public func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { - NSLog("URLSession: \(session), didBecomeInvalidWithError: \(error?.localizedDescription ?? "None")") + NSLog("%@", "URLSession: \(session), didBecomeInvalidWithError: \(error?.localizedDescription ?? "None")") } public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) { - NSLog("URLSession: \(session), task: \(task), didReceiveChallenge: \(challenge)") + NSLog("%@", "URLSession: \(session), task: \(task), didReceiveChallenge: \(challenge)") } public func urlSession(_ session: URLSession, @@ -43,47 +43,47 @@ public final class NSLoggingEventMonitor: EventMonitor { didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) { - NSLog("URLSession: \(session), task: \(task), didSendBodyData: \(bytesSent), totalBytesSent: \(totalBytesSent), totalBytesExpectedToSent: \(totalBytesExpectedToSend)") + NSLog("%@", "URLSession: \(session), task: \(task), didSendBodyData: \(bytesSent), totalBytesSent: \(totalBytesSent), totalBytesExpectedToSent: \(totalBytesExpectedToSend)") } public func urlSession(_ session: URLSession, taskNeedsNewBodyStream task: URLSessionTask) { - NSLog("URLSession: \(session), taskNeedsNewBodyStream: \(task)") + NSLog("%@", "URLSession: \(session), taskNeedsNewBodyStream: \(task)") } public func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) { - NSLog("URLSession: \(session), task: \(task), willPerformHTTPRedirection: \(response), newRequest: \(request)") + NSLog("%@", "URLSession: \(session), task: \(task), willPerformHTTPRedirection: \(response), newRequest: \(request)") } public func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - NSLog("URLSession: \(session), task: \(task), didFinishCollecting: \(metrics)") + NSLog("%@", "URLSession: \(session), task: \(task), didFinishCollecting: \(metrics)") } public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { - NSLog("URLSession: \(session), task: \(task), didCompleteWithError: \(error?.localizedDescription ?? "None")") + NSLog("%@", "URLSession: \(session), task: \(task), didCompleteWithError: \(error?.localizedDescription ?? "None")") } public func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) { - NSLog("URLSession: \(session), taskIsWaitingForConnectivity: \(task)") + NSLog("%@", "URLSession: \(session), taskIsWaitingForConnectivity: \(task)") } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { - NSLog("URLSession: \(session), dataTask: \(dataTask), didReceiveDataOfLength: \(data.count)") + NSLog("%@", "URLSession: \(session), dataTask: \(dataTask), didReceiveDataOfLength: \(data.count)") } public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) { - NSLog("URLSession: \(session), dataTask: \(dataTask), willCacheResponse: \(proposedResponse)") + NSLog("%@", "URLSession: \(session), dataTask: \(dataTask), willCacheResponse: \(proposedResponse)") } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) { - NSLog("URLSession: \(session), downloadTask: \(downloadTask), didResumeAtOffset: \(fileOffset), expectedTotalBytes: \(expectedTotalBytes)") + NSLog("%@", "URLSession: \(session), downloadTask: \(downloadTask), didResumeAtOffset: \(fileOffset), expectedTotalBytes: \(expectedTotalBytes)") } public func urlSession(_ session: URLSession, @@ -91,132 +91,132 @@ public final class NSLoggingEventMonitor: EventMonitor { didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) { - NSLog("URLSession: \(session), downloadTask: \(downloadTask), didWriteData bytesWritten: \(bytesWritten), totalBytesWritten: \(totalBytesWritten), totalBytesExpectedToWrite: \(totalBytesExpectedToWrite)") + NSLog("%@", "URLSession: \(session), downloadTask: \(downloadTask), didWriteData bytesWritten: \(bytesWritten), totalBytesWritten: \(totalBytesWritten), totalBytesExpectedToWrite: \(totalBytesExpectedToWrite)") } public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { - NSLog("URLSession: \(session), downloadTask: \(downloadTask), didFinishDownloadingTo: \(location)") + NSLog("%@", "URLSession: \(session), downloadTask: \(downloadTask), didFinishDownloadingTo: \(location)") } public func request(_ request: Request, didCreateInitialURLRequest urlRequest: URLRequest) { - NSLog("Request: \(request) didCreateInitialURLRequest: \(urlRequest)") + NSLog("%@", "Request: \(request) didCreateInitialURLRequest: \(urlRequest)") } public func request(_ request: Request, didFailToCreateURLRequestWithError error: Error) { - NSLog("Request: \(request) didFailToCreateURLRequestWithError: \(error)") + NSLog("%@", "Request: \(request) didFailToCreateURLRequestWithError: \(error)") } public func request(_ request: Request, didAdaptInitialRequest initialRequest: URLRequest, to adaptedRequest: URLRequest) { - NSLog("Request: \(request) didAdaptInitialRequest \(initialRequest) to \(adaptedRequest)") + NSLog("%@", "Request: \(request) didAdaptInitialRequest \(initialRequest) to \(adaptedRequest)") } public func request(_ request: Request, didFailToAdaptURLRequest initialRequest: URLRequest, withError error: Error) { - NSLog("Request: \(request) didFailToAdaptURLRequest \(initialRequest) withError \(error)") + NSLog("%@", "Request: \(request) didFailToAdaptURLRequest \(initialRequest) withError \(error)") } public func request(_ request: Request, didCreateURLRequest urlRequest: URLRequest) { - NSLog("Request: \(request) didCreateURLRequest: \(urlRequest)") + NSLog("%@", "Request: \(request) didCreateURLRequest: \(urlRequest)") } public func request(_ request: Request, didCreateTask task: URLSessionTask) { - NSLog("Request: \(request) didCreateTask \(task)") + NSLog("%@", "Request: \(request) didCreateTask \(task)") } public func request(_ request: Request, didGatherMetrics metrics: URLSessionTaskMetrics) { - NSLog("Request: \(request) didGatherMetrics \(metrics)") + NSLog("%@", "Request: \(request) didGatherMetrics \(metrics)") } public func request(_ request: Request, didFailTask task: URLSessionTask, earlyWithError error: Error) { - NSLog("Request: \(request) didFailTask \(task) earlyWithError \(error)") + NSLog("%@", "Request: \(request) didFailTask \(task) earlyWithError \(error)") } public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: Error?) { - NSLog("Request: \(request) didCompleteTask \(task) withError: \(error?.localizedDescription ?? "None")") + NSLog("%@", "Request: \(request) didCompleteTask \(task) withError: \(error?.localizedDescription ?? "None")") } public func requestDidFinish(_ request: Request) { - NSLog("Request: \(request) didFinish") + NSLog("%@", "Request: \(request) didFinish") } public func requestDidResume(_ request: Request) { - NSLog("Request: \(request) didResume") + NSLog("%@", "Request: \(request) didResume") } public func request(_ request: Request, didResumeTask task: URLSessionTask) { - NSLog("Request: \(request) didResumeTask: \(task)") + NSLog("%@", "Request: \(request) didResumeTask: \(task)") } public func requestDidSuspend(_ request: Request) { - NSLog("Request: \(request) didSuspend") + NSLog("%@", "Request: \(request) didSuspend") } public func request(_ request: Request, didSuspendTask task: URLSessionTask) { - NSLog("Request: \(request) didSuspendTask: \(task)") + NSLog("%@", "Request: \(request) didSuspendTask: \(task)") } public func requestDidCancel(_ request: Request) { - NSLog("Request: \(request) didCancel") + NSLog("%@", "Request: \(request) didCancel") } public func request(_ request: Request, didCancelTask task: URLSessionTask) { - NSLog("Request: \(request) didCancelTask: \(task)") + NSLog("%@", "Request: \(request) didCancelTask: \(task)") } public func request(_ request: DataRequest, didParseResponse response: DataResponse) { - NSLog("Request: \(request), didParseResponse: \(response)") + NSLog("%@", "Request: \(request), didParseResponse: \(response)") } public func request(_ request: DataRequest, didParseResponse response: DataResponse) { - NSLog("Request: \(request), didParseResponse: \(response)") + NSLog("%@", "Request: \(request), didParseResponse: \(response)") } public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { - NSLog("Request: \(request), didParseResponse: \(response)") + NSLog("%@", "Request: \(request), didParseResponse: \(response)") } public func request(_ request: DownloadRequest, didParseResponse response: DownloadResponse) { - NSLog("Request: \(request), didParseResponse: \(response)") + NSLog("%@", "Request: \(request), didParseResponse: \(response)") } public func requestIsRetrying(_ request: Request) { - NSLog("Request: \(request), isRetrying") + NSLog("%@", "Request: \(request), isRetrying") } public func request(_ request: DataRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, data: Data?, withResult result: Request.ValidationResult) { - NSLog("Request: \(request), didValidateRequestWithResult: \(result)") + NSLog("%@", "Request: \(request), didValidateRequestWithResult: \(result)") } public func request(_ request: DataStreamRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, withResult result: Request.ValidationResult) { - NSLog("Request: \(request), didValidateRequestWithResult: \(result)") + NSLog("%@", "Request: \(request), didValidateRequestWithResult: \(result)") } public func request(_ request: DataStreamRequest, didParseStream result: Result) { - NSLog("Request: \(request), didParseStreamWithResult: \(result)") + NSLog("%@", "Request: \(request), didParseStreamWithResult: \(result)") } public func request(_ request: UploadRequest, didCreateUploadable uploadable: UploadRequest.Uploadable) { - NSLog("Request: \(request), didCreateUploadable: \(uploadable)") + NSLog("%@", "Request: \(request), didCreateUploadable: \(uploadable)") } public func request(_ request: UploadRequest, didFailToCreateUploadableWithError error: Error) { - NSLog("Request: \(request), didFailToCreateUploadableWithError: \(error)") + NSLog("%@", "Request: \(request), didFailToCreateUploadableWithError: \(error)") } public func request(_ request: UploadRequest, didProvideInputStream stream: InputStream) { - NSLog("Request: \(request), didProvideInputStream: \(stream)") + NSLog("%@", "Request: \(request), didProvideInputStream: \(stream)") } public func request(_ request: DownloadRequest, didFinishDownloadingUsing task: URLSessionTask, with result: Result) { - NSLog("Request: \(request), didFinishDownloadingUsing: \(task), withResult: \(result)") + NSLog("%@", "Request: \(request), didFinishDownloadingUsing: \(task), withResult: \(result)") } public func request(_ request: DownloadRequest, didCreateDestinationURL url: URL) { - NSLog("Request: \(request), didCreateDestinationURL: \(url)") + NSLog("%@", "Request: \(request), didCreateDestinationURL: \(url)") } public func request(_ request: DownloadRequest, didValidateRequest urlRequest: URLRequest?, response: HTTPURLResponse, temporaryURL: URL?, destinationURL: URL?, withResult result: Request.ValidationResult) { - NSLog("Request: \(request), didValidateRequestWithResult: \(result)") + NSLog("%@", "Request: \(request), didValidateRequestWithResult: \(result)") } }