-
Notifications
You must be signed in to change notification settings - Fork 16
Support tracking HTTP request overall progress and cancellation #669
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,37 +9,92 @@ struct HTTPAPIResponse<Body> { | |
|
|
||
| extension URLSession { | ||
|
|
||
| /// Send a HTTP request and return its response as a `WordPressAPIResult` instance. | ||
| /// | ||
| /// ## Progress Tracking and Cancellation | ||
| /// | ||
| /// You can track the HTTP request's overall progress by passing a `Progress` instance to the `fulfillingProgress` | ||
| /// parameter, which must satisify following requirements: | ||
| /// - `totalUnitCount` must not be zero. | ||
| /// - `completedUnitCount` must be zero. | ||
| /// - It's used exclusivity for tracking the HTTP request overal progress: No children in its progress tree. | ||
| /// - `cancellationHandler` must be nil. You can call `fulfillingProgress.cancel()` to cancel the ongoing HTTP request. | ||
|
Comment on lines
+16
to
+21
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think about encoding these constraints in either a dedicated type that wraps Type wrapping
|
||
| /// | ||
| /// Upon completion, the HTTP request's progress fulfills the `fulfillingProgress`. | ||
| /// | ||
| /// - Parameters: | ||
| /// - builder: A `HTTPRequestBuilder` instance that represents an HTTP request to be sent. | ||
| /// - acceptableStatusCodes: HTTP status code ranges that are considered a successful response. Responses with | ||
| /// a status code outside of these ranges are returned as a `WordPressAPIResult.unacceptableStatusCode` instance. | ||
| /// - parentProgress: A `Progress` instance that will be used as the parent progress of the HTTP request's overall | ||
| /// progress. See the function documentation regarding requirements on this argument. | ||
| /// - errorType: The concret endpoint error type. | ||
| func perform<E: LocalizedError>( | ||
| request builder: HTTPRequestBuilder, | ||
| acceptableStatusCodes: [ClosedRange<Int>] = [200...299], | ||
| fulfillingProgress parentProgress: Progress? = nil, | ||
| errorType: E.Type = E.self | ||
| ) async -> WordPressAPIResult<HTTPAPIResponse<Data>, E> { | ||
| if let parentProgress { | ||
| assert(parentProgress.completedUnitCount == 0 && parentProgress.totalUnitCount > 0, "Invalid parent progress") | ||
| assert(parentProgress.cancellationHandler == nil, "The progress instance's cancellationHandler property must be nil") | ||
| } | ||
|
|
||
| guard let request = try? builder.build() else { | ||
| return .failure(.requestEncodingFailure) | ||
| } | ||
|
|
||
| let result: (Data, URLResponse) | ||
| do { | ||
| result = try await data(for: request) | ||
| } catch { | ||
| if let urlError = error as? URLError { | ||
| return .failure(.connection(urlError)) | ||
| } else { | ||
| return .failure(.unknown(underlyingError: error)) | ||
| return await withCheckedContinuation { continuation in | ||
| let task = dataTask(with: request) { data, response, error in | ||
| let result: WordPressAPIResult<HTTPAPIResponse<Data>, E> = Self.parseResponse( | ||
| data: data, | ||
| response: response, | ||
| error: error, | ||
| acceptableStatusCodes: acceptableStatusCodes | ||
| ) | ||
|
|
||
| continuation.resume(returning: result) | ||
| } | ||
| } | ||
| task.resume() | ||
|
|
||
| let (body, response) = result | ||
| if let parentProgress, parentProgress.totalUnitCount > parentProgress.completedUnitCount { | ||
| let pending = parentProgress.totalUnitCount - parentProgress.completedUnitCount | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need this math given the assert above for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, I don't think it's needed. But, in my head, the code expresses the idea more explicitly: letting the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair 👍 |
||
| parentProgress.addChild(task.progress, withPendingUnitCount: pending) | ||
|
|
||
| guard let response = response as? HTTPURLResponse else { | ||
| return .failure(.unparsableResponse(response: nil, body: body)) | ||
| parentProgress.cancellationHandler = { [weak task] in | ||
| task?.cancel() | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| guard acceptableStatusCodes.contains(where: { $0 ~= response.statusCode }) else { | ||
| return .failure(.unacceptableStatusCode(response: response, body: body)) | ||
| private static func parseResponse<E: LocalizedError>( | ||
| data: Data?, | ||
| response: URLResponse?, | ||
| error: Error?, | ||
| acceptableStatusCodes: [ClosedRange<Int>] | ||
| ) -> WordPressAPIResult<HTTPAPIResponse<Data>, E> { | ||
| let result: WordPressAPIResult<HTTPAPIResponse<Data>, E> | ||
|
|
||
| if let error { | ||
| if let urlError = error as? URLError { | ||
| result = .failure(.connection(urlError)) | ||
| } else { | ||
| result = .failure(.unknown(underlyingError: error)) | ||
| } | ||
| } else { | ||
| if let httpResponse = response as? HTTPURLResponse { | ||
| if acceptableStatusCodes.contains(where: { $0 ~= httpResponse.statusCode }) { | ||
| result = .success(HTTPAPIResponse(response: httpResponse, body: data ?? Data())) | ||
| } else { | ||
| result = .failure(.unacceptableStatusCode(response: httpResponse, body: data ?? Data())) | ||
| } | ||
| } else { | ||
| result = .failure(.unparsableResponse(response: nil, body: data)) | ||
| } | ||
| } | ||
|
|
||
| return .success(.init(response: response, body: body)) | ||
| return result | ||
| } | ||
|
|
||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.