|
|
@@ -26,7 +26,7 @@ import Foundation
|
|
|
|
|
|
protocol RequestDelegate: AnyObject {
|
|
|
func isRetryingRequest(_ request: Request, ifNecessaryWithError error: Error) -> Bool
|
|
|
-
|
|
|
+
|
|
|
func cancelRequest(_ request: Request)
|
|
|
func suspendRequest(_ request: Request)
|
|
|
func resumeRequest(_ request: Request)
|
|
|
@@ -35,20 +35,45 @@ protocol RequestDelegate: AnyObject {
|
|
|
open class Request {
|
|
|
// TODO: Make publicly readable properties protected?
|
|
|
|
|
|
+ public enum State {
|
|
|
+ case initialized, resumed, suspended, cancelled
|
|
|
+
|
|
|
+ func canTransitionTo(_ state: State) -> Bool {
|
|
|
+ switch (self, state) {
|
|
|
+ case (.initialized, _): return true
|
|
|
+ case (_, .initialized): return false
|
|
|
+ case (.resumed, .cancelled), (.suspended, .cancelled),
|
|
|
+ (.resumed, .suspended), (.suspended, .resumed): return true
|
|
|
+ case (.suspended, .suspended), (.resumed, .resumed): return false
|
|
|
+ case (.cancelled, _): return false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
// MARK: - Initial State
|
|
|
-
|
|
|
+
|
|
|
let id: UUID
|
|
|
let convertible: URLRequestConvertible
|
|
|
let underlyingQueue: DispatchQueue
|
|
|
let serializationQueue: DispatchQueue
|
|
|
let eventMonitor: RequestEventMonitor?
|
|
|
weak var delegate: RequestDelegate?
|
|
|
-
|
|
|
+
|
|
|
// TODO: Do we still want to expose the queue(s?) as public API?
|
|
|
open let internalQueue: OperationQueue
|
|
|
-
|
|
|
+
|
|
|
// MARK: - Updated State
|
|
|
-
|
|
|
+
|
|
|
+ private var protectedState: Protector<State> = Protector(.initialized)
|
|
|
+ public private(set) var state: State {
|
|
|
+ get { return protectedState.directValue }
|
|
|
+ set { protectedState.directValue = newValue }
|
|
|
+ }
|
|
|
+ public var isCancelled: Bool { return state == .cancelled }
|
|
|
+ public var isResumed: Bool { return state == .resumed }
|
|
|
+ public var isSuspended: Bool { return state == .suspended }
|
|
|
+ public var isInitialized: Bool { return state == .initialized }
|
|
|
+
|
|
|
public var retryCount: Int { return protectedTasks.read { max($0.count - 1, 0) } }
|
|
|
private(set) var initialRequest: URLRequest?
|
|
|
open var request: URLRequest? {
|
|
|
@@ -60,6 +85,7 @@ open class Request {
|
|
|
|
|
|
private(set) var metrics: URLSessionTaskMetrics?
|
|
|
// TODO: How to expose task progress on iOS 11?
|
|
|
+
|
|
|
private var protectedTasks = Protector<[URLSessionTask]>([])
|
|
|
public var tasks: [URLSessionTask] {
|
|
|
get { return protectedTasks.directValue }
|
|
|
@@ -102,40 +128,40 @@ open class Request {
|
|
|
|
|
|
// MARK: - Internal API
|
|
|
// Called from internal queue.
|
|
|
-
|
|
|
+
|
|
|
func didCreateURLRequest(_ request: URLRequest) {
|
|
|
initialRequest = request
|
|
|
-
|
|
|
+
|
|
|
eventMonitor?.request(self, didCreateURLRequest: request)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
func didFailToCreateURLRequest(with error: Error) {
|
|
|
self.error = error
|
|
|
-
|
|
|
+
|
|
|
eventMonitor?.request(self, didFailToCreateURLRequestWithError: error)
|
|
|
-
|
|
|
+
|
|
|
retryOrFinish(error: error)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
func didAdaptInitialRequest(_ initialRequest: URLRequest, to adaptedRequest: URLRequest) {
|
|
|
self.initialRequest = adaptedRequest
|
|
|
// Set initialRequest or something else?
|
|
|
eventMonitor?.request(self, didAdaptInitialRequest: initialRequest, to: adaptedRequest)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
func didFailToAdaptURLRequest(_ request: URLRequest, withError error: Error) {
|
|
|
self.error = error
|
|
|
-
|
|
|
+
|
|
|
eventMonitor?.request(self, didFailToAdaptURLRequest: request, withError: error)
|
|
|
-
|
|
|
+
|
|
|
retryOrFinish(error: error)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
func didCreateTask(_ task: URLSessionTask) {
|
|
|
self.task = task
|
|
|
// TODO: Reset behavior?
|
|
|
self.error = nil
|
|
|
-
|
|
|
+
|
|
|
eventMonitor?.request(self, didCreateTask: task)
|
|
|
}
|
|
|
|
|
|
@@ -149,33 +175,33 @@ open class Request {
|
|
|
|
|
|
func didCancel() {
|
|
|
error = AFError.explicitlyCancelled
|
|
|
-
|
|
|
+
|
|
|
eventMonitor?.requestDidCancel(self)
|
|
|
}
|
|
|
|
|
|
func didGatherMetrics(_ metrics: URLSessionTaskMetrics) {
|
|
|
self.metrics = metrics
|
|
|
-
|
|
|
+
|
|
|
eventMonitor?.request(self, didGatherMetrics: metrics)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// Should only be triggered by internal AF code, never URLSession
|
|
|
func didFailTask(_ task: URLSessionTask, earlyWithError error: Error) {
|
|
|
self.error = error
|
|
|
// Task will still complete, so didCompleteTask(_:with:) will handle retry.
|
|
|
eventMonitor?.request(self, didFailTask: task, earlyWithError: error)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// Completion point for all tasks.
|
|
|
func didCompleteTask(_ task: URLSessionTask, with error: Error?) {
|
|
|
self.error = self.error ?? error
|
|
|
validators.forEach { $0() }
|
|
|
-
|
|
|
+
|
|
|
eventMonitor?.request(self, didCompleteTask: task, with: error)
|
|
|
-
|
|
|
+
|
|
|
retryOrFinish(error: self.error)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
func retryOrFinish(error: Error?) {
|
|
|
if let error = error, delegate?.isRetryingRequest(self, ifNecessaryWithError: error) == true {
|
|
|
return
|
|
|
@@ -183,16 +209,16 @@ open class Request {
|
|
|
finish()
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
func finish() {
|
|
|
// Start response handlers
|
|
|
internalQueue.isSuspended = false
|
|
|
-
|
|
|
+
|
|
|
eventMonitor?.requestDidFinish(self)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// MARK: Task Creation
|
|
|
-
|
|
|
+
|
|
|
// Subclasses wanting something other than URLSessionDataTask should override.
|
|
|
func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
|
|
|
return session.dataTask(with: request)
|
|
|
@@ -202,16 +228,37 @@ open class Request {
|
|
|
|
|
|
// Callable from any queue.
|
|
|
|
|
|
- public func cancel() {
|
|
|
+ @discardableResult
|
|
|
+ public func cancel() -> Self {
|
|
|
+ guard state.canTransitionTo(.cancelled) else { return self }
|
|
|
+
|
|
|
+ state = .cancelled
|
|
|
+
|
|
|
delegate?.cancelRequest(self)
|
|
|
+
|
|
|
+ return self
|
|
|
}
|
|
|
|
|
|
- public func suspend() {
|
|
|
+ @discardableResult
|
|
|
+ public func suspend() -> Self {
|
|
|
+ guard state.canTransitionTo(.suspended) else { return self }
|
|
|
+
|
|
|
+ state = .suspended
|
|
|
+
|
|
|
delegate?.suspendRequest(self)
|
|
|
+
|
|
|
+ return self
|
|
|
}
|
|
|
|
|
|
- public func resume() {
|
|
|
+ @discardableResult
|
|
|
+ public func resume() -> Self {
|
|
|
+ guard state.canTransitionTo(.resumed) else { return self }
|
|
|
+
|
|
|
+ state = .resumed
|
|
|
+
|
|
|
delegate?.resumeRequest(self)
|
|
|
+
|
|
|
+ return self
|
|
|
}
|
|
|
|
|
|
// MARK: - Closure API
|
|
|
@@ -283,13 +330,13 @@ open class DataRequest: Request {
|
|
|
}
|
|
|
|
|
|
open class DownloadRequest: Request {
|
|
|
-
|
|
|
+
|
|
|
/// A `DownloadOptions` flag that creates intermediate directories for the destination URL if specified.
|
|
|
public static let createIntermediateDirectories = Options(rawValue: 1 << 0)
|
|
|
-
|
|
|
+
|
|
|
/// A `DownloadOptions` flag that removes a previous file from the destination URL if specified.
|
|
|
public static let removePreviousFile = Options(rawValue: 1 << 1)
|
|
|
-
|
|
|
+
|
|
|
/// A collection of options to be executed prior to moving a downloaded file from the temporary URL to the
|
|
|
/// destination URL.
|
|
|
public struct Options: OptionSet {
|
|
|
@@ -331,15 +378,15 @@ open class DownloadRequest: Request {
|
|
|
return (url, [])
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
// MARK: Initial State
|
|
|
-
|
|
|
+
|
|
|
private let destination: Destination
|
|
|
-
|
|
|
+
|
|
|
// MARK: Updated State
|
|
|
-
|
|
|
+
|
|
|
private(set) var temporaryURL: URL?
|
|
|
-
|
|
|
+
|
|
|
// MARK: Init
|
|
|
|
|
|
init(id: UUID = UUID(),
|
|
|
@@ -362,7 +409,7 @@ open class DownloadRequest: Request {
|
|
|
func didComplete(task: URLSessionTask, with url: URL) {
|
|
|
temporaryURL = url
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
|
|
|
// TODO: Need resume data.
|
|
|
return session.downloadTask(with: request)
|
|
|
@@ -424,7 +471,7 @@ open class UploadRequest: DataRequest {
|
|
|
eventMonitor: eventMonitor,
|
|
|
delegate: delegate)
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask {
|
|
|
switch uploadable {
|
|
|
case let .data(data): return session.uploadTask(with: request, from: data)
|