Skip to content

Commit c00dc7e

Browse files
authored
feat: implement credentials refresh (#72)
* chore(spm): update dependency space-code/typhoon to v2 * feat: implement credentials refresh * feat(network): prevent concurrent token refresh by queueing pending requests * fix: fix a typo
1 parent 4e5b81b commit c00dc7e

15 files changed

Lines changed: 876 additions & 81 deletions

File tree

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let package = Package(
1818
],
1919
dependencies: [
2020
.package(url: "https://github.com/space-code/atomic", exact: "1.1.1"),
21-
.package(url: "https://github.com/space-code/typhoon", exact: "1.4.0"),
21+
.package(url: "https://github.com/space-code/typhoon", exact: "3.0.0"),
2222
.package(url: "https://github.com/WeTransfer/Mocker", exact: "3.0.2"),
2323
],
2424
targets: [

Package@swift-5.10.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let package = Package(
1818
],
1919
dependencies: [
2020
.package(url: "https://github.com/space-code/atomic", exact: "1.1.0"),
21-
.package(url: "https://github.com/space-code/typhoon", exact: "1.4.0"),
21+
.package(url: "https://github.com/space-code/typhoon", exact: "3.0.0"),
2222
.package(url: "https://github.com/WeTransfer/Mocker", exact: "3.0.1"),
2323
],
2424
targets: [

Package@swift-6.0.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let package = Package(
1818
],
1919
dependencies: [
2020
.package(url: "https://github.com/space-code/atomic", exact: "1.1.0"),
21-
.package(url: "https://github.com/space-code/typhoon", exact: "1.4.0"),
21+
.package(url: "https://github.com/space-code/typhoon", exact: "3.0.0"),
2222
.package(url: "https://github.com/WeTransfer/Mocker", exact: "3.0.1"),
2323
],
2424
targets: [

Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift

Lines changed: 209 additions & 55 deletions
Large diffs are not rendered by default.

Sources/NetworkLayer/Classes/DI/NetworkLayerAssembly.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
2424
private let jsonEncoder: JSONEncoder
2525
/// A global evaluator to determine if a retry should be attempted based on the error.
2626
/// This applies to all requests processed by this instance.
27-
private let retryEvaluator: (@Sendable (Error) -> Bool)?
27+
private let retryEvaluator: (@Sendable (Error) -> RetryAction)?
2828

2929
// MARK: Initialization
3030

@@ -47,7 +47,7 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
4747
delegate: RequestProcessorDelegate? = nil,
4848
interceptor: IAuthenticationInterceptor? = nil,
4949
jsonEncoder: JSONEncoder = JSONEncoder(),
50-
retryEvaluator: (@Sendable (Error) -> Bool)? = nil
50+
retryEvaluator: (@Sendable (Error) -> RetryAction)? = nil
5151
) {
5252
self.configure = configure
5353
self.delegate = SafeRequestProcessorDelegate(delegate: delegate)
@@ -59,7 +59,7 @@ public final class NetworkLayerAssembly: INetworkLayerAssembly {
5959
case .none:
6060
retryPolicyStrategy = nil
6161
case .default:
62-
retryPolicyStrategy = .constant(retry: 5, duration: .seconds(1))
62+
retryPolicyStrategy = .constant(retry: 5, dispatchDuration: .seconds(1))
6363
case let .custom(strategy):
6464
retryPolicyStrategy = strategy
6565
}

Sources/NetworkLayerInterfaces/Classes/Core/Models/Response.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,6 @@ public struct Response<T> {
3535
}
3636
}
3737

38-
// MARK: @unchecked Sendable
38+
// MARK: Sendable
3939

40-
extension Response: @unchecked Sendable where T: Sendable {}
40+
extension Response: Sendable where T: Sendable {}

Sources/NetworkLayerInterfaces/Classes/Core/Services/IRequestProcessor.swift

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,25 @@ public protocol IRequestProcessor {
2323
strategy: RetryPolicyStrategy?,
2424
delegate: URLSessionDelegate?,
2525
configure: (@Sendable (inout URLRequest) throws -> Void)?,
26-
shouldRetry: (@Sendable (Error) -> Bool)?
26+
shouldRetry: (@Sendable (Error) -> RetryAction)?
2727
) async throws -> Response<M>
28+
29+
/// Sends a network request and returns the result along with retry information.
30+
///
31+
/// - Parameters:
32+
/// - request: The request object conforming to the `IRequest` protocol.
33+
/// - strategy: An optional override for the retry policy strategy.
34+
/// - delegate: An optional `URLSessionDelegate`.
35+
/// - configure: An optional closure to modify the `URLRequest`.
36+
/// - shouldRetry: An optional closure to determine if a retry should be attempted.
37+
/// - Returns: A retry result containing the response.
38+
func sendWithResult<M: Decodable>(
39+
_ request: some IRequest,
40+
strategy: RetryPolicyStrategy?,
41+
delegate: URLSessionDelegate?,
42+
configure: (@Sendable (inout URLRequest) throws -> Void)?,
43+
shouldRetry: (@Sendable (Error) -> RetryAction)?
44+
) async throws -> RetryResult<Response<M>>
2845
}
2946

3047
extension IRequestProcessor {
@@ -46,4 +63,23 @@ extension IRequestProcessor {
4663
func send<M: Decodable>(_ request: some IRequest) async throws -> Response<M> {
4764
try await send(request, strategy: nil, delegate: nil, configure: nil, shouldRetry: nil)
4865
}
66+
67+
/// Sends a network request with result with default parameters.
68+
///
69+
/// - Parameters:
70+
/// - request: The request object conforming to the `IRequest` protocol.
71+
func sendWithResult<M: Decodable>(
72+
_ request: some IRequest,
73+
strategy: RetryPolicyStrategy?
74+
) async throws -> RetryResult<Response<M>> {
75+
try await sendWithResult(request, strategy: strategy, delegate: nil, configure: nil, shouldRetry: nil)
76+
}
77+
78+
/// Sends a network request with result with default parameters.
79+
///
80+
/// - Parameters:
81+
/// - request: The request object conforming to the `IRequest` protocol.
82+
func sendWithResult<M: Decodable>(_ request: some IRequest) async throws -> RetryResult<Response<M>> {
83+
try await sendWithResult(request, strategy: nil, delegate: nil, configure: nil, shouldRetry: nil)
84+
}
4985
}

Sources/NetworkLayerInterfaces/Classes/DI/INetworkLayerAssembly.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55

66
import Foundation
7+
import enum Typhoon.RetryAction
78
import enum Typhoon.RetryPolicyStrategy
89

910
// MARK: - INetworkLayerAssembly
@@ -28,7 +29,7 @@ public protocol INetworkLayerAssembly {
2829
delegate: RequestProcessorDelegate?,
2930
interceptor: IAuthenticationInterceptor?,
3031
jsonEncoder: JSONEncoder,
31-
retryEvaluator: (@Sendable (Error) -> Bool)?
32+
retryEvaluator: (@Sendable (Error) -> RetryAction)?
3233
)
3334

3435
/// Construct and link all internal components to create a request processor.

Tests/NetworkLayerTests/Classes/Helpers/Helpers/RequestProcessor+Mock.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension RequestProcessor {
2727
queryFormatter: QueryParametersFormatter()
2828
),
2929
dataRequestHandler: DataRequestHandler(),
30-
retryPolicyService: RetryPolicyService(strategy: .constant(retry: 1, duration: .seconds(0))),
30+
retryPolicyService: RetryPolicyService(strategy: .constant(retry: 1, dispatchDuration: .seconds(0))),
3131
delegate: SafeRequestProcessorDelegate(delegate: requestProcessorDelegate),
3232
interceptor: interceptor,
3333
retryEvaluator: { _ in true }

Tests/NetworkLayerTests/Classes/Helpers/Mocks/AuthentificatorInterceptorMock.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,28 @@ final class AuthentificatorInterceptorMock: IAuthenticationInterceptor, @uncheck
2323
var invokedRefreshCount = 0
2424
var invokedRefreshParameters: (request: URLRequest, response: HTTPURLResponse, session: URLSession)?
2525
var invokedRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse, session: URLSession)]()
26+
var refreshClosure: (() async throws -> Void)?
2627

27-
func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) {
28+
func refresh(_ request: URLRequest, with response: HTTPURLResponse, for session: URLSession) async throws {
2829
invokedRefresh = true
2930
invokedRefreshCount += 1
3031
invokedRefreshParameters = (request, response, session)
3132
invokedRefreshParametersList.append((request, response, session))
33+
try await refreshClosure?()
3234
}
3335

3436
var invokedIsRequireRefresh = false
3537
var invokedIsRequireRefreshCount = 0
3638
var invokedIsRequireRefreshParameters: (request: URLRequest, response: HTTPURLResponse)?
3739
var invokedIsRequireRefreshParametersList = [(request: URLRequest, response: HTTPURLResponse)]()
3840
var stubbedIsRequireRefreshResult: Bool! = false
41+
var isRequireRefreshClosure: (() -> Bool)?
3942

4043
func isRequireRefresh(_ request: URLRequest, response: HTTPURLResponse) -> Bool {
4144
invokedIsRequireRefresh = true
4245
invokedIsRequireRefreshCount += 1
4346
invokedIsRequireRefreshParameters = (request, response)
4447
invokedIsRequireRefreshParametersList.append((request, response))
45-
return stubbedIsRequireRefreshResult
48+
return isRequireRefreshClosure?() ?? stubbedIsRequireRefreshResult
4649
}
4750
}

0 commit comments

Comments
 (0)