Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use actor for AuthHandler (token refresh) #23

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,97 +1,87 @@
import Foundation
import Alamofire
import Fetch
import Foundation
import Models

public class AuthHandler: RequestInterceptor {

private typealias RefreshCompletion = (_ credentials: Credentials?, _ statusCode: Int?) -> Void
private typealias RequestRetryCompletion = (Alamofire.RetryResult) -> Void

private static let apiLogger = APILogger(verbose: true)

private let session: Session = {
let configuration = URLSessionConfiguration.default
return Session(configuration: configuration, eventMonitors: [apiLogger])
}()

private let lock = NSLock()
private let queue = DispatchQueue(label: "network.auth.queue")
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []

private let actor: CredentialsRefreshActor = .init()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actor is a keyword so I wouldn't use it to name the variable


// MARK: - RequestAdapter
public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {

public func adapt(_ urlRequest: URLRequest, for _: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
if let accessToken = CredentialsController.shared.currentCredentials?.accessToken {
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}
completion(.success(urlRequest))
}

// MARK: - RequestRetrier

public func retry(_ request: Alamofire.Request, for session: Alamofire.Session, dueTo error: Error, completion: @escaping (Alamofire.RetryResult) -> Void) {
lock.lock() ; defer { lock.unlock() }


public func retry(_ request: Alamofire.Request, for _: Alamofire.Session, dueTo _: Error, completion: @escaping (Alamofire.RetryResult) -> Void) {
guard let response = request.task?.response as? HTTPURLResponse,
let refreshToken = CredentialsController.shared.currentCredentials?.refreshToken,
response.statusCode == 401 else {
completion(.doNotRetry)
return
let refreshToken = CredentialsController.shared.currentCredentials?.refreshToken,
response.statusCode == 401 else {
completion(.doNotRetry)
return
}

requestsToRetry.append(completion)

if !isRefreshing {
refreshCredentials(refreshToken) { [weak self] (credentials, statusCode) in
guard let self = self else { return }

self.lock.lock() ; defer { self.lock.unlock() }

if let credentials = credentials {
CredentialsController.shared.currentCredentials = credentials
self.requestsToRetry.forEach { $0(.retry) }
} else {
if statusCode == 401 {
CredentialsController.shared.currentCredentials = nil
}
self.requestsToRetry.forEach { $0(.doNotRetry) }
}

self.requestsToRetry.removeAll()
}

Task {
await actor.addRequest(completion)
await actor.refresh(refreshToken: refreshToken)
}
}
}

actor CredentialsRefreshActor {
typealias RequestRetryCompletion = (Alamofire.RetryResult) -> Void

private static let apiLogger = APILogger(verbose: true)

// MARK: - Private - Refresh Tokens

private func refreshCredentials(_ refreshToken: String, completion: @escaping RefreshCompletion) {
private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []
private let session: Session = {
let configuration = URLSessionConfiguration.default
return Session(configuration: configuration, eventMonitors: [apiLogger])
}()

func addRequest(_ request: @escaping RequestRetryCompletion) async {
requestsToRetry.append(request)
}

func refresh(refreshToken: String) async {
guard !isRefreshing else { return }

isRefreshing = true


defer {
requestsToRetry.removeAll()
isRefreshing = false
}

guard let urlRequest = try? API.Auth.tokenRefresh(refreshToken).asURLRequest() else {
completion(nil, nil)
requestsToRetry.forEach { $0(.doNotRetry) }
return
}

session
.request(urlRequest)

let response = await AF.request(urlRequest)
.validate()
.responseDecodable(queue: queue, completionHandler: { [weak self] (response: DataResponse<Credentials, AFError>) in
guard let self = self else { return }

let statusCode = response.response?.statusCode

switch response.result {
case .success(let credentials):
completion(credentials, statusCode)
case .failure:
completion(nil, statusCode)
}
self.isRefreshing = false
})
.serializingDecodable(Credentials.self)
.response

let statusCode = response.response?.statusCode

switch response.result {
case .success(let credentials):
CredentialsController.shared.currentCredentials = credentials
requestsToRetry.forEach { $0(.retry) }

case .failure:
if statusCode == 401 {
CredentialsController.shared.currentCredentials = nil
}
requestsToRetry.forEach { $0(.doNotRetry) }
}
isRefreshing = false
}
}