From 5cea2aea9f293307a493578ff255a4d4bec866e3 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 11:47:42 -0400 Subject: [PATCH 01/10] [Functions] More Swift 6 improvements --- .../Sources/Callable+Codable.swift | 27 ++-- FirebaseFunctions/Sources/Functions.swift | 69 +++++----- .../Sources/FunctionsError.swift | 6 +- FirebaseFunctions/Sources/HTTPSCallable.swift | 123 +++++++++++++----- .../FirebaseDataEncoder.swift | 2 +- 5 files changed, 155 insertions(+), 72 deletions(-) diff --git a/FirebaseFunctions/Sources/Callable+Codable.swift b/FirebaseFunctions/Sources/Callable+Codable.swift index 287eff55ebb..9b41fef77d6 100644 --- a/FirebaseFunctions/Sources/Callable+Codable.swift +++ b/FirebaseFunctions/Sources/Callable+Codable.swift @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FirebaseSharedSwift +@preconcurrency import FirebaseSharedSwift import Foundation /// A `Callable` is a reference to a particular Callable HTTPS trigger in Cloud Functions. @@ -20,7 +20,7 @@ import Foundation /// - Note: If the Callable HTTPS trigger accepts no parameters, ``Never`` can be used for /// iOS 17.0+. Otherwise, a simple encodable placeholder type (e.g., /// `struct EmptyRequest: Encodable {}`) can be used. -public struct Callable { +public struct Callable: Sendable { /// The timeout to use when calling the function. Defaults to 70 seconds. public var timeoutInterval: TimeInterval { get { @@ -61,11 +61,10 @@ public struct Callable { /// - Parameter data: Parameters to pass to the trigger. /// - Parameter completion: The block to call when the HTTPS request has completed. public func call(_ data: Request, - completion: @escaping (Result) + completion: @escaping @MainActor (Result) -> Void) { do { let encoded = try encoder.encode(data) - callable.call(encoded) { result, error in do { if let result { @@ -81,7 +80,9 @@ public struct Callable { } } } catch { - completion(.failure(error)) + DispatchQueue.main.async { + completion(.failure(error)) + } } } @@ -108,7 +109,7 @@ public struct Callable { /// - data: Parameters to pass to the trigger. /// - completion: The block to call when the HTTPS request has completed. public func callAsFunction(_ data: Request, - completion: @escaping (Result) + completion: @escaping @MainActor (Result) -> Void) { call(data, completion: completion) } @@ -265,9 +266,9 @@ public extension Callable where Request: Sendable, Response: Sendable { /// - Returns: A stream wrapping responses yielded by the streaming callable function or /// a ``FunctionsError`` if an error occurred. func stream(_ data: Request? = nil) throws -> AsyncThrowingStream { - let encoded: Any + let encoded: SendableWrapper do { - encoded = try encoder.encode(data) + encoded = try SendableWrapper(value: encoder.encode(data)) } catch { throw FunctionsError(.invalidArgument, userInfo: [NSUnderlyingErrorKey: error]) } @@ -282,7 +283,7 @@ public extension Callable where Request: Sendable, Response: Sendable { // `StreamResponseProtocol`, we know the `Response` generic argument // is `StreamResponse<_, _>`. let responseJSON = switch response { - case .message(let json), .result(let json): json + case let .message(json), let .result(json): json } let response = try decoder.decode(Response.self, from: responseJSON) if response is StreamResponseProtocol { @@ -336,3 +337,11 @@ enum JSONStreamResponse { case message([String: Any]) case result([String: Any]) } + +// TODO: Remove need for below type by changing `FirebaseDataEncoder` to not returning `Any`. +/// This wrapper is only intended to be used for passing encoded data in the +/// `stream` function's hierarchy. When using, carefully audit that `value` is +/// only ever accessed in one isolation domain. +struct SendableWrapper: @unchecked Sendable { + let value: Any +} diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 54f01e21e9f..871ad8986d4 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -19,21 +19,25 @@ import FirebaseMessagingInterop import FirebaseSharedSwift import Foundation #if COCOAPODS - import GTMSessionFetcher + @preconcurrency import GTMSessionFetcher #else - import GTMSessionFetcherCore + @preconcurrency import GTMSessionFetcherCore #endif internal import FirebaseCoreExtension -final class AtomicBox { - private var _value: T +final class AtomicBox: Sendable { + private nonisolated(unsafe) var _value: T private let lock = NSLock() - public init(_ value: T) { + public init(_ value: T) where T: Sendable { _value = value } + public init(_ value: @Sendable () -> T) where T == Any { + _value = value() + } + public func value() -> T { lock.withLock { _value @@ -68,7 +72,7 @@ enum FunctionsConstants { } /// `Functions` is the client for Cloud Functions for a Firebase project. -@objc(FIRFunctions) open class Functions: NSObject { +@objc(FIRFunctions) public final class Functions: NSObject, Sendable { // MARK: - Private Variables /// The network client to use for http requests. @@ -82,7 +86,7 @@ enum FunctionsConstants { /// A map of active instances, grouped by app. Keys are FirebaseApp names and values are arrays /// containing all instances of Functions associated with the given app. - private nonisolated(unsafe) static var instances: AtomicBox<[String: [Functions]]> = + private static let instances: AtomicBox<[String: [Functions]]> = AtomicBox([:]) /// The custom domain to use for all functions references (optional). @@ -91,15 +95,19 @@ enum FunctionsConstants { /// The region to use for all function references. let region: String + private let _emulatorOrigin: AtomicBox + // MARK: - Public APIs /// The current emulator origin, or `nil` if it is not set. - open private(set) var emulatorOrigin: String? + public var emulatorOrigin: String? { + _emulatorOrigin.value() + } /// Creates a Cloud Functions client using the default or returns a pre-existing instance if it /// already exists. /// - Returns: A shared Functions instance initialized with the default `FirebaseApp`. - @objc(functions) open class func functions() -> Functions { + @objc(functions) public class func functions() -> Functions { return functions( app: FirebaseApp.app(), region: FunctionsConstants.defaultRegion, @@ -111,7 +119,7 @@ enum FunctionsConstants { /// instance if one already exists. /// - Parameter app: The app for the Firebase project. /// - Returns: A shared Functions instance initialized with the specified `FirebaseApp`. - @objc(functionsForApp:) open class func functions(app: FirebaseApp) -> Functions { + @objc(functionsForApp:) public class func functions(app: FirebaseApp) -> Functions { return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: nil) } @@ -119,7 +127,7 @@ enum FunctionsConstants { /// - Parameter region: The region for the HTTP trigger, such as `us-central1`. /// - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a /// custom region. - @objc(functionsForRegion:) open class func functions(region: String) -> Functions { + @objc(functionsForRegion:) public class func functions(region: String) -> Functions { return functions(app: FirebaseApp.app(), region: region, customDomain: nil) } @@ -129,7 +137,7 @@ enum FunctionsConstants { /// "https://mydomain.com". /// - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a /// custom HTTP trigger domain. - @objc(functionsForCustomDomain:) open class func functions(customDomain: String) -> Functions { + @objc(functionsForCustomDomain:) public class func functions(customDomain: String) -> Functions { return functions(app: FirebaseApp.app(), region: FunctionsConstants.defaultRegion, customDomain: customDomain) } @@ -140,8 +148,8 @@ enum FunctionsConstants { /// - app: The app for the Firebase project. /// - region: The region for the HTTP trigger, such as `us-central1`. /// - Returns: An instance of `Functions` with a custom app and region. - @objc(functionsForApp:region:) open class func functions(app: FirebaseApp, - region: String) -> Functions { + @objc(functionsForApp:region:) public class func functions(app: FirebaseApp, + region: String) -> Functions { return functions(app: app, region: region, customDomain: nil) } @@ -152,8 +160,8 @@ enum FunctionsConstants { /// - app: The app for the Firebase project. /// - customDomain: A custom domain for the HTTP trigger, such as `https://mydomain.com`. /// - Returns: An instance of `Functions` with a custom app and HTTP trigger domain. - @objc(functionsForApp:customDomain:) open class func functions(app: FirebaseApp, - customDomain: String) + @objc(functionsForApp:customDomain:) public class func functions(app: FirebaseApp, + customDomain: String) -> Functions { return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: customDomain) } @@ -161,7 +169,7 @@ enum FunctionsConstants { /// Creates a reference to the Callable HTTPS trigger with the given name. /// - Parameter name: The name of the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithName:) open func httpsCallable(_ name: String) -> HTTPSCallable { + @objc(HTTPSCallableWithName:) public func httpsCallable(_ name: String) -> HTTPSCallable { HTTPSCallable(functions: self, url: functionURL(for: name)!) } @@ -180,7 +188,7 @@ enum FunctionsConstants { /// Creates a reference to the Callable HTTPS trigger with the given name. /// - Parameter url: The URL of the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithURL:) open func httpsCallable(_ url: URL) -> HTTPSCallable { + @objc(HTTPSCallableWithURL:) public func httpsCallable(_ url: URL) -> HTTPSCallable { return HTTPSCallable(functions: self, url: url) } @@ -207,7 +215,7 @@ enum FunctionsConstants { /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud /// Functions invocations. - open func httpsCallable(_ name: String, requestAs: Request.Type = Request.self, responseAs: Response.Type = Response.self, @@ -235,7 +243,7 @@ enum FunctionsConstants { /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud /// Functions invocations. - open func httpsCallable(_ name: String, options: HTTPSCallableOptions, requestAs: Request.Type = Request.self, @@ -263,7 +271,7 @@ enum FunctionsConstants { /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud /// Functions invocations. - open func httpsCallable(_ url: URL, requestAs: Request.Type = Request.self, responseAs: Response.Type = Response.self, @@ -291,7 +299,7 @@ enum FunctionsConstants { /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud /// Functions invocations. - open func httpsCallable(_ url: URL, options: HTTPSCallableOptions, requestAs: Request.Type = Request.self, @@ -315,10 +323,12 @@ enum FunctionsConstants { * - host: The host of the local emulator, such as "localhost". * - port: The port of the local emulator, for example 5005. */ - @objc open func useEmulator(withHost host: String, port: Int) { + @objc public func useEmulator(withHost host: String, port: Int) { let prefix = host.hasPrefix("http") ? "" : "http://" let origin = String(format: "\(prefix)\(host):%li", port) - emulatorOrigin = origin + _emulatorOrigin.withLock { emulatorOrigin in + emulatorOrigin = origin + } } // MARK: - Private Funcs (or Internal for tests) @@ -365,7 +375,7 @@ enum FunctionsConstants { self.projectID = projectID self.region = region self.customDomain = customDomain - emulatorOrigin = nil + _emulatorOrigin = AtomicBox(nil) contextProvider = FunctionsContextProvider(auth: auth, messaging: messaging, appCheck: appCheck) @@ -414,7 +424,7 @@ enum FunctionsConstants { func callFunction(at url: URL, withObject data: Any?, options: HTTPSCallableOptions?, - timeout: TimeInterval) async throws -> HTTPSCallableResult { + timeout: TimeInterval) async throws -> sending HTTPSCallableResult { let context = try await contextProvider.context(options: options) let fetcher = try makeFetcher( url: url, @@ -461,7 +471,8 @@ enum FunctionsConstants { options: HTTPSCallableOptions?, timeout: TimeInterval, context: FunctionsContext, - completion: @escaping @MainActor (Result) -> Void) { + completion: @escaping @MainActor (Result) + -> Void) { let fetcher: GTMSessionFetcher do { fetcher = try makeFetcher( @@ -500,7 +511,7 @@ enum FunctionsConstants { @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) func stream(at url: URL, - data: Any?, + data: SendableWrapper?, options: HTTPSCallableOptions?, timeout: TimeInterval) -> AsyncThrowingStream { @@ -511,7 +522,7 @@ enum FunctionsConstants { let context = try await contextProvider.context(options: options) urlRequest = try makeRequestForStreamableContent( url: url, - data: data, + data: data?.value, options: options, timeout: timeout, context: context diff --git a/FirebaseFunctions/Sources/FunctionsError.swift b/FirebaseFunctions/Sources/FunctionsError.swift index f495f51e68e..7d1b5d0902b 100644 --- a/FirebaseFunctions/Sources/FunctionsError.swift +++ b/FirebaseFunctions/Sources/FunctionsError.swift @@ -217,9 +217,9 @@ struct FunctionsError: CustomNSError { } if code == .OK { - // Technically, there's an edge case where a developer could explicitly return an error code - // of - // OK, and we will treat it as success, but that seems reasonable. + // Technically, there's an edge case where a developer could explicitly + // return an error code of OK, and we will treat it as success, but that + // seems reasonable. return nil } diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index 671a7b67dce..670f51cc0b4 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -33,25 +33,23 @@ open class HTTPSCallableResult: NSObject { * A `HTTPSCallable` is a reference to a particular Callable HTTPS trigger in Cloud Functions. */ @objc(FIRHTTPSCallable) -open class HTTPSCallable: NSObject { +open class HTTPSCallable: NSObject, @unchecked Sendable { // MARK: - Private Properties - // The functions client to use for making calls. - private let functions: Functions - - private let url: URL - - private let options: HTTPSCallableOptions? + /// Until this class can be marked *checked* `Sendable`, it's implementation + /// is delegated to an auxialiary class that is checked Sendable. + private let sendableCallable: SendableHTTPSCallable // MARK: - Public Properties /// The timeout to use when calling the function. Defaults to 70 seconds. - @objc open var timeoutInterval: TimeInterval = 70 + @objc open var timeoutInterval: TimeInterval { + get { sendableCallable.timeoutInterval } + set { sendableCallable.timeoutInterval = newValue } + } init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) { - self.functions = functions - self.url = url - self.options = options + sendableCallable = SendableHTTPSCallable(functions: functions, url: url, options: options) } /// Executes this Callable HTTPS trigger asynchronously. @@ -75,9 +73,88 @@ open class HTTPSCallable: NSObject { /// - Parameters: /// - data: Parameters to pass to the trigger. /// - completion: The block to call when the HTTPS request has completed. - @objc(callWithObject:completion:) open func call(_ data: Any? = nil, + @objc(callWithObject:completion:) open func call(_ data: sending Any? = nil, completion: @escaping @MainActor (HTTPSCallableResult?, - Error?) -> Void) { + Error?) + -> Void) { + sendableCallable.call(data, completion: completion) + } + + /// Executes this Callable HTTPS trigger asynchronously. This API should only be used from + /// Objective-C. + /// + /// The request to the Cloud Functions backend made by this method automatically includes a + /// Firebase Installations ID token to identify the app instance. If a user is logged in with + /// Firebase Auth, an auth ID token for the user is also automatically included. + /// + /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect + /// information + /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It + /// resumes with a new FCM Token the next time you call this method. + /// + /// - Parameter completion: The block to call when the HTTPS request has completed. + @objc(callWithCompletion:) public func __call(completion: @escaping @MainActor (HTTPSCallableResult?, + Error?) -> Void) { + call(nil, completion: completion) + } + + /// Executes this Callable HTTPS trigger asynchronously. + /// + /// The request to the Cloud Functions backend made by this method automatically includes a + /// FCM token to identify the app instance. If a user is logged in with Firebase + /// Auth, an auth ID token for the user is also automatically included. + /// + /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect + /// information + /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It + /// resumes with a new FCM Token the next time you call this method. + /// + /// - Parameter data: Parameters to pass to the trigger. + /// - Throws: An error if the Cloud Functions invocation failed. + /// - Returns: The result of the call. + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + open func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult { + try await sendableCallable.call(data) + } + + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func stream(_ data: SendableWrapper? = nil) -> AsyncThrowingStream { + sendableCallable.stream(data) + } +} + +private final class SendableHTTPSCallable: Sendable { + // MARK: - Private Properties + + // The functions client to use for making calls. + private let functions: Functions + + private let url: URL + + private let options: HTTPSCallableOptions? + + // MARK: - Public Properties + + let _timeoutInterval: AtomicBox = .init(70) + + /// The timeout to use when calling the function. Defaults to 70 seconds. + var timeoutInterval: TimeInterval { + get { _timeoutInterval.value() } + set { + _timeoutInterval.withLock { timeoutInterval in + timeoutInterval = newValue + } + } + } + + init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) { + self.functions = functions + self.url = url + self.options = options + } + + func call(_ data: sending Any? = nil, + completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) { if #available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) { Task { do { @@ -110,21 +187,7 @@ open class HTTPSCallable: NSObject { } } - /// Executes this Callable HTTPS trigger asynchronously. This API should only be used from - /// Objective-C. - /// - /// The request to the Cloud Functions backend made by this method automatically includes a - /// Firebase Installations ID token to identify the app instance. If a user is logged in with - /// Firebase Auth, an auth ID token for the user is also automatically included. - /// - /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect - /// information - /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It - /// resumes with a new FCM Token the next time you call this method. - /// - /// - Parameter completion: The block to call when the HTTPS request has completed. - @objc(callWithCompletion:) public func __call(completion: @escaping (HTTPSCallableResult?, - Error?) -> Void) { + func __call(completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) { call(nil, completion: completion) } @@ -143,13 +206,13 @@ open class HTTPSCallable: NSObject { /// - Throws: An error if the Cloud Functions invocation failed. /// - Returns: The result of the call. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - open func call(_ data: Any? = nil) async throws -> HTTPSCallableResult { + func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult { try await functions .callFunction(at: url, withObject: data, options: options, timeout: timeoutInterval) } @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) - func stream(_ data: Any? = nil) -> AsyncThrowingStream { + func stream(_ data: SendableWrapper? = nil) -> AsyncThrowingStream { functions.stream(at: url, data: data, options: options, timeout: timeoutInterval) } } diff --git a/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift index cc3dc36bd30..b881486388e 100644 --- a/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift +++ b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift @@ -286,7 +286,7 @@ public class FirebaseDataEncoder { /// - returns: A new `Data` value containing the encoded JSON data. /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`. /// - throws: An error if any value throws an error during encoding. - open func encode(_ value: T) throws -> Any { + open func encode(_ value: T) throws -> sending Any { let encoder = __JSONEncoder(options: self.options) guard let topLevel = try encoder.box_(value) else { From 78f272f03b82d0f8c7359e3a9cb714312ea24ee0 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 11:48:53 -0400 Subject: [PATCH 02/10] Revert SharedSwift change --- .../third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift index b881486388e..cc3dc36bd30 100644 --- a/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift +++ b/FirebaseSharedSwift/Sources/third_party/FirebaseDataEncoder/FirebaseDataEncoder.swift @@ -286,7 +286,7 @@ public class FirebaseDataEncoder { /// - returns: A new `Data` value containing the encoded JSON data. /// - throws: `EncodingError.invalidValue` if a non-conforming floating-point value is encountered during encoding, and the encoding strategy is `.throw`. /// - throws: An error if any value throws an error during encoding. - open func encode(_ value: T) throws -> sending Any { + open func encode(_ value: T) throws -> Any { let encoder = __JSONEncoder(options: self.options) guard let topLevel = try encoder.box_(value) else { From af3c49e5c43b6c990bdfa701d0a2d29a3f18189e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 12:02:05 -0400 Subject: [PATCH 03/10] style --- FirebaseFunctions/Sources/HTTPSCallable.swift | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index 670f51cc0b4..d4d51fef95e 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -191,20 +191,6 @@ private final class SendableHTTPSCallable: Sendable { call(nil, completion: completion) } - /// Executes this Callable HTTPS trigger asynchronously. - /// - /// The request to the Cloud Functions backend made by this method automatically includes a - /// FCM token to identify the app instance. If a user is logged in with Firebase - /// Auth, an auth ID token for the user is also automatically included. - /// - /// Firebase Cloud Messaging sends data to the Firebase backend periodically to collect - /// information - /// regarding the app instance. To stop this, see `Messaging.deleteData()`. It - /// resumes with a new FCM Token the next time you call this method. - /// - /// - Parameter data: Parameters to pass to the trigger. - /// - Throws: An error if the Cloud Functions invocation failed. - /// - Returns: The result of the call. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult { try await functions From fcd44a1e8ae680573b047d002319c4a60cb91d61 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 13:08:18 -0400 Subject: [PATCH 04/10] Move unchecked Sendable --- FirebaseFunctions/Sources/Functions.swift | 42 +++++++++++------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 871ad8986d4..7e63b6b72c0 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -72,7 +72,7 @@ enum FunctionsConstants { } /// `Functions` is the client for Cloud Functions for a Firebase project. -@objc(FIRFunctions) public final class Functions: NSObject, Sendable { +@objc(FIRFunctions) open class Functions: NSObject, @unchecked Sendable { // MARK: - Private Variables /// The network client to use for http requests. @@ -100,14 +100,14 @@ enum FunctionsConstants { // MARK: - Public APIs /// The current emulator origin, or `nil` if it is not set. - public var emulatorOrigin: String? { + open var emulatorOrigin: String? { _emulatorOrigin.value() } /// Creates a Cloud Functions client using the default or returns a pre-existing instance if it /// already exists. /// - Returns: A shared Functions instance initialized with the default `FirebaseApp`. - @objc(functions) public class func functions() -> Functions { + @objc(functions) open class func functions() -> Functions { return functions( app: FirebaseApp.app(), region: FunctionsConstants.defaultRegion, @@ -119,7 +119,7 @@ enum FunctionsConstants { /// instance if one already exists. /// - Parameter app: The app for the Firebase project. /// - Returns: A shared Functions instance initialized with the specified `FirebaseApp`. - @objc(functionsForApp:) public class func functions(app: FirebaseApp) -> Functions { + @objc(functionsForApp:) open class func functions(app: FirebaseApp) -> Functions { return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: nil) } @@ -127,7 +127,7 @@ enum FunctionsConstants { /// - Parameter region: The region for the HTTP trigger, such as `us-central1`. /// - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a /// custom region. - @objc(functionsForRegion:) public class func functions(region: String) -> Functions { + @objc(functionsForRegion:) open class func functions(region: String) -> Functions { return functions(app: FirebaseApp.app(), region: region, customDomain: nil) } @@ -137,7 +137,7 @@ enum FunctionsConstants { /// "https://mydomain.com". /// - Returns: A shared Functions instance initialized with the default `FirebaseApp` and a /// custom HTTP trigger domain. - @objc(functionsForCustomDomain:) public class func functions(customDomain: String) -> Functions { + @objc(functionsForCustomDomain:) open class func functions(customDomain: String) -> Functions { return functions(app: FirebaseApp.app(), region: FunctionsConstants.defaultRegion, customDomain: customDomain) } @@ -148,8 +148,8 @@ enum FunctionsConstants { /// - app: The app for the Firebase project. /// - region: The region for the HTTP trigger, such as `us-central1`. /// - Returns: An instance of `Functions` with a custom app and region. - @objc(functionsForApp:region:) public class func functions(app: FirebaseApp, - region: String) -> Functions { + @objc(functionsForApp:region:) open class func functions(app: FirebaseApp, + region: String) -> Functions { return functions(app: app, region: region, customDomain: nil) } @@ -160,8 +160,8 @@ enum FunctionsConstants { /// - app: The app for the Firebase project. /// - customDomain: A custom domain for the HTTP trigger, such as `https://mydomain.com`. /// - Returns: An instance of `Functions` with a custom app and HTTP trigger domain. - @objc(functionsForApp:customDomain:) public class func functions(app: FirebaseApp, - customDomain: String) + @objc(functionsForApp:customDomain:) open class func functions(app: FirebaseApp, + customDomain: String) -> Functions { return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: customDomain) } @@ -169,7 +169,7 @@ enum FunctionsConstants { /// Creates a reference to the Callable HTTPS trigger with the given name. /// - Parameter name: The name of the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithName:) public func httpsCallable(_ name: String) -> HTTPSCallable { + @objc(HTTPSCallableWithName:) open func httpsCallable(_ name: String) -> HTTPSCallable { HTTPSCallable(functions: self, url: functionURL(for: name)!) } @@ -179,8 +179,8 @@ enum FunctionsConstants { /// - name: The name of the Callable HTTPS trigger. /// - options: The options with which to customize the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithName:options:) public func httpsCallable(_ name: String, - options: HTTPSCallableOptions) + @objc(HTTPSCallableWithName:options:) open func httpsCallable(_ name: String, + options: HTTPSCallableOptions) -> HTTPSCallable { HTTPSCallable(functions: self, url: functionURL(for: name)!, options: options) } @@ -188,7 +188,7 @@ enum FunctionsConstants { /// Creates a reference to the Callable HTTPS trigger with the given name. /// - Parameter url: The URL of the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithURL:) public func httpsCallable(_ url: URL) -> HTTPSCallable { + @objc(HTTPSCallableWithURL:) open func httpsCallable(_ url: URL) -> HTTPSCallable { return HTTPSCallable(functions: self, url: url) } @@ -198,8 +198,8 @@ enum FunctionsConstants { /// - url: The URL of the Callable HTTPS trigger. /// - options: The options with which to customize the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithURL:options:) public func httpsCallable(_ url: URL, - options: HTTPSCallableOptions) + @objc(HTTPSCallableWithURL:options:) open func httpsCallable(_ url: URL, + options: HTTPSCallableOptions) -> HTTPSCallable { return HTTPSCallable(functions: self, url: url, options: options) } @@ -215,7 +215,7 @@ enum FunctionsConstants { /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud /// Functions invocations. - public func httpsCallable(_ name: String, requestAs: Request.Type = Request.self, responseAs: Response.Type = Response.self, @@ -243,7 +243,7 @@ enum FunctionsConstants { /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud /// Functions invocations. - public func httpsCallable(_ name: String, options: HTTPSCallableOptions, requestAs: Request.Type = Request.self, @@ -271,7 +271,7 @@ enum FunctionsConstants { /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud /// Functions invocations. - public func httpsCallable(_ url: URL, requestAs: Request.Type = Request.self, responseAs: Response.Type = Response.self, @@ -299,7 +299,7 @@ enum FunctionsConstants { /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud /// Functions invocations. - public func httpsCallable(_ url: URL, options: HTTPSCallableOptions, requestAs: Request.Type = Request.self, @@ -323,7 +323,7 @@ enum FunctionsConstants { * - host: The host of the local emulator, such as "localhost". * - port: The port of the local emulator, for example 5005. */ - @objc public func useEmulator(withHost host: String, port: Int) { + @objc open func useEmulator(withHost host: String, port: Int) { let prefix = host.hasPrefix("http") ? "" : "http://" let origin = String(format: "\(prefix)\(host):%li", port) _emulatorOrigin.withLock { emulatorOrigin in From d006f60a4d3aa635abb180546ad55d2b8cd5f02d Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 13:09:31 -0400 Subject: [PATCH 05/10] Remove unused function --- FirebaseFunctions/Sources/Functions.swift | 4 ---- 1 file changed, 4 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 7e63b6b72c0..eb16f800ca5 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -34,10 +34,6 @@ final class AtomicBox: Sendable { _value = value } - public init(_ value: @Sendable () -> T) where T == Any { - _value = value() - } - public func value() -> T { lock.withLock { _value From 89a3e877066e646c4a7d9e4b60ff87c1b2439cc6 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 13:12:05 -0400 Subject: [PATCH 06/10] More fixes --- FirebaseFunctions/Sources/Functions.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index eb16f800ca5..8f04368d8d2 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -175,8 +175,8 @@ enum FunctionsConstants { /// - name: The name of the Callable HTTPS trigger. /// - options: The options with which to customize the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithName:options:) open func httpsCallable(_ name: String, - options: HTTPSCallableOptions) + @objc(HTTPSCallableWithName:options:) public func httpsCallable(_ name: String, + options: HTTPSCallableOptions) -> HTTPSCallable { HTTPSCallable(functions: self, url: functionURL(for: name)!, options: options) } @@ -194,8 +194,8 @@ enum FunctionsConstants { /// - url: The URL of the Callable HTTPS trigger. /// - options: The options with which to customize the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithURL:options:) open func httpsCallable(_ url: URL, - options: HTTPSCallableOptions) + @objc(HTTPSCallableWithURL:options:) public func httpsCallable(_ url: URL, + options: HTTPSCallableOptions) -> HTTPSCallable { return HTTPSCallable(functions: self, url: url, options: options) } From 088f8f331a39a7d77c9ba70fb4f0fe844c555e78 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 13:14:24 -0400 Subject: [PATCH 07/10] Try in extension --- FirebaseFunctions/Sources/HTTPSCallable.swift | 124 +++++++++--------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index d4d51fef95e..d2040de17c5 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -29,9 +29,7 @@ open class HTTPSCallableResult: NSObject { } } -/** - * A `HTTPSCallable` is a reference to a particular Callable HTTPS trigger in Cloud Functions. - */ +/// A `HTTPSCallable` is a reference to a particular Callable HTTPS trigger in Cloud Functions. @objc(FIRHTTPSCallable) open class HTTPSCallable: NSObject, @unchecked Sendable { // MARK: - Private Properties @@ -123,82 +121,84 @@ open class HTTPSCallable: NSObject, @unchecked Sendable { } } -private final class SendableHTTPSCallable: Sendable { - // MARK: - Private Properties +extension HTTPSCallable { + private final class SendableHTTPSCallable: Sendable { + // MARK: - Private Properties - // The functions client to use for making calls. - private let functions: Functions + // The functions client to use for making calls. + private let functions: Functions - private let url: URL + private let url: URL - private let options: HTTPSCallableOptions? + private let options: HTTPSCallableOptions? - // MARK: - Public Properties + // MARK: - Public Properties - let _timeoutInterval: AtomicBox = .init(70) + let _timeoutInterval: AtomicBox = .init(70) - /// The timeout to use when calling the function. Defaults to 70 seconds. - var timeoutInterval: TimeInterval { - get { _timeoutInterval.value() } - set { - _timeoutInterval.withLock { timeoutInterval in - timeoutInterval = newValue + /// The timeout to use when calling the function. Defaults to 70 seconds. + var timeoutInterval: TimeInterval { + get { _timeoutInterval.value() } + set { + _timeoutInterval.withLock { timeoutInterval in + timeoutInterval = newValue + } } } - } - init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) { - self.functions = functions - self.url = url - self.options = options - } + init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) { + self.functions = functions + self.url = url + self.options = options + } - func call(_ data: sending Any? = nil, - completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) { - if #available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) { - Task { - do { - let result = try await call(data) - await completion(result, nil) - } catch { - await completion(nil, error) - } - } - } else { - // This isn’t expected to ever be called because Functions - // doesn’t officially support the older platforms. - functions.callFunction( - at: url, - withObject: data, - options: options, - timeout: timeoutInterval - ) { result in - switch result { - case let .success(callableResult): - DispatchQueue.main.async { - completion(callableResult, nil) + func call(_ data: sending Any? = nil, + completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) { + if #available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) { + Task { + do { + let result = try await call(data) + await completion(result, nil) + } catch { + await completion(nil, error) } - case let .failure(error): - DispatchQueue.main.async { - completion(nil, error) + } + } else { + // This isn’t expected to ever be called because Functions + // doesn’t officially support the older platforms. + functions.callFunction( + at: url, + withObject: data, + options: options, + timeout: timeoutInterval + ) { result in + switch result { + case let .success(callableResult): + DispatchQueue.main.async { + completion(callableResult, nil) + } + case let .failure(error): + DispatchQueue.main.async { + completion(nil, error) + } } } } } - } - func __call(completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) { - call(nil, completion: completion) - } + func __call(completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) { + call(nil, completion: completion) + } - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult { - try await functions - .callFunction(at: url, withObject: data, options: options, timeout: timeoutInterval) - } + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) + func call(_ data: Any? = nil) async throws -> sending HTTPSCallableResult { + try await functions + .callFunction(at: url, withObject: data, options: options, timeout: timeoutInterval) + } - @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) - func stream(_ data: SendableWrapper? = nil) -> AsyncThrowingStream { - functions.stream(at: url, data: data, options: options, timeout: timeoutInterval) + @available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) + func stream(_ data: SendableWrapper? = nil) -> AsyncThrowingStream { + functions.stream(at: url, data: data, options: options, timeout: timeoutInterval) + } } } From 97491a5de27752223e8464c2fb08283fdb95bd75 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 13:15:46 -0400 Subject: [PATCH 08/10] done --- FirebaseFunctions/Sources/HTTPSCallable.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index d2040de17c5..78b79634d80 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -121,8 +121,8 @@ open class HTTPSCallable: NSObject, @unchecked Sendable { } } -extension HTTPSCallable { - private final class SendableHTTPSCallable: Sendable { +private extension HTTPSCallable { + final class SendableHTTPSCallable: Sendable { // MARK: - Private Properties // The functions client to use for making calls. From f82ef92e77743870c66cf6627e15e3c8d6a6c28a Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 16:35:25 -0400 Subject: [PATCH 09/10] Fix objc tests: --- FirebaseFunctions/Sources/HTTPSCallable.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index 78b79634d80..00b6ee37463 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -71,7 +71,7 @@ open class HTTPSCallable: NSObject, @unchecked Sendable { /// - Parameters: /// - data: Parameters to pass to the trigger. /// - completion: The block to call when the HTTPS request has completed. - @objc(callWithObject:completion:) open func call(_ data: sending Any? = nil, + @objc(callWithObject:completion:) open func call(_ data: Any? = nil, completion: @escaping @MainActor (HTTPSCallableResult?, Error?) -> Void) { From feb94a8a647853caa55e0fe4a249c51c96a24851 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Tue, 29 Apr 2025 16:51:12 -0400 Subject: [PATCH 10/10] combine fix --- FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift b/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift index 1db73d3930a..5db8b80fd77 100644 --- a/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift +++ b/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift @@ -28,14 +28,14 @@ import XCTest private let timeoutInterval: TimeInterval = 70.0 private let expectationTimeout: TimeInterval = 2 -class MockFunctions: Functions { +class MockFunctions: Functions, @unchecked Sendable { let mockCallFunction: () throws -> HTTPSCallableResult var verifyParameters: ((_ url: URL, _ data: Any?, _ timeout: TimeInterval) throws -> Void)? override func callFunction(at url: URL, withObject data: Any?, options: HTTPSCallableOptions?, - timeout: TimeInterval) async throws -> HTTPSCallableResult { + timeout: TimeInterval) async throws -> sending HTTPSCallableResult { try verifyParameters?(url, data, timeout) return try mockCallFunction() }