From ecd90259a84706b09739d15a216b9a6f933bc23f Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 16 Oct 2025 15:45:04 -0300 Subject: [PATCH 1/8] feat: introduce Alamofire in a backward compatible way --- Package.resolved | 11 +++- Package.swift | 2 + Package@swift-6.1.swift | 2 + Sources/Functions/Deprecated.swift | 43 +++++++++++++++ Sources/Functions/FunctionsClient.swift | 36 +++++++----- .../Helpers/HTTP/AlamofireHTTPClient.swift | 55 +++++++++++++++++++ Sources/Supabase/Deprecated.swift | 25 ++++++++- Sources/Supabase/Types.swift | 15 ++++- .../xcshareddata/swiftpm/Package.resolved | 11 +++- 9 files changed, 181 insertions(+), 19 deletions(-) create mode 100644 Sources/Functions/Deprecated.swift create mode 100644 Sources/Helpers/HTTP/AlamofireHTTPClient.swift diff --git a/Package.resolved b/Package.resolved index 144b24ad3..a25097885 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "a251003bb005f3d13125c805f4a0badb262fc6da6c108fc3a99ecb47bcb70c9d", + "originHash" : "9cca310b019efec807ce08b7fbd5c71e6457d6e87be30a704f6b65200a8f8123", "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", + "version" : "5.10.2" + } + }, { "identity" : "mocker", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 86bffd635..c619c530e 100644 --- a/Package.swift +++ b/Package.swift @@ -24,6 +24,7 @@ let package = Package( targets: ["Supabase", "Functions", "PostgREST", "Auth", "Realtime", "Storage"]), ], dependencies: [ + .package(url: "https://github.com/alamofire/alamofire.git", from: "5.0.0"), .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"5.0.0"), .package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"), .package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.0"), @@ -41,6 +42,7 @@ let package = Package( .product(name: "HTTPTypes", package: "swift-http-types"), .product(name: "Clocks", package: "swift-clocks"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + .product(name: "Alamofire", package: "Alamofire"), ] ), .testTarget( diff --git a/Package@swift-6.1.swift b/Package@swift-6.1.swift index 7a2612388..8e53483ba 100644 --- a/Package@swift-6.1.swift +++ b/Package@swift-6.1.swift @@ -32,6 +32,7 @@ let package = Package( ) ], dependencies: [ + .package(url: "https://github.com/alamofire/alamofire.git", from: "5.0.0"), .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"5.0.0"), .package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"), .package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.0"), @@ -49,6 +50,7 @@ let package = Package( .product(name: "HTTPTypes", package: "swift-http-types"), .product(name: "Clocks", package: "swift-clocks"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + .product(name: "Alamofire", package: "Alamofire"), ] ), .testTarget( diff --git a/Sources/Functions/Deprecated.swift b/Sources/Functions/Deprecated.swift new file mode 100644 index 000000000..ecc76c981 --- /dev/null +++ b/Sources/Functions/Deprecated.swift @@ -0,0 +1,43 @@ +import Foundation + +extension FunctionsClient { + @available( + *, deprecated, + message: + "Use init(url:headers:region:logger:alamofireSession:) instead. This initializer will be removed in a future version." + ) + @_disfavoredOverload + public convenience init( + url: URL, + headers: [String: String] = [:], + region: String? = nil, + logger: (any SupabaseLogger)? = nil, + fetch: @escaping FetchHandler + ) { + self.init( + url: url, + headers: headers, + region: region, + logger: logger, + fetch: fetch, + alamofireSession: .default + ) + } + + @available( + *, deprecated, + message: + "Use init(url:headers:region:logger:alamofireSession:) instead. This initializer will be removed in a future version." + ) + public convenience init( + url: URL, + headers: [String: String] = [:], + region: FunctionRegion? = nil, + logger: (any SupabaseLogger)? = nil, + fetch: @escaping FetchHandler + ) { + self.init( + url: url, headers: headers, region: region?.rawValue, logger: logger, fetch: fetch, + alamofireSession: .default) + } +} diff --git a/Sources/Functions/FunctionsClient.swift b/Sources/Functions/FunctionsClient.swift index fa6dc135d..ff59301a7 100644 --- a/Sources/Functions/FunctionsClient.swift +++ b/Sources/Functions/FunctionsClient.swift @@ -1,6 +1,8 @@ +import Alamofire import ConcurrencyExtras import Foundation import HTTPTypes +import Helpers #if canImport(FoundationNetworking) import FoundationNetworking @@ -11,9 +13,10 @@ let version = Helpers.version /// An actor representing a client for invoking functions. public final class FunctionsClient: Sendable { /// Fetch handler used to make requests. - public typealias FetchHandler = @Sendable (_ request: URLRequest) async throws -> ( - Data, URLResponse - ) + public typealias FetchHandler = + @Sendable (_ request: URLRequest) async throws -> ( + Data, URLResponse + ) /// Request idle timeout: 150s (If an Edge Function doesn't send a response before the timeout, 504 Gateway Timeout will be returned) /// @@ -53,15 +56,15 @@ public final class FunctionsClient: Sendable { headers: [String: String] = [:], region: String? = nil, logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } + alamofireSession: Alamofire.Session = .default ) { self.init( url: url, headers: headers, region: region, logger: logger, - fetch: fetch, - sessionConfiguration: .default + fetch: nil, + alamofireSession: alamofireSession ) } @@ -70,22 +73,27 @@ public final class FunctionsClient: Sendable { headers: [String: String] = [:], region: String? = nil, logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, - sessionConfiguration: URLSessionConfiguration + fetch: FetchHandler?, + alamofireSession: Alamofire.Session ) { var interceptors: [any HTTPClientInterceptor] = [] if let logger { interceptors.append(LoggerInterceptor(logger: logger)) } - let http = HTTPClient(fetch: fetch, interceptors: interceptors) + let http: any HTTPClientType = + if let fetch { + HTTPClient(fetch: fetch, interceptors: interceptors) + } else { + AlamofireHTTPClient(session: alamofireSession) + } self.init( url: url, headers: headers, region: region, http: http, - sessionConfiguration: sessionConfiguration + sessionConfiguration: alamofireSession.session.configuration ) } @@ -94,7 +102,7 @@ public final class FunctionsClient: Sendable { headers: [String: String], region: String?, http: any HTTPClientType, - sessionConfiguration: URLSessionConfiguration = .default + sessionConfiguration: URLSessionConfiguration ) { self.url = url self.region = region @@ -122,9 +130,11 @@ public final class FunctionsClient: Sendable { headers: [String: String] = [:], region: FunctionRegion? = nil, logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } + alamofireSession: Alamofire.Session = .default ) { - self.init(url: url, headers: headers, region: region?.rawValue, logger: logger, fetch: fetch) + self.init( + url: url, headers: headers, region: region?.rawValue, logger: logger, fetch: nil, + alamofireSession: alamofireSession) } /// Updates the authorization header. diff --git a/Sources/Helpers/HTTP/AlamofireHTTPClient.swift b/Sources/Helpers/HTTP/AlamofireHTTPClient.swift new file mode 100644 index 000000000..d52d5517f --- /dev/null +++ b/Sources/Helpers/HTTP/AlamofireHTTPClient.swift @@ -0,0 +1,55 @@ +import Alamofire +import ConcurrencyExtras +import Foundation + +extension HTTPRequest: URLRequestConvertible { + package func asURLRequest() throws -> URLRequest { + guard let urlRequest = self.urlRequest else { + throw AFError.invalidURL(url: self.url.absoluteString) + } + return urlRequest + } +} + +package struct AlamofireHTTPClient: HTTPClientType { + let session: Session + + package init(session: Session = .default) { + self.session = session + } + + package func send(_ request: HTTPRequest) async throws -> HTTPResponse { + let response = await session.request(request).serializingData().response + + guard let httpResponse = response.response else { + throw URLError(.badServerResponse) + } + + return HTTPResponse(data: response.data ?? Data(), response: httpResponse) + } + + package func stream( + _ request: HTTPRequest + ) -> AsyncThrowingStream { + let stream = + session + .streamRequest(request) + .streamTask() + .streamingData() + .compactMap { + switch $0.event { + case .stream(let result): + return result.get() + + case .complete(let completion): + if let error = completion.error { + throw error + } + // If the stream is complete, return nil + return nil + } + } + + return AsyncThrowingStream(UncheckedSendable(stream)) + } +} diff --git a/Sources/Supabase/Deprecated.swift b/Sources/Supabase/Deprecated.swift index 5043e4119..2b5a4b291 100644 --- a/Sources/Supabase/Deprecated.swift +++ b/Sources/Supabase/Deprecated.swift @@ -5,6 +5,7 @@ // Created by Guilherme Souza on 15/05/24. // +import Alamofire import Foundation extension SupabaseClient { @@ -12,7 +13,8 @@ extension SupabaseClient { @available( *, deprecated, - message: "Direct access to database is deprecated, please use one of the available methods such as, SupabaseClient.from(_:), SupabaseClient.rpc(_:params:), or SupabaseClient.schema(_:)." + message: + "Direct access to database is deprecated, please use one of the available methods such as, SupabaseClient.from(_:), SupabaseClient.rpc(_:params:), or SupabaseClient.schema(_:)." ) public var database: PostgrestClient { rest @@ -24,3 +26,24 @@ extension SupabaseClient { _realtime.value } } + +extension SupabaseClientOptions.GlobalOptions { + @available( + *, deprecated, + message: + "Use init(headers:alamofireSession:logger:) instead. This initializer will be removed in a future version." + ) + public init( + headers: [String: String] = [:], + session: URLSession, + logger: (any SupabaseLogger)? = nil + ) { + self.init( + headers: headers, + alamofireSession: Alamofire.Session( + session: session, delegate: SessionDelegate(), + rootQueue: DispatchQueue(label: "com.supabase.session")), + logger: logger + ) + } +} diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index b567d7d34..8f77e3c52 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -1,3 +1,4 @@ +import Alamofire import Foundation #if canImport(FoundationNetworking) @@ -89,18 +90,26 @@ public struct SupabaseClientOptions: Sendable { public let headers: [String: String] /// A session to use for making requests, defaults to `URLSession.shared`. + @available( + *, deprecated, + message: "Use alamofireSession instead. This will be removed in a future version." + ) public let session: URLSession + /// Alamofire session to use for making requests, defaults to `Alamofire.Session.default`. + public let alamofireSession: Alamofire.Session + /// The logger to use across all Supabase sub-packages. public let logger: (any SupabaseLogger)? public init( headers: [String: String] = [:], - session: URLSession = .shared, + alamofireSession: Alamofire.Session = .default, logger: (any SupabaseLogger)? = nil ) { self.headers = headers - self.session = session + self.session = alamofireSession.session + self.alamofireSession = alamofireSession self.logger = logger } } @@ -122,7 +131,7 @@ public struct SupabaseClientOptions: Sendable { public struct StorageOptions: Sendable { /// Whether storage client should be initialized with the new hostname format, i.e. `project-ref.storage.supabase.co` public let useNewHostname: Bool - + public init(useNewHostname: Bool = false) { self.useNewHostname = useNewHostname } diff --git a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved index f43063471..ac40f727b 100644 --- a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "68a31593121bf823182bc731b17208689dafb38f7cb085035de5e74a0ed41e89", + "originHash" : "7a227a9457a4d33a10498436bcd6edbbb8ef72312871f8a59a70176b1b27c9ab", "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire", + "state" : { + "revision" : "513364f870f6bfc468f9d2ff0a95caccc10044c5", + "version" : "5.10.2" + } + }, { "identity" : "appauth-ios", "kind" : "remoteSourceControl", From 8f4a246718b0257be7a7c63cc7810aebb9ca8aa2 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 16 Oct 2025 17:28:56 -0300 Subject: [PATCH 2/8] feat: adopt Alamofire on remaining modules --- Sources/Auth/AuthClient.swift | 2 +- Sources/Auth/AuthClientConfiguration.swift | 54 +++++++-- Sources/Auth/Deprecated.swift | 127 +++++++++++++++++++-- Sources/Auth/Internal/APIClient.swift | 27 +++-- Sources/PostgREST/Deprecated.swift | 97 ++++++++++++++-- Sources/PostgREST/PostgrestBuilder.swift | 8 +- Sources/PostgREST/PostgrestClient.swift | 49 +++++++- Sources/Storage/Deprecated.swift | 35 +++++- Sources/Storage/StorageApi.swift | 11 +- Sources/Storage/SupabaseStorage.swift | 44 ++++++- Sources/Supabase/Deprecated.swift | 12 +- Sources/Supabase/Types.swift | 8 -- 12 files changed, 397 insertions(+), 77 deletions(-) diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index 5d8f80249..011d524d7 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -127,7 +127,7 @@ public actor AuthClient { Dependencies[clientID] = Dependencies( configuration: configuration, - http: HTTPClient(configuration: configuration), + http: makeHTTPClient(configuration: configuration), api: APIClient(clientID: clientID), codeVerifierStorage: .live(clientID: clientID), sessionStorage: .live(clientID: clientID), diff --git a/Sources/Auth/AuthClientConfiguration.swift b/Sources/Auth/AuthClientConfiguration.swift index a9a0dc38f..7aacca2c8 100644 --- a/Sources/Auth/AuthClientConfiguration.swift +++ b/Sources/Auth/AuthClientConfiguration.swift @@ -5,6 +5,7 @@ // Created by Guilherme Souza on 29/04/24. // +import Alamofire import Foundation #if canImport(FoundationNetworking) @@ -13,9 +14,10 @@ import Foundation extension AuthClient { /// FetchHandler is a type alias for asynchronous network request handling. - public typealias FetchHandler = @Sendable ( - _ request: URLRequest - ) async throws -> (Data, URLResponse) + public typealias FetchHandler = + @Sendable ( + _ request: URLRequest + ) async throws -> (Data, URLResponse) /// Configuration struct represents the client configuration. public struct Configuration: Sendable { @@ -43,6 +45,9 @@ extension AuthClient { /// A custom fetch implementation. public let fetch: FetchHandler + /// Alamofire session to use for making requests. + public let alamofireSession: Alamofire.Session + /// Set to `true` if you want to automatically refresh the token before expiring. public let autoRefreshToken: Bool @@ -58,7 +63,7 @@ extension AuthClient { /// - logger: The logger to use. /// - encoder: The JSON encoder to use for encoding requests. /// - decoder: The JSON decoder to use for decoding responses. - /// - fetch: The asynchronous fetch handler for network requests. + /// - alamofireSession: Alamofire session to use for making requests. /// - autoRefreshToken: Set to `true` if you want to automatically refresh the token before expiring. public init( url: URL? = nil, @@ -70,8 +75,38 @@ extension AuthClient { logger: (any SupabaseLogger)? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + alamofireSession: Alamofire.Session = .default, autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken + ) { + self.init( + url: url, + headers: headers, + flowType: flowType, + redirectToURL: redirectToURL, + storageKey: storageKey, + localStorage: localStorage, + logger: logger, + encoder: encoder, + decoder: decoder, + fetch: { try await alamofireSession.session.data(for: $0) }, + alamofireSession: alamofireSession, + autoRefreshToken: autoRefreshToken + ) + } + + init( + url: URL?, + headers: [String: String], + flowType: AuthFlowType, + redirectToURL: URL?, + storageKey: String?, + localStorage: any AuthLocalStorage, + logger: (any SupabaseLogger)?, + encoder: JSONEncoder, + decoder: JSONDecoder, + fetch: FetchHandler?, + alamofireSession: Alamofire.Session, + autoRefreshToken: Bool ) { let headers = headers.merging(Configuration.defaultHeaders) { l, _ in l } @@ -84,7 +119,8 @@ extension AuthClient { self.logger = logger self.encoder = encoder self.decoder = decoder - self.fetch = fetch + self.fetch = fetch ?? { try await alamofireSession.session.data(for: $0) } + self.alamofireSession = alamofireSession self.autoRefreshToken = autoRefreshToken } } @@ -101,7 +137,7 @@ extension AuthClient { /// - logger: The logger to use. /// - encoder: The JSON encoder to use for encoding requests. /// - decoder: The JSON decoder to use for decoding responses. - /// - fetch: The asynchronous fetch handler for network requests. + /// - alamofireSession: Alamofire session to use for making requests. /// - autoRefreshToken: Set to `true` if you want to automatically refresh the token before expiring. public init( url: URL? = nil, @@ -113,7 +149,7 @@ extension AuthClient { logger: (any SupabaseLogger)? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + alamofireSession: Alamofire.Session = .default, autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken ) { self.init( @@ -127,7 +163,7 @@ extension AuthClient { logger: logger, encoder: encoder, decoder: decoder, - fetch: fetch, + alamofireSession: alamofireSession, autoRefreshToken: autoRefreshToken ) ) diff --git a/Sources/Auth/Deprecated.swift b/Sources/Auth/Deprecated.swift index 9b0ca5f24..86e145c1f 100644 --- a/Sources/Auth/Deprecated.swift +++ b/Sources/Auth/Deprecated.swift @@ -5,6 +5,7 @@ // Created by Guilherme Souza on 14/12/23. // +import Alamofire import Foundation #if canImport(FoundationNetworking) @@ -67,7 +68,7 @@ extension AuthClient.Configuration { *, deprecated, message: - "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" + "Replace usages of this initializer with new init(url:headers:flowType:redirectToURL:storageKey:localStorage:logger:encoder:decoder:alamofireSession:autoRefreshToken:)" ) public init( url: URL, @@ -82,50 +83,156 @@ extension AuthClient.Configuration { url: url, headers: headers, flowType: flowType, + redirectToURL: nil, + storageKey: nil, localStorage: localStorage, logger: nil, encoder: encoder, decoder: decoder, - fetch: fetch + fetch: fetch, + alamofireSession: .default, + autoRefreshToken: Self.defaultAutoRefreshToken ) } -} -extension AuthClient { /// Initializes a AuthClient Configuration with optional parameters. /// /// - Parameters: /// - url: The base URL of the Auth server. /// - headers: Custom headers to be included in requests. /// - flowType: The authentication flow type. + /// - redirectToURL: Default URL to be used for redirect on the flows that requires it. + /// - storageKey: Optional key name used for storing tokens in local storage. /// - localStorage: The storage mechanism for local data. + /// - logger: The logger to use. /// - encoder: The JSON encoder to use for encoding requests. /// - decoder: The JSON decoder to use for decoding responses. /// - fetch: The asynchronous fetch handler for network requests. + /// - autoRefreshToken: Set to `true` if you want to automatically refresh the token before expiring. @available( *, deprecated, message: - "Replace usages of this initializer with new init(url:headers:flowType:localStorage:logger:encoder:decoder:fetch)" + "Use init(url:headers:flowType:redirectToURL:storageKey:localStorage:logger:encoder:decoder:alamofireSession:autoRefreshToken:) instead. This initializer will be removed in a future version." ) + @_disfavoredOverload public init( - url: URL, + url: URL? = nil, headers: [String: String] = [:], - flowType: AuthFlowType = Configuration.defaultFlowType, + flowType: AuthFlowType = Self.defaultFlowType, + redirectToURL: URL? = nil, + storageKey: String? = nil, localStorage: any AuthLocalStorage, + logger: (any SupabaseLogger)? = nil, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, - fetch: @escaping AuthClient.FetchHandler = { try await URLSession.shared.data(for: $0) } + fetch: @escaping AuthClient.FetchHandler, + autoRefreshToken: Bool = Self.defaultAutoRefreshToken ) { self.init( url: url, headers: headers, flowType: flowType, + redirectToURL: redirectToURL, + storageKey: storageKey, localStorage: localStorage, - logger: nil, + logger: logger, encoder: encoder, decoder: decoder, - fetch: fetch + fetch: fetch, + alamofireSession: .default, + autoRefreshToken: autoRefreshToken + ) + } +} + +extension AuthClient { + /// Initializes a AuthClient Configuration with optional parameters. + /// + /// - Parameters: + /// - url: The base URL of the Auth server. + /// - headers: Custom headers to be included in requests. + /// - flowType: The authentication flow type. + /// - localStorage: The storage mechanism for local data. + /// - encoder: The JSON encoder to use for encoding requests. + /// - decoder: The JSON decoder to use for decoding responses. + /// - fetch: The asynchronous fetch handler for network requests. + @available( + *, + deprecated, + message: + "Replace usages of this initializer with new init(url:headers:flowType:redirectToURL:storageKey:localStorage:logger:encoder:decoder:alamofireSession:autoRefreshToken:)" + ) + public init( + url: URL, + headers: [String: String] = [:], + flowType: AuthFlowType = Configuration.defaultFlowType, + localStorage: any AuthLocalStorage, + encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, + decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, + fetch: @escaping AuthClient.FetchHandler = { try await URLSession.shared.data(for: $0) } + ) { + self.init( + configuration: Configuration( + url: url, + headers: headers, + flowType: flowType, + localStorage: localStorage, + encoder: encoder, + decoder: decoder, + fetch: fetch + ) + ) + } + + /// Initializes a AuthClient with optional parameters. + /// + /// - Parameters: + /// - url: The base URL of the Auth server. + /// - headers: Custom headers to be included in requests. + /// - flowType: The authentication flow type. + /// - redirectToURL: Default URL to be used for redirect on the flows that requires it. + /// - storageKey: Optional key name used for storing tokens in local storage. + /// - localStorage: The storage mechanism for local data. + /// - logger: The logger to use. + /// - encoder: The JSON encoder to use for encoding requests. + /// - decoder: The JSON decoder to use for decoding responses. + /// - fetch: The asynchronous fetch handler for network requests. + /// - autoRefreshToken: Set to `true` if you want to automatically refresh the token before expiring. + @available( + *, + deprecated, + message: + "Use init(url:headers:flowType:redirectToURL:storageKey:localStorage:logger:encoder:decoder:alamofireSession:autoRefreshToken:) instead. This initializer will be removed in a future version." + ) + @_disfavoredOverload + public init( + url: URL? = nil, + headers: [String: String] = [:], + flowType: AuthFlowType = Configuration.defaultFlowType, + redirectToURL: URL? = nil, + storageKey: String? = nil, + localStorage: any AuthLocalStorage, + logger: (any SupabaseLogger)? = nil, + encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, + decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, + fetch: @escaping AuthClient.FetchHandler, + autoRefreshToken: Bool = AuthClient.Configuration.defaultAutoRefreshToken + ) { + self.init( + configuration: Configuration( + url: url, + headers: headers, + flowType: flowType, + redirectToURL: redirectToURL, + storageKey: storageKey, + localStorage: localStorage, + logger: logger, + encoder: encoder, + decoder: decoder, + fetch: fetch, + autoRefreshToken: autoRefreshToken + ) ) } } diff --git a/Sources/Auth/Internal/APIClient.swift b/Sources/Auth/Internal/APIClient.swift index 3a5bae1b6..22050f536 100644 --- a/Sources/Auth/Internal/APIClient.swift +++ b/Sources/Auth/Internal/APIClient.swift @@ -1,22 +1,25 @@ +import Alamofire import Foundation import HTTPTypes -extension HTTPClient { - init(configuration: AuthClient.Configuration) { - var interceptors: [any HTTPClientInterceptor] = [] - if let logger = configuration.logger { - interceptors.append(LoggerInterceptor(logger: logger)) - } +func makeHTTPClient(configuration: AuthClient.Configuration) -> any HTTPClientType { + var interceptors: [any HTTPClientInterceptor] = [] + if let logger = configuration.logger { + interceptors.append(LoggerInterceptor(logger: logger)) + } - interceptors.append( - RetryRequestInterceptor( - retryableHTTPMethods: RetryRequestInterceptor.defaultRetryableHTTPMethods.union( - [.post] // Add POST method so refresh token are also retried. - ) + interceptors.append( + RetryRequestInterceptor( + retryableHTTPMethods: RetryRequestInterceptor.defaultRetryableHTTPMethods.union( + [.post] // Add POST method so refresh token are also retried. ) ) + ) - self.init(fetch: configuration.fetch, interceptors: interceptors) + if let fetch = configuration.fetch { + return HTTPClient(fetch: fetch, interceptors: interceptors) + } else { + return AlamofireHTTPClient(session: configuration.alamofireSession) } } diff --git a/Sources/PostgREST/Deprecated.swift b/Sources/PostgREST/Deprecated.swift index da8fe3459..6043b7c50 100644 --- a/Sources/PostgREST/Deprecated.swift +++ b/Sources/PostgREST/Deprecated.swift @@ -5,6 +5,7 @@ // Created by Guilherme Souza on 16/01/24. // +import Alamofire import Foundation #if canImport(FoundationNetworking) @@ -24,7 +25,7 @@ extension PostgrestClient.Configuration { *, deprecated, message: - "Replace usages of this initializer with new init(url:schema:headers:logger:fetch:encoder:decoder:)" + "Replace usages of this initializer with new init(url:schema:headers:logger:alamofireSession:encoder:decoder:)" ) public init( url: URL, @@ -40,32 +41,34 @@ extension PostgrestClient.Configuration { headers: headers, logger: nil, fetch: fetch, + alamofireSession: .default, encoder: encoder, decoder: decoder ) } -} -extension PostgrestClient { - /// Creates a PostgREST client with the specified parameters. + /// Initializes a new configuration for the PostgREST client. /// - Parameters: /// - url: The URL of the PostgREST server. /// - schema: The schema to use. /// - headers: The headers to include in requests. - /// - session: The URLSession to use for requests. + /// - logger: The logger to use. + /// - fetch: The fetch handler to use for requests. /// - encoder: The JSONEncoder to use for encoding. /// - decoder: The JSONDecoder to use for decoding. @available( *, deprecated, message: - "Replace usages of this initializer with new init(url:schema:headers:logger:fetch:encoder:decoder:)" + "Use init(url:schema:headers:logger:alamofireSession:encoder:decoder:) instead. This initializer will be removed in a future version." ) - public convenience init( + @_disfavoredOverload + public init( url: URL, schema: String? = nil, headers: [String: String] = [:], - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + logger: (any SupabaseLogger)? = nil, + fetch: @escaping PostgrestClient.FetchHandler, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { @@ -73,14 +76,90 @@ extension PostgrestClient { url: url, schema: schema, headers: headers, - logger: nil, + logger: logger, fetch: fetch, + alamofireSession: .default, encoder: encoder, decoder: decoder ) } } +extension PostgrestClient { + /// Creates a PostgREST client with the specified parameters. + /// - Parameters: + /// - url: The URL of the PostgREST server. + /// - schema: The schema to use. + /// - headers: The headers to include in requests. + /// - fetch: The fetch handler to use for requests. + /// - encoder: The JSONEncoder to use for encoding. + /// - decoder: The JSONDecoder to use for decoding. + @available( + *, + deprecated, + message: + "Replace usages of this initializer with new init(url:schema:headers:logger:alamofireSession:encoder:decoder:)" + ) + public convenience init( + url: URL, + schema: String? = nil, + headers: [String: String] = [:], + fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, + decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder + ) { + self.init( + configuration: Configuration( + url: url, + schema: schema, + headers: headers, + logger: nil, + fetch: fetch, + encoder: encoder, + decoder: decoder + ) + ) + } + + /// Creates a PostgREST client with the specified parameters. + /// - Parameters: + /// - url: The URL of the PostgREST server. + /// - schema: The schema to use. + /// - headers: The headers to include in requests. + /// - logger: The logger to use. + /// - fetch: The fetch handler to use for requests. + /// - encoder: The JSONEncoder to use for encoding. + /// - decoder: The JSONDecoder to use for decoding. + @available( + *, + deprecated, + message: + "Use init(url:schema:headers:logger:alamofireSession:encoder:decoder:) instead. This initializer will be removed in a future version." + ) + @_disfavoredOverload + public convenience init( + url: URL, + schema: String? = nil, + headers: [String: String] = [:], + logger: (any SupabaseLogger)? = nil, + fetch: @escaping FetchHandler, + encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, + decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder + ) { + self.init( + configuration: Configuration( + url: url, + schema: schema, + headers: headers, + logger: logger, + fetch: fetch, + encoder: encoder, + decoder: decoder + ) + ) + } +} + extension PostgrestFilterBuilder { @available(*, deprecated, renamed: "like(_:pattern:)") diff --git a/Sources/PostgREST/PostgrestBuilder.swift b/Sources/PostgREST/PostgrestBuilder.swift index 2f91af44e..767a8fbf1 100644 --- a/Sources/PostgREST/PostgrestBuilder.swift +++ b/Sources/PostgREST/PostgrestBuilder.swift @@ -26,13 +26,7 @@ public class PostgrestBuilder: @unchecked Sendable { request: Helpers.HTTPRequest ) { self.configuration = configuration - - var interceptors: [any HTTPClientInterceptor] = [] - if let logger = configuration.logger { - interceptors.append(LoggerInterceptor(logger: logger)) - } - - http = HTTPClient(fetch: configuration.fetch, interceptors: interceptors) + http = configuration.http mutableState = LockIsolated( MutableState( diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index 903cee75c..aad9d4d23 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -1,3 +1,4 @@ +import Alamofire import ConcurrencyExtras import Foundation import HTTPTypes @@ -22,6 +23,7 @@ public final class PostgrestClient: Sendable { public var encoder: JSONEncoder public var decoder: JSONDecoder + let http: any HTTPClientType let logger: (any SupabaseLogger)? /// Creates a PostgREST client. @@ -30,7 +32,7 @@ public final class PostgrestClient: Sendable { /// - schema: Postgres schema to switch to. /// - headers: Custom headers. /// - logger: The logger to use. - /// - fetch: Custom fetch. + /// - alamofireSession: Alamofire session to use for making requests. /// - encoder: The JSONEncoder to use for encoding. /// - decoder: The JSONDecoder to use for decoding. public init( @@ -38,17 +40,52 @@ public final class PostgrestClient: Sendable { schema: String? = nil, headers: [String: String] = [:], logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + alamofireSession: Alamofire.Session = .default, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder + ) { + self.init( + url: url, + schema: schema, + headers: headers, + logger: logger, + fetch: { try await alamofireSession.session.data(for: $0) }, + alamofireSession: alamofireSession, + encoder: encoder, + decoder: decoder + ) + } + + init( + url: URL, + schema: String?, + headers: [String: String], + logger: (any SupabaseLogger)?, + fetch: FetchHandler?, + alamofireSession: Alamofire.Session, + encoder: JSONEncoder, + decoder: JSONDecoder ) { self.url = url self.schema = schema self.headers = headers self.logger = logger - self.fetch = fetch self.encoder = encoder self.decoder = decoder + + var interceptors: [any HTTPClientInterceptor] = [] + if let logger { + interceptors.append(LoggerInterceptor(logger: logger)) + } + + self.http = + if let fetch { + HTTPClient(fetch: fetch, interceptors: interceptors) + } else { + AlamofireHTTPClient(session: alamofireSession) + } + + self.fetch = fetch ?? { try await alamofireSession.session.data(for: $0) } } } @@ -70,7 +107,7 @@ public final class PostgrestClient: Sendable { /// - schema: Postgres schema to switch to. /// - headers: Custom headers. /// - logger: The logger to use. - /// - fetch: Custom fetch. + /// - alamofireSession: Alamofire session to use for making requests. /// - encoder: The JSONEncoder to use for encoding. /// - decoder: The JSONDecoder to use for decoding. public convenience init( @@ -78,7 +115,7 @@ public final class PostgrestClient: Sendable { schema: String? = nil, headers: [String: String] = [:], logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) }, + alamofireSession: Alamofire.Session = .default, encoder: JSONEncoder = PostgrestClient.Configuration.jsonEncoder, decoder: JSONDecoder = PostgrestClient.Configuration.jsonDecoder ) { @@ -88,7 +125,7 @@ public final class PostgrestClient: Sendable { schema: schema, headers: headers, logger: logger, - fetch: fetch, + alamofireSession: alamofireSession, encoder: encoder, decoder: decoder ) diff --git a/Sources/Storage/Deprecated.swift b/Sources/Storage/Deprecated.swift index ed39b06b4..624f0b03d 100644 --- a/Sources/Storage/Deprecated.swift +++ b/Sources/Storage/Deprecated.swift @@ -5,6 +5,7 @@ // Created by Guilherme Souza on 16/01/24. // +import Alamofire import Foundation extension StorageClientConfiguration { @@ -12,7 +13,7 @@ extension StorageClientConfiguration { *, deprecated, message: - "Replace usages of this initializer with new init(url:headers:encoder:decoder:session:logger)" + "Replace usages of this initializer with new init(url:headers:encoder:decoder:alamofireSession:logger:useNewHostname:)" ) public init( url: URL, @@ -27,7 +28,37 @@ extension StorageClientConfiguration { encoder: encoder, decoder: decoder, session: session, - logger: nil + alamofireSession: .default, + logger: nil, + useNewHostname: false + ) + } + + @available( + *, + deprecated, + message: + "Use init(url:headers:encoder:decoder:alamofireSession:logger:useNewHostname:) instead. This initializer will be removed in a future version." + ) + @_disfavoredOverload + public init( + url: URL, + headers: [String: String], + encoder: JSONEncoder = .defaultStorageEncoder, + decoder: JSONDecoder = .defaultStorageDecoder, + session: StorageHTTPSession, + logger: (any SupabaseLogger)? = nil, + useNewHostname: Bool = false + ) { + self.init( + url: url, + headers: headers, + encoder: encoder, + decoder: decoder, + session: session, + alamofireSession: .default, + logger: logger, + useNewHostname: useNewHostname ) } } diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index c3f3ac422..710aad21b 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -39,16 +39,7 @@ public class StorageApi: @unchecked Sendable { } self.configuration = configuration - - var interceptors: [any HTTPClientInterceptor] = [] - if let logger = configuration.logger { - interceptors.append(LoggerInterceptor(logger: logger)) - } - - http = HTTPClient( - fetch: configuration.session.fetch, - interceptors: interceptors - ) + http = configuration.http } @discardableResult diff --git a/Sources/Storage/SupabaseStorage.swift b/Sources/Storage/SupabaseStorage.swift index ba043c8b8..30d6699f9 100644 --- a/Sources/Storage/SupabaseStorage.swift +++ b/Sources/Storage/SupabaseStorage.swift @@ -1,3 +1,4 @@ +import Alamofire import Foundation public struct StorageClientConfiguration: Sendable { @@ -5,6 +6,11 @@ public struct StorageClientConfiguration: Sendable { public var headers: [String: String] public let encoder: JSONEncoder public let decoder: JSONDecoder + let http: any HTTPClientType + @available( + *, deprecated, + message: "Use alamofireSession instead. This will be removed in a future version." + ) public let session: StorageHTTPSession public let logger: (any SupabaseLogger)? public let useNewHostname: Bool @@ -14,17 +20,51 @@ public struct StorageClientConfiguration: Sendable { headers: [String: String], encoder: JSONEncoder = .defaultStorageEncoder, decoder: JSONDecoder = .defaultStorageDecoder, - session: StorageHTTPSession = .init(), + alamofireSession: Alamofire.Session = .default, logger: (any SupabaseLogger)? = nil, useNewHostname: Bool = false + ) { + self.init( + url: url, + headers: headers, + encoder: encoder, + decoder: decoder, + session: nil, + alamofireSession: alamofireSession, + logger: logger, + useNewHostname: useNewHostname + ) + } + + init( + url: URL, + headers: [String: String], + encoder: JSONEncoder, + decoder: JSONDecoder, + session: StorageHTTPSession?, + alamofireSession: Alamofire.Session, + logger: (any SupabaseLogger)?, + useNewHostname: Bool ) { self.url = url self.headers = headers self.encoder = encoder self.decoder = decoder - self.session = session + self.session = session ?? StorageHTTPSession() self.logger = logger self.useNewHostname = useNewHostname + + var interceptors: [any HTTPClientInterceptor] = [] + if let logger { + interceptors.append(LoggerInterceptor(logger: logger)) + } + + self.http = + if let session { + HTTPClient(fetch: session.fetch, interceptors: interceptors) + } else { + AlamofireHTTPClient(session: alamofireSession) + } } } diff --git a/Sources/Supabase/Deprecated.swift b/Sources/Supabase/Deprecated.swift index 2b5a4b291..399771843 100644 --- a/Sources/Supabase/Deprecated.swift +++ b/Sources/Supabase/Deprecated.swift @@ -28,6 +28,15 @@ extension SupabaseClient { } extension SupabaseClientOptions.GlobalOptions { + /// A session to use for making requests, defaults to `URLSession.shared`. + @available( + *, deprecated, + message: "Use alamofireSession instead. This will be removed in a future version." + ) + public var session: URLSession { + alamofireSession.session + } + @available( *, deprecated, message: @@ -41,7 +50,8 @@ extension SupabaseClientOptions.GlobalOptions { self.init( headers: headers, alamofireSession: Alamofire.Session( - session: session, delegate: SessionDelegate(), + session: session, + delegate: SessionDelegate(), rootQueue: DispatchQueue(label: "com.supabase.session")), logger: logger ) diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 8f77e3c52..63801718f 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -89,13 +89,6 @@ public struct SupabaseClientOptions: Sendable { /// Optional headers for initializing the client, it will be passed down to all sub-clients. public let headers: [String: String] - /// A session to use for making requests, defaults to `URLSession.shared`. - @available( - *, deprecated, - message: "Use alamofireSession instead. This will be removed in a future version." - ) - public let session: URLSession - /// Alamofire session to use for making requests, defaults to `Alamofire.Session.default`. public let alamofireSession: Alamofire.Session @@ -108,7 +101,6 @@ public struct SupabaseClientOptions: Sendable { logger: (any SupabaseLogger)? = nil ) { self.headers = headers - self.session = alamofireSession.session self.alamofireSession = alamofireSession self.logger = logger } From 7585e66df01340588627579ef41d910670ed9b7b Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Thu, 16 Oct 2025 17:44:18 -0300 Subject: [PATCH 3/8] fix: build error --- Sources/Auth/AuthClientConfiguration.swift | 3 +++ Sources/Auth/Internal/APIClient.swift | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Sources/Auth/AuthClientConfiguration.swift b/Sources/Auth/AuthClientConfiguration.swift index 7aacca2c8..f10a9c4c9 100644 --- a/Sources/Auth/AuthClientConfiguration.swift +++ b/Sources/Auth/AuthClientConfiguration.swift @@ -51,6 +51,8 @@ extension AuthClient { /// Set to `true` if you want to automatically refresh the token before expiring. public let autoRefreshToken: Bool + let initWithFetch: Bool + /// Initializes a AuthClient Configuration with optional parameters. /// /// - Parameters: @@ -122,6 +124,7 @@ extension AuthClient { self.fetch = fetch ?? { try await alamofireSession.session.data(for: $0) } self.alamofireSession = alamofireSession self.autoRefreshToken = autoRefreshToken + self.initWithFetch = fetch != nil } } diff --git a/Sources/Auth/Internal/APIClient.swift b/Sources/Auth/Internal/APIClient.swift index 22050f536..056543bc1 100644 --- a/Sources/Auth/Internal/APIClient.swift +++ b/Sources/Auth/Internal/APIClient.swift @@ -16,8 +16,8 @@ func makeHTTPClient(configuration: AuthClient.Configuration) -> any HTTPClientTy ) ) - if let fetch = configuration.fetch { - return HTTPClient(fetch: fetch, interceptors: interceptors) + if configuration.initWithFetch { + return HTTPClient(fetch: configuration.fetch, interceptors: interceptors) } else { return AlamofireHTTPClient(session: configuration.alamofireSession) } From bd81543cea89ebbeebf765a21881e18ada31e8cc Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 20 Oct 2025 16:44:25 -0300 Subject: [PATCH 4/8] feat: use Alamofire session for initializing sub-clients --- Sources/Supabase/Alamofire+Auth.swift | 53 +++++++++++++++++++++++++++ Sources/Supabase/SupabaseClient.swift | 51 ++++++++++---------------- 2 files changed, 72 insertions(+), 32 deletions(-) create mode 100644 Sources/Supabase/Alamofire+Auth.swift diff --git a/Sources/Supabase/Alamofire+Auth.swift b/Sources/Supabase/Alamofire+Auth.swift new file mode 100644 index 000000000..7b17ed66a --- /dev/null +++ b/Sources/Supabase/Alamofire+Auth.swift @@ -0,0 +1,53 @@ +import Alamofire +import Foundation + +extension Alamofire.Session { + /// Returns a new session with the authentication adapter added. + /// - Parameter getAccessToken: A closure that returns the access token. + /// - Returns: A new session with the authentication adapter added. + func authenticated( + getAccessToken: @escaping @Sendable () async throws -> String? + ) -> Alamofire.Session { + let interceptor = + self.interceptor != nil + ? Interceptor( + adapters: [AuthenticationAdapter(getAccessToken: getAccessToken)], + interceptors: [self.interceptor!]) + : Interceptor(adapters: [AuthenticationAdapter(getAccessToken: getAccessToken)]) + return Alamofire.Session( + configuration: self.sessionConfiguration, + delegate: self.delegate, + rootQueue: self.rootQueue, + startRequestsImmediately: self.startRequestsImmediately, + requestQueue: self.requestQueue, + serializationQueue: self.serializationQueue, + interceptor: interceptor, + serverTrustManager: self.serverTrustManager, + redirectHandler: self.redirectHandler, + cachedResponseHandler: self.cachedResponseHandler, + eventMonitors: [self.eventMonitor] + ) + } +} + +private struct AuthenticationAdapter: RequestAdapter { + + let getAccessToken: @Sendable () async throws -> String? + + func adapt( + _ urlRequest: URLRequest, + for session: Alamofire.Session, + completion: @escaping @Sendable (Result) -> Void + ) { + Task { + let token = try? await getAccessToken() + + var request = urlRequest + if let token { + request.headers.add(.authorization(bearerToken: token)) + } + + completion(.success(request)) + } + } +} diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index b419a94e8..afe0d0e00 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -1,3 +1,4 @@ +import Alamofire import ConcurrencyExtras import Foundation import HTTPTypes @@ -39,7 +40,7 @@ public final class SupabaseClient: Sendable { schema: options.db.schema, headers: headers, logger: options.global.logger, - fetch: fetchWithAuth, + alamofireSession: authenticatedAlamofireSession, encoder: options.db.encoder, decoder: options.db.decoder ) @@ -57,7 +58,7 @@ public final class SupabaseClient: Sendable { configuration: StorageClientConfiguration( url: storageURL, headers: headers, - session: StorageHTTPSession(fetch: fetchWithAuth, upload: uploadWithAuth), + alamofireSession: authenticatedAlamofireSession, logger: options.global.logger, useNewHostname: options.storage.useNewHostname ) @@ -89,7 +90,7 @@ public final class SupabaseClient: Sendable { headers: headers, region: options.functions.region, logger: options.global.logger, - fetch: fetchWithAuth + alamofireSession: authenticatedAlamofireSession ) } @@ -113,12 +114,23 @@ public final class SupabaseClient: Sendable { var realtime: RealtimeClientV2? var changedAccessToken: String? + var authenticatedAlamofireSession: Alamofire.Session? } let mutableState = LockIsolated(MutableState()) - private var session: URLSession { - options.global.session + private var alamofireSession: Alamofire.Session { + options.global.alamofireSession + } + + private var authenticatedAlamofireSession: Alamofire.Session { + mutableState.withValue { + if $0.authenticatedAlamofireSession == nil { + $0.authenticatedAlamofireSession = alamofireSession.authenticated( + getAccessToken: _getAccessToken) + } + return $0.authenticatedAlamofireSession! + } } #if !os(Linux) && !os(Android) @@ -177,10 +189,7 @@ public final class SupabaseClient: Sendable { logger: options.global.logger, encoder: options.auth.encoder, decoder: options.auth.decoder, - fetch: { - // DON'T use `fetchWithAuth` method within the AuthClient as it may cause a deadlock. - try await options.global.session.data(for: $0) - }, + alamofireSession: options.global.alamofireSession, autoRefreshToken: options.auth.autoRefreshToken ) @@ -329,28 +338,6 @@ public final class SupabaseClient: Sendable { } @Sendable - private func fetchWithAuth(_ request: URLRequest) async throws -> (Data, URLResponse) { - try await session.data(for: adapt(request: request)) - } - - @Sendable - private func uploadWithAuth( - _ request: URLRequest, - from data: Data - ) async throws -> (Data, URLResponse) { - try await session.upload(for: adapt(request: request), from: data) - } - - private func adapt(request: URLRequest) async -> URLRequest { - let token = try? await _getAccessToken() - - var request = request - if let token { - request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") - } - return request - } - private func _getAccessToken() async throws -> String? { if let accessToken = options.auth.accessToken { try await accessToken() @@ -370,7 +357,7 @@ public final class SupabaseClient: Sendable { } } - private func handleTokenChanged(event: AuthChangeEvent, session: Session?) async { + private func handleTokenChanged(event: AuthChangeEvent, session: Auth.Session?) async { let accessToken: String? = mutableState.withValue { if [.initialSession, .signedIn, .tokenRefreshed].contains(event), $0.changedAccessToken != session?.accessToken From b69a5c498489dc9a524fa014bf56cd6366307194 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Mon, 20 Oct 2025 16:56:17 -0300 Subject: [PATCH 5/8] fix tests --- Sources/Functions/FunctionsClient.swift | 2 +- Sources/Helpers/HTTP/AlamofireHTTPClient.swift | 13 +++++++------ Sources/Helpers/HTTP/HTTPClient.swift | 2 +- Sources/Helpers/HTTP/HTTPRequest.swift | 2 +- Sources/Helpers/HTTP/LoggerInterceptor.swift | 2 +- Sources/Supabase/Deprecated.swift | 5 +---- Tests/FunctionsTests/FunctionsClientTests.swift | 8 ++------ 7 files changed, 14 insertions(+), 20 deletions(-) diff --git a/Sources/Functions/FunctionsClient.swift b/Sources/Functions/FunctionsClient.swift index ff59301a7..da334a5a0 100644 --- a/Sources/Functions/FunctionsClient.swift +++ b/Sources/Functions/FunctionsClient.swift @@ -238,7 +238,7 @@ public final class FunctionsClient: Sendable { let session = URLSession( configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil) - let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest + let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest! let task = session.dataTask(with: urlRequest) task.resume() diff --git a/Sources/Helpers/HTTP/AlamofireHTTPClient.swift b/Sources/Helpers/HTTP/AlamofireHTTPClient.swift index d52d5517f..2615b2d92 100644 --- a/Sources/Helpers/HTTP/AlamofireHTTPClient.swift +++ b/Sources/Helpers/HTTP/AlamofireHTTPClient.swift @@ -1,14 +1,15 @@ import Alamofire import ConcurrencyExtras import Foundation +import HTTPTypesFoundation extension HTTPRequest: URLRequestConvertible { - package func asURLRequest() throws -> URLRequest { - guard let urlRequest = self.urlRequest else { - throw AFError.invalidURL(url: self.url.absoluteString) - } - return urlRequest - } +// package func asURLRequest() throws -> URLRequest { +// guard let urlRequest = self.urlRequest else { +// throw AFError.invalidURL(url: self.url.absoluteString) +// } +// return urlRequest +// } } package struct AlamofireHTTPClient: HTTPClientType { diff --git a/Sources/Helpers/HTTP/HTTPClient.swift b/Sources/Helpers/HTTP/HTTPClient.swift index 164463037..713ba1084 100644 --- a/Sources/Helpers/HTTP/HTTPClient.swift +++ b/Sources/Helpers/HTTP/HTTPClient.swift @@ -30,7 +30,7 @@ package actor HTTPClient: HTTPClientType { package func send(_ request: HTTPRequest) async throws -> HTTPResponse { var next: @Sendable (HTTPRequest) async throws -> HTTPResponse = { _request in let urlRequest = _request.urlRequest - let (data, response) = try await self.fetch(urlRequest) + let (data, response) = try await self.fetch(urlRequest!) guard let httpURLResponse = response as? HTTPURLResponse else { throw URLError(.badServerResponse) } diff --git a/Sources/Helpers/HTTP/HTTPRequest.swift b/Sources/Helpers/HTTP/HTTPRequest.swift index c67f78aae..ffb447975 100644 --- a/Sources/Helpers/HTTP/HTTPRequest.swift +++ b/Sources/Helpers/HTTP/HTTPRequest.swift @@ -48,7 +48,7 @@ package struct HTTPRequest: Sendable { self.init(url: url, method: method, query: query, headers: headers, body: body, timeoutInterval: timeoutInterval) } - package var urlRequest: URLRequest { + package func asURLRequest() throws -> URLRequest { var urlRequest = URLRequest(url: query.isEmpty ? url : url.appendingQueryItems(query), timeoutInterval: timeoutInterval) urlRequest.httpMethod = method.rawValue urlRequest.allHTTPHeaderFields = .init(headers.map { ($0.name.rawName, $0.value) }) { $1 } diff --git a/Sources/Helpers/HTTP/LoggerInterceptor.swift b/Sources/Helpers/HTTP/LoggerInterceptor.swift index e58819535..7f760d8b5 100644 --- a/Sources/Helpers/HTTP/LoggerInterceptor.swift +++ b/Sources/Helpers/HTTP/LoggerInterceptor.swift @@ -20,7 +20,7 @@ package struct LoggerInterceptor: HTTPClientInterceptor { ) async throws -> HTTPResponse { let id = UUID().uuidString return try await SupabaseLoggerTaskLocal.$additionalContext.withValue(merging: ["requestID": .string(id)]) { - let urlRequest = request.urlRequest + let urlRequest = request.urlRequest! logger.verbose( """ diff --git a/Sources/Supabase/Deprecated.swift b/Sources/Supabase/Deprecated.swift index 399771843..21f317668 100644 --- a/Sources/Supabase/Deprecated.swift +++ b/Sources/Supabase/Deprecated.swift @@ -49,10 +49,7 @@ extension SupabaseClientOptions.GlobalOptions { ) { self.init( headers: headers, - alamofireSession: Alamofire.Session( - session: session, - delegate: SessionDelegate(), - rootQueue: DispatchQueue(label: "com.supabase.session")), + alamofireSession: .default, // TODO: check how to derive Alamofire.Session from URLSession logger: logger ) } diff --git a/Tests/FunctionsTests/FunctionsClientTests.swift b/Tests/FunctionsTests/FunctionsClientTests.swift index 652a1a9f8..4f41d906c 100644 --- a/Tests/FunctionsTests/FunctionsClientTests.swift +++ b/Tests/FunctionsTests/FunctionsClientTests.swift @@ -1,3 +1,4 @@ +import Alamofire import ConcurrencyExtras import HTTPTypes import InlineSnapshotTesting @@ -22,8 +23,6 @@ final class FunctionsClientTests: XCTestCase { return sessionConfiguration }() - lazy var session = URLSession(configuration: sessionConfiguration) - var region: String? lazy var sut = FunctionsClient( @@ -32,10 +31,7 @@ final class FunctionsClientTests: XCTestCase { "apikey": apiKey ], region: region, - fetch: { request in - try await self.session.data(for: request) - }, - sessionConfiguration: sessionConfiguration + alamofireSession: Alamofire.Session(configuration: sessionConfiguration) ) override func setUp() { From 62748616eab46ecb042a5c509fc2ed22c8f541c9 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 21 Oct 2025 12:03:54 -0300 Subject: [PATCH 6/8] functions add stream --- Sources/Functions/FunctionsClient.swift | 21 ++------------------- Sources/Helpers/HTTP/HTTPClient.swift | 7 ++++++- Sources/TestHelpers/HTTPClientMock.swift | 6 +++++- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/Sources/Functions/FunctionsClient.swift b/Sources/Functions/FunctionsClient.swift index da334a5a0..2be0dfb31 100644 --- a/Sources/Functions/FunctionsClient.swift +++ b/Sources/Functions/FunctionsClient.swift @@ -232,25 +232,8 @@ public final class FunctionsClient: Sendable { _ functionName: String, options invokeOptions: FunctionInvokeOptions = .init() ) -> AsyncThrowingStream { - let (stream, continuation) = AsyncThrowingStream.makeStream() - let delegate = StreamResponseDelegate(continuation: continuation) - - let session = URLSession( - configuration: sessionConfiguration, delegate: delegate, delegateQueue: nil) - - let urlRequest = buildRequest(functionName: functionName, options: invokeOptions).urlRequest! - - let task = session.dataTask(with: urlRequest) - task.resume() - - continuation.onTermination = { _ in - task.cancel() - - // Hold a strong reference to delegate until continuation terminates. - _ = delegate - } - - return stream + let request = buildRequest(functionName: functionName, options: invokeOptions) + return http.stream(request) } private func buildRequest(functionName: String, options: FunctionInvokeOptions) diff --git a/Sources/Helpers/HTTP/HTTPClient.swift b/Sources/Helpers/HTTP/HTTPClient.swift index 713ba1084..f585975c9 100644 --- a/Sources/Helpers/HTTP/HTTPClient.swift +++ b/Sources/Helpers/HTTP/HTTPClient.swift @@ -13,9 +13,10 @@ import Foundation package protocol HTTPClientType: Sendable { func send(_ request: HTTPRequest) async throws -> HTTPResponse + func stream(_ request: HTTPRequest) -> AsyncThrowingStream } -package actor HTTPClient: HTTPClientType { +package struct HTTPClient: HTTPClientType { let fetch: @Sendable (URLRequest) async throws -> (Data, URLResponse) let interceptors: [any HTTPClientInterceptor] @@ -46,6 +47,10 @@ package actor HTTPClient: HTTPClientType { return try await next(request) } + + package func stream(_ request: HTTPRequest) -> AsyncThrowingStream { + fatalError("Unsupported, please use AlamofireHTTPClient.") + } } package protocol HTTPClientInterceptor: Sendable { diff --git a/Sources/TestHelpers/HTTPClientMock.swift b/Sources/TestHelpers/HTTPClientMock.swift index 4b8abcd36..ede0af8a3 100644 --- a/Sources/TestHelpers/HTTPClientMock.swift +++ b/Sources/TestHelpers/HTTPClientMock.swift @@ -9,7 +9,7 @@ import ConcurrencyExtras import Foundation import XCTestDynamicOverlay -package actor HTTPClientMock: HTTPClientType { +package final class HTTPClientMock: HTTPClientType { package struct MockNotFound: Error {} private var mocks = [@Sendable (HTTPRequest) async throws -> HTTPResponse?]() @@ -61,4 +61,8 @@ package actor HTTPClientMock: HTTPClientType { XCTFail("Mock not found for: \(request)") throw MockNotFound() } + + package func stream(_ request: HTTPRequest) -> AsyncThrowingStream { + fatalError("Not supported") + } } From e8893372c19524f4b59792d34ad987a98cb0770f Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 21 Oct 2025 12:08:28 -0300 Subject: [PATCH 7/8] fix tests --- Tests/RealtimeTests/PushV2Tests.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/RealtimeTests/PushV2Tests.swift b/Tests/RealtimeTests/PushV2Tests.swift index 040eb4fc1..57bf0bac3 100644 --- a/Tests/RealtimeTests/PushV2Tests.swift +++ b/Tests/RealtimeTests/PushV2Tests.swift @@ -336,4 +336,8 @@ private struct MockHTTPClient: HTTPClientType { func send(_ request: HTTPRequest) async throws -> HTTPResponse { return HTTPResponse(data: Data(), response: HTTPURLResponse()) } + + func stream(_ request: HTTPRequest) -> AsyncThrowingStream { + .finished() + } } From 509f7490333c0473838cd23d0a1d9838d6a81ad6 Mon Sep 17 00:00:00 2001 From: Guilherme Souza Date: Tue, 21 Oct 2025 13:24:19 -0300 Subject: [PATCH 8/8] simplify function region --- Sources/Functions/Deprecated.swift | 19 +---- Sources/Functions/FunctionsClient.swift | 40 ++------- Sources/Functions/Types.swift | 81 +++++++------------ Sources/Supabase/Types.swift | 9 +-- .../FunctionsTests/FunctionsClientTests.swift | 6 +- 5 files changed, 41 insertions(+), 114 deletions(-) diff --git a/Sources/Functions/Deprecated.swift b/Sources/Functions/Deprecated.swift index ecc76c981..12f0fc94a 100644 --- a/Sources/Functions/Deprecated.swift +++ b/Sources/Functions/Deprecated.swift @@ -10,7 +10,7 @@ extension FunctionsClient { public convenience init( url: URL, headers: [String: String] = [:], - region: String? = nil, + region: FunctionRegion? = nil, logger: (any SupabaseLogger)? = nil, fetch: @escaping FetchHandler ) { @@ -23,21 +23,4 @@ extension FunctionsClient { alamofireSession: .default ) } - - @available( - *, deprecated, - message: - "Use init(url:headers:region:logger:alamofireSession:) instead. This initializer will be removed in a future version." - ) - public convenience init( - url: URL, - headers: [String: String] = [:], - region: FunctionRegion? = nil, - logger: (any SupabaseLogger)? = nil, - fetch: @escaping FetchHandler - ) { - self.init( - url: url, headers: headers, region: region?.rawValue, logger: logger, fetch: fetch, - alamofireSession: .default) - } } diff --git a/Sources/Functions/FunctionsClient.swift b/Sources/Functions/FunctionsClient.swift index 2be0dfb31..9a4902a19 100644 --- a/Sources/Functions/FunctionsClient.swift +++ b/Sources/Functions/FunctionsClient.swift @@ -27,7 +27,7 @@ public final class FunctionsClient: Sendable { let url: URL /// The Region to invoke the functions in. - let region: String? + let region: FunctionRegion? struct MutableState { /// Headers to be included in the requests. @@ -36,7 +36,6 @@ public final class FunctionsClient: Sendable { private let http: any HTTPClientType private let mutableState = LockIsolated(MutableState()) - private let sessionConfiguration: URLSessionConfiguration var headers: HTTPFields { mutableState.headers @@ -54,7 +53,7 @@ public final class FunctionsClient: Sendable { public convenience init( url: URL, headers: [String: String] = [:], - region: String? = nil, + region: FunctionRegion? = nil, logger: (any SupabaseLogger)? = nil, alamofireSession: Alamofire.Session = .default ) { @@ -71,7 +70,7 @@ public final class FunctionsClient: Sendable { convenience init( url: URL, headers: [String: String] = [:], - region: String? = nil, + region: FunctionRegion? = nil, logger: (any SupabaseLogger)? = nil, fetch: FetchHandler?, alamofireSession: Alamofire.Session @@ -92,22 +91,19 @@ public final class FunctionsClient: Sendable { url: url, headers: headers, region: region, - http: http, - sessionConfiguration: alamofireSession.session.configuration + http: http ) } init( url: URL, headers: [String: String], - region: String?, - http: any HTTPClientType, - sessionConfiguration: URLSessionConfiguration + region: FunctionRegion?, + http: any HTTPClientType ) { self.url = url self.region = region self.http = http - self.sessionConfiguration = sessionConfiguration mutableState.withValue { $0.headers = HTTPFields(headers) @@ -117,26 +113,6 @@ public final class FunctionsClient: Sendable { } } - /// Initializes a new instance of `FunctionsClient`. - /// - /// - Parameters: - /// - url: The base URL for the functions. - /// - headers: Headers to be included in the requests. (Default: empty dictionary) - /// - region: The Region to invoke the functions in. - /// - logger: SupabaseLogger instance to use. - /// - fetch: The fetch handler used to make requests. (Default: URLSession.shared.data(for:)) - public convenience init( - url: URL, - headers: [String: String] = [:], - region: FunctionRegion? = nil, - logger: (any SupabaseLogger)? = nil, - alamofireSession: Alamofire.Session = .default - ) { - self.init( - url: url, headers: headers, region: region?.rawValue, logger: logger, fetch: nil, - alamofireSession: alamofireSession) - } - /// Updates the authorization header. /// /// - Parameter token: The new JWT token sent in the authorization header. @@ -250,8 +226,8 @@ public final class FunctionsClient: Sendable { ) if let region = options.region ?? region { - request.headers[.xRegion] = region - query.appendOrUpdate(URLQueryItem(name: "forceFunctionRegion", value: region)) + request.headers[.xRegion] = region.rawValue + query.appendOrUpdate(URLQueryItem(name: "forceFunctionRegion", value: region.rawValue)) request.query = query } diff --git a/Sources/Functions/Types.swift b/Sources/Functions/Types.swift index e53f06fdd..5a9454120 100644 --- a/Sources/Functions/Types.swift +++ b/Sources/Functions/Types.swift @@ -13,7 +13,7 @@ public enum FunctionsError: Error, LocalizedError { switch self { case .relayError: "Relay Error invoking the Edge Function" - case let .httpError(code, _): + case .httpError(let code, _): "Edge Function returned a non-2xx status code: \(code)" } } @@ -28,7 +28,7 @@ public struct FunctionInvokeOptions: Sendable { /// Body data to be sent with the function invocation. let body: Data? /// The Region to invoke the function in. - let region: String? + let region: FunctionRegion? /// The query to be included in the function invocation. let query: [URLQueryItem] @@ -40,12 +40,11 @@ public struct FunctionInvokeOptions: Sendable { /// - headers: Headers to be included in the function invocation. (Default: empty dictionary) /// - region: The Region to invoke the function in. /// - body: The body data to be sent with the function invocation. (Default: nil) - @_disfavoredOverload public init( method: Method? = nil, query: [URLQueryItem] = [], headers: [String: String] = [:], - region: String? = nil, + region: FunctionRegion? = nil, body: some Encodable ) { var defaultHeaders = HTTPFields() @@ -76,12 +75,11 @@ public struct FunctionInvokeOptions: Sendable { /// - query: The query to be included in the function invocation. /// - headers: Headers to be included in the function invocation. (Default: empty dictionary) /// - region: The Region to invoke the function in. - @_disfavoredOverload public init( method: Method? = nil, query: [URLQueryItem] = [], headers: [String: String] = [:], - region: String? = nil + region: FunctionRegion? = nil ) { self.method = method self.headers = HTTPFields(headers) @@ -116,56 +114,31 @@ public struct FunctionInvokeOptions: Sendable { } } -public enum FunctionRegion: String, Sendable { - case apNortheast1 = "ap-northeast-1" - case apNortheast2 = "ap-northeast-2" - case apSouth1 = "ap-south-1" - case apSoutheast1 = "ap-southeast-1" - case apSoutheast2 = "ap-southeast-2" - case caCentral1 = "ca-central-1" - case euCentral1 = "eu-central-1" - case euWest1 = "eu-west-1" - case euWest2 = "eu-west-2" - case euWest3 = "eu-west-3" - case saEast1 = "sa-east-1" - case usEast1 = "us-east-1" - case usWest1 = "us-west-1" - case usWest2 = "us-west-2" -} +public struct FunctionRegion: RawRepresentable, Sendable { + public var rawValue: String -extension FunctionInvokeOptions { - /// Initializes the `FunctionInvokeOptions` structure. - /// - /// - Parameters: - /// - method: Method to use in the function invocation. - /// - headers: Headers to be included in the function invocation. (Default: empty dictionary) - /// - region: The Region to invoke the function in. - /// - body: The body data to be sent with the function invocation. (Default: nil) - public init( - method: Method? = nil, - headers: [String: String] = [:], - region: FunctionRegion? = nil, - body: some Encodable - ) { - self.init( - method: method, - headers: headers, - region: region?.rawValue, - body: body - ) + public init(rawValue: String) { + self.rawValue = rawValue } - /// Initializes the `FunctionInvokeOptions` structure. - /// - /// - Parameters: - /// - method: Method to use in the function invocation. - /// - headers: Headers to be included in the function invocation. (Default: empty dictionary) - /// - region: The Region to invoke the function in. - public init( - method: Method? = nil, - headers: [String: String] = [:], - region: FunctionRegion? = nil - ) { - self.init(method: method, headers: headers, region: region?.rawValue) + public static let apNortheast1 = FunctionRegion(rawValue: "ap-northeast-1") + public static let apNortheast2 = FunctionRegion(rawValue: "ap-northeast-2") + public static let apSouth1 = FunctionRegion(rawValue: "ap-south-1") + public static let apSoutheast1 = FunctionRegion(rawValue: "ap-southeast-1") + public static let apSoutheast2 = FunctionRegion(rawValue: "ap-southeast-2") + public static let caCentral1 = FunctionRegion(rawValue: "ca-central-1") + public static let euCentral1 = FunctionRegion(rawValue: "eu-central-1") + public static let euWest1 = FunctionRegion(rawValue: "eu-west-1") + public static let euWest2 = FunctionRegion(rawValue: "eu-west-2") + public static let euWest3 = FunctionRegion(rawValue: "eu-west-3") + public static let saEast1 = FunctionRegion(rawValue: "sa-east-1") + public static let usEast1 = FunctionRegion(rawValue: "us-east-1") + public static let usWest1 = FunctionRegion(rawValue: "us-west-1") + public static let usWest2 = FunctionRegion(rawValue: "us-west-2") +} + +extension FunctionRegion: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(rawValue: value) } } diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index 63801718f..0c9eb2efd 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -108,15 +108,10 @@ public struct SupabaseClientOptions: Sendable { public struct FunctionsOptions: Sendable { /// The Region to invoke the functions in. - public let region: String? - - @_disfavoredOverload - public init(region: String? = nil) { - self.region = region - } + public let region: FunctionRegion? public init(region: FunctionRegion? = nil) { - self.init(region: region?.rawValue) + self.region = region } } diff --git a/Tests/FunctionsTests/FunctionsClientTests.swift b/Tests/FunctionsTests/FunctionsClientTests.swift index 4f41d906c..4a518d368 100644 --- a/Tests/FunctionsTests/FunctionsClientTests.swift +++ b/Tests/FunctionsTests/FunctionsClientTests.swift @@ -23,7 +23,7 @@ final class FunctionsClientTests: XCTestCase { return sessionConfiguration }() - var region: String? + var region: FunctionRegion? lazy var sut = FunctionsClient( url: url, @@ -45,7 +45,7 @@ final class FunctionsClientTests: XCTestCase { headers: ["apikey": apiKey], region: .saEast1 ) - XCTAssertEqual(client.region, "sa-east-1") + XCTAssertEqual(client.region?.rawValue, "sa-east-1") XCTAssertEqual(client.headers[.init("apikey")!], apiKey) XCTAssertNotNil(client.headers[.init("X-Client-Info")!]) @@ -156,7 +156,7 @@ final class FunctionsClientTests: XCTestCase { } func testInvokeWithRegionDefinedInClient() async throws { - region = FunctionRegion.caCentral1.rawValue + region = FunctionRegion.caCentral1 Mock( url: url.appendingPathComponent("hello-world"),