Skip to content

[Functions] More Swift 6 improvements #14788

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions FirebaseFunctions/Sources/Callable+Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
// 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.
///
/// - 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<Request: Encodable, Response: Decodable> {
public struct Callable<Request: Encodable, Response: Decodable>: Sendable {
/// The timeout to use when calling the function. Defaults to 70 seconds.
public var timeoutInterval: TimeInterval {
get {
Expand Down Expand Up @@ -61,11 +61,10 @@ public struct Callable<Request: Encodable, Response: Decodable> {
/// - 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<Response, Error>)
completion: @escaping @MainActor (Result<Response, Error>)
-> Void) {
do {
let encoded = try encoder.encode(data)

callable.call(encoded) { result, error in
do {
if let result {
Expand All @@ -81,7 +80,9 @@ public struct Callable<Request: Encodable, Response: Decodable> {
}
}
} catch {
completion(.failure(error))
DispatchQueue.main.async {
completion(.failure(error))
}
}
}

Expand All @@ -108,7 +109,7 @@ public struct Callable<Request: Encodable, Response: Decodable> {
/// - 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<Response, Error>)
completion: @escaping @MainActor (Result<Response, Error>)
-> Void) {
call(data, completion: completion)
}
Expand Down Expand Up @@ -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<Response, Error> {
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])
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
35 changes: 21 additions & 14 deletions FirebaseFunctions/Sources/Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ 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<T> {
private var _value: T
final class AtomicBox<T>: Sendable {
private nonisolated(unsafe) var _value: T
private let lock = NSLock()

public init(_ value: T) {
public init(_ value: T) where T: Sendable {
_value = value
}

Expand Down Expand Up @@ -68,7 +68,7 @@ enum FunctionsConstants {
}

/// `Functions` is the client for Cloud Functions for a Firebase project.
@objc(FIRFunctions) open class Functions: NSObject {
@objc(FIRFunctions) open class Functions: NSObject, @unchecked Sendable {
// MARK: - Private Variables

/// The network client to use for http requests.
Expand All @@ -82,7 +82,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).
Expand All @@ -91,10 +91,14 @@ enum FunctionsConstants {
/// The region to use for all function references.
let region: String

private let _emulatorOrigin: AtomicBox<String?>

// MARK: - Public APIs

/// The current emulator origin, or `nil` if it is not set.
open private(set) 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.
Expand Down Expand Up @@ -318,7 +322,9 @@ enum FunctionsConstants {
@objc open 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)
Expand Down Expand Up @@ -365,7 +371,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)
Expand Down Expand Up @@ -414,7 +420,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,
Expand Down Expand Up @@ -461,7 +467,8 @@ enum FunctionsConstants {
options: HTTPSCallableOptions?,
timeout: TimeInterval,
context: FunctionsContext,
completion: @escaping @MainActor (Result<HTTPSCallableResult, Error>) -> Void) {
completion: @escaping @MainActor (Result<HTTPSCallableResult, Error>)
-> Void) {
let fetcher: GTMSessionFetcher
do {
fetcher = try makeFetcher(
Expand Down Expand Up @@ -500,7 +507,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<JSONStreamResponse, Error> {
Expand All @@ -511,7 +518,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
Expand Down
6 changes: 3 additions & 3 deletions FirebaseFunctions/Sources/FunctionsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
Loading
Loading