Skip to content

Commit

Permalink
1.6.13
Browse files Browse the repository at this point in the history
  • Loading branch information
dankinsoid committed Apr 11, 2024
1 parent b1a4233 commit bea9e13
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ import PackageDescription
let package = Package(
name: "SomeProject",
dependencies: [
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.6.12")
.package(url: "https://github.com/dankinsoid/swift-api-client.git", from: "1.6.13")
],
targets: [
.target(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
let currentExpiryDate = dateFormatter.date(from: expiryDateString),
currentExpiryDate > Date()
{
(accessToken, refreshToken, _) = try await refreshTokenAndCache(configs, refreshToken: refreshToken)
(accessToken, refreshToken, _) = try await refreshTokenAndCache(configs, accessToken: currentToken, refreshToken: refreshToken)
} else {
accessToken = currentToken
}
Expand All @@ -110,7 +110,7 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
try auth(accessToken).modifier(&authorizedRequest, configs)
let result = try await next(authorizedRequest, configs)
if expiredStatusCodes.contains(result.1.status) {
(accessToken, refreshToken, _) = try await refreshTokenAndCache(configs, refreshToken: refreshToken)
(accessToken, refreshToken, _) = try await refreshTokenAndCache(configs, accessToken: accessToken, refreshToken: refreshToken)
authorizedRequest = request
try auth(accessToken).modifier(&authorizedRequest, configs)
return try await next(authorizedRequest, configs)
Expand All @@ -124,6 +124,7 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {
guard let request else {
throw Errors.custom("No cached token found.")
}

let (token, refreshToken, expiryDate) = try await request(configs)
tokenCacheService[.accessToken] = token
if let refreshToken {
Expand All @@ -137,9 +138,12 @@ public struct TokenRefresherMiddleware: HTTPClientMiddleware {

private func refreshTokenAndCache(
_ configs: APIClient.Configs,
accessToken: String,
refreshToken: String?
) async throws -> (String, String?, Date?) {
let (token, refreshToken, expiryDate) = try await refresh(refreshToken, configs)
let (token, refreshToken, expiryDate) = try await withThrowingSynchronizedAccess(id: accessToken) {
try await refresh(refreshToken, configs)
}
tokenCacheService[.accessToken] = token
if let refreshToken {
tokenCacheService[.refreshToken] = refreshToken
Expand Down
80 changes: 80 additions & 0 deletions Sources/SwiftAPIClient/Utils/Barrier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Foundation

/// Executes a given task with synchronization on a shared resource identified by `taskIdentifier`.
/// This function ensures that tasks associated with the same identifier are executed in a way that respects
/// the shared nature of the resource, possibly blocking or queuing tasks as necessary.
/// - Parameters:
/// - id: A unique identifier for the task, correlating to the shared resource that might require blocking or synchronization.
/// - task: The async task to be executed, which may read from or write to the shared resource.
/// - Returns: The result of the task if it is successfully completed.
public func withThrowingSynchronizedAccess<T, ID: Hashable>(
id taskIdentifier: ID,
task: @escaping @Sendable () async throws -> T
) async throws -> T {
if let cached = await Barriers.shared.tasks[taskIdentifier] {
if let task = cached as? Task<T, Error> {
return try await task.value
} else if let task = cached as? Task<T, Never> {
return await task.value
} else {
runtimeWarn("Unexpected task type found in the barrier.")
}
}
let task = Task(operation: task)
await Barriers.shared.setTask(for: taskIdentifier, task: task)
do {
let result = try await task.value
await Barriers.shared.removeTask(for: taskIdentifier)
return result
} catch {
await Barriers.shared.removeTask(for: taskIdentifier)
throw error
}
}

/// Executes a given task with synchronization on a shared resource identified by `taskIdentifier`.
/// This function ensures that tasks associated with the same identifier are executed in a way that respects
/// the shared nature of the resource, possibly blocking or queuing tasks as necessary.
/// - Parameters:
/// - id: A unique identifier for the task, correlating to the shared resource that might require blocking or synchronization.
/// - task: The async task to be executed, which may read from or write to the shared resource.
/// - Returns: The result of the task if it is successfully completed.
public func withSynchronizedAccess<T, ID: Hashable>(
id taskIdentifier: ID,
task: @escaping @Sendable () async -> T
) async -> T {
if let cached = await Barriers.shared.tasks[taskIdentifier] {
if let task = cached as? Task<T, Error> {
runtimeWarn("Attempted to access a throwing synchronized task from a non-throwing context.")
if let result = try? await task.value {
return result
}
} else if let task = cached as? Task<T, Never> {
return await task.value
} else {
runtimeWarn("Unexpected task type found in the barrier.")
}
}
let task = Task(operation: task)
await Barriers.shared.setTask(for: taskIdentifier, task: task)
let result = await task.value
await Barriers.shared.removeTask(for: taskIdentifier)
return result
}

private final actor Barriers {

static let shared = Barriers()

var tasks: [AnyHashable: Any] = [:]

private init() {}

func removeTask(for key: AnyHashable) {
tasks[key] = nil
}

func setTask<T, E: Error>(for key: AnyHashable, task: Task<T, E>) {
tasks[key] = task
}
}
71 changes: 71 additions & 0 deletions Sources/SwiftAPIClient/Utils/RuntimeWarnings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation

@_transparent
@usableFromInline
@inline(__always)
func runtimeWarn(
_ message: @autoclosure () -> String,
category: String? = "swift-api-client"
) {
#if DEBUG
let message = message()
let category = category ?? "Runtime Warning"
if _XCTIsTesting {
#if canImport(XCTest)
XCTFail(message)
#endif
} else {
#if canImport(os)
os_log(
.fault,
dso: dso,
log: OSLog(subsystem: "com.apple.runtime-issues", category: category),
"%@",
message
)
#else
fputs("\(formatter.string(from: Date())) [\(category)] \(message)\n", stderr)
#endif
}
#endif
}

#if DEBUG
#if canImport(XCTest)
import XCTest
#endif

#if canImport(os)
import os

// NB: Xcode runtime warnings offer a much better experience than traditional assertions and
// breakpoints, but Apple provides no means of creating custom runtime warnings ourselves.
// To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead.
//
// Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc
@usableFromInline
let dso = { () -> UnsafeMutableRawPointer in
let count = _dyld_image_count()
for i in 0 ..< count {
if let name = _dyld_get_image_name(i) {
let swiftString = String(cString: name)
if swiftString.hasSuffix("/SwiftUI") {
if let header = _dyld_get_image_header(i) {
return UnsafeMutableRawPointer(mutating: UnsafeRawPointer(header))
}
}
}
}
return UnsafeMutableRawPointer(mutating: #dsohandle)
}()
#else
import Foundation

@usableFromInline
let formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:MM:SS.sssZ"
return formatter
}()
#endif
#endif

0 comments on commit bea9e13

Please sign in to comment.