diff --git a/FirebaseFunctions/CHANGELOG.md b/FirebaseFunctions/CHANGELOG.md index 7931990a060..9bb5c1aa365 100644 --- a/FirebaseFunctions/CHANGELOG.md +++ b/FirebaseFunctions/CHANGELOG.md @@ -1,3 +1,8 @@ +# Unreleased +- [changed] **Breaking Change**: Mark `HTTPSCallable` and `HTTPSCallableOptions` + as `final` classes for Swift clients. This was to achieve Swift 6 checked + `Sendable` support. + # 11.12.0 - [fixed] Fix regression from 11.6.0 where `HTTPSCallable` did not invoke completion block on main thread (#14653). diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index ab839003e17..b03d5b8fccd 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -33,23 +33,34 @@ open class HTTPSCallableResult: NSObject { /// A `HTTPSCallable` is a reference to a particular Callable HTTPS trigger in Cloud Functions. @objc(FIRHTTPSCallable) -open class HTTPSCallable: NSObject, @unchecked Sendable { +public final class HTTPSCallable: NSObject, Sendable { // MARK: - Private Properties - /// Until this class can be marked *checked* `Sendable`, it's implementation - /// is delegated to an auxiliary class that is checked Sendable. - private let sendableCallable: SendableHTTPSCallable + // The functions client to use for making calls. + private let functions: Functions + + private let url: URL + + private let options: HTTPSCallableOptions? + + private let _timeoutInterval: AtomicBox = .init(70) // MARK: - Public Properties /// The timeout to use when calling the function. Defaults to 70 seconds. - @objc open var timeoutInterval: TimeInterval { - get { sendableCallable.timeoutInterval } - set { sendableCallable.timeoutInterval = newValue } + @objc public var timeoutInterval: TimeInterval { + get { _timeoutInterval.value() } + set { + _timeoutInterval.withLock { timeoutInterval in + timeoutInterval = newValue + } + } } init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) { - sendableCallable = SendableHTTPSCallable(functions: functions, url: url, options: options) + self.functions = functions + self.url = url + self.options = options } /// Executes this Callable HTTPS trigger asynchronously. @@ -74,11 +85,11 @@ open class HTTPSCallable: NSObject, @unchecked Sendable { /// - data: Parameters to pass to the trigger. /// - completion: The block to call when the HTTPS request has completed. @available(swift 1000.0) // Objective-C only API - @objc(callWithObject:completion:) open func call(_ data: Any? = nil, - completion: @escaping @MainActor (HTTPSCallableResult?, - Error?) - -> Void) { - sendableCallable.call(SendableWrapper(value: data as Any), completion: completion) + @objc(callWithObject:completion:) public func call(_ data: Any? = nil, + completion: @escaping @MainActor (HTTPSCallableResult?, + Error?) + -> Void) { + call(SendableWrapper(value: data as Any), completion: completion) } /// Executes this Callable HTTPS trigger asynchronously. @@ -102,11 +113,19 @@ 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. - @nonobjc open func call(_ data: sending Any? = nil, - completion: @escaping @MainActor (HTTPSCallableResult?, - Error?) - -> Void) { - sendableCallable.call(data, completion: completion) + @nonobjc public func call(_ data: sending Any? = nil, + completion: @escaping @MainActor (HTTPSCallableResult?, + Error?) + -> Void) { + let data = (data as? SendableWrapper)?.value ?? data + Task { + do { + let result = try await call(data) + await completion(result, nil) + } catch { + await completion(nil, error) + } + } } /// Executes this Callable HTTPS trigger asynchronously. This API should only be used from @@ -142,73 +161,13 @@ open class HTTPSCallable: NSObject, @unchecked Sendable { /// - 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) + public 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 { - sendableCallable.stream(data) - } -} - -private extension HTTPSCallable { - 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 = FIRAllocatedUnfairLock(initialState: 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) { - let data = (data as? SendableWrapper)?.value ?? data - Task { - do { - let result = try await call(data) - await completion(result, nil) - } catch { - await completion(nil, error) - } - } - } - - 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(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) - } + functions.stream(at: url, data: data, options: options, timeout: timeoutInterval) } } diff --git a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift index 371180ddb3e..9f7cc8a8eef 100644 --- a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift +++ b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift @@ -15,7 +15,7 @@ import Foundation /// Configuration options for a ``HTTPSCallable`` instance. -@objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject, @unchecked Sendable { +@objc(FIRHTTPSCallableOptions) public final class HTTPSCallableOptions: NSObject, Sendable { /// Whether or not to protect the callable function with a limited-use App Check token. @objc public let requireLimitedUseAppCheckTokens: Bool diff --git a/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift b/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift index c01521fb2e5..8b342f11785 100644 --- a/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift +++ b/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift @@ -54,14 +54,6 @@ class MockFunctions: Functions, @unchecked Sendable { } } -public class HTTPSCallableResultFake: HTTPSCallableResult { - let fakeData: String - init(data: String) { - fakeData = data - super.init(data: data) - } -} - @available(iOS 13.0, macOS 10.15, macCatalyst 13.0, tvOS 13.0, watchOS 6.0, *) class HTTPSCallableTests: XCTestCase { func testCallWithoutParametersSuccess() { @@ -73,7 +65,7 @@ class HTTPSCallableTests: XCTestCase { let functions = MockFunctions { httpsFunctionWasCalledExpectation.fulfill() - return HTTPSCallableResultFake(data: expectedResult) + return HTTPSCallableResult(data: expectedResult) } let dummyFunction = functions.httpsCallable("dummyFunction") @@ -115,7 +107,7 @@ class HTTPSCallableTests: XCTestCase { let expectedResult = "mockResult w/ parameters: \(inputParameter)" let functions = MockFunctions { httpsFunctionWasCalledExpectation.fulfill() - return HTTPSCallableResultFake(data: expectedResult) + return HTTPSCallableResult(data: expectedResult) } functions.verifyParameters = { url, data, timeout in XCTAssertEqual(