Skip to content

[Swift 6] Address low-hanging Swift concurrency errors in Functions #14597

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

Merged
merged 5 commits into from
Mar 19, 2025
Merged
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
157 changes: 109 additions & 48 deletions FirebaseFunctions/Sources/Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,36 @@ import Foundation
// Avoids exposing internal FirebaseCore APIs to Swift users.
@_implementationOnly import FirebaseCoreExtension

final class AtomicBox<T> {
private var _value: T
private let lock = NSLock()

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

public func value() -> T {
lock.withLock {
_value
}
}

@discardableResult
public func withLock(_ mutatingBody: (_ value: inout T) -> Void) -> T {
lock.withLock {
mutatingBody(&_value)
return _value
}
}

@discardableResult
public func withLock<R>(_ mutatingBody: (_ value: inout T) throws -> R) rethrows -> R {
try lock.withLock {
try mutatingBody(&_value)
}
}
}

/// File specific constants.
private enum Constants {
static let appCheckTokenHeader = "X-Firebase-AppCheck"
Expand All @@ -53,10 +83,12 @@ 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 static var instances: [String: [Functions]] = [:]

/// Lock to manage access to the instances array to avoid race conditions.
private static var instancesLock: os_unfair_lock = .init()
#if compiler(>=6.0)
private nonisolated(unsafe) static var instances: AtomicBox<[String: [Functions]]> =
AtomicBox([:])
#else
private static var instances: AtomicBox<[String: [Functions]]> = AtomicBox([:])
#endif

/// The custom domain to use for all functions references (optional).
let customDomain: String?
Expand Down Expand Up @@ -304,30 +336,28 @@ enum FunctionsConstants {
guard let app else {
fatalError("`FirebaseApp.configure()` needs to be called before using Functions.")
}
os_unfair_lock_lock(&instancesLock)

// Unlock before the function returns.
defer { os_unfair_lock_unlock(&instancesLock) }

if let associatedInstances = instances[app.name] {
for instance in associatedInstances {
// Domains may be nil, so handle with care.
var equalDomains = false
if let instanceCustomDomain = instance.customDomain {
equalDomains = instanceCustomDomain == customDomain
} else {
equalDomains = customDomain == nil
}
// Check if it's a match.
if instance.region == region, equalDomains {
return instance

return instances.withLock { instances in
if let associatedInstances = instances[app.name] {
for instance in associatedInstances {
// Domains may be nil, so handle with care.
var equalDomains = false
if let instanceCustomDomain = instance.customDomain {
equalDomains = instanceCustomDomain == customDomain
} else {
equalDomains = customDomain == nil
}
// Check if it's a match.
if instance.region == region, equalDomains {
return instance
}
}
}
let newInstance = Functions(app: app, region: region, customDomain: customDomain)
let existingInstances = instances[app.name, default: []]
instances[app.name] = existingInstances + [newInstance]
return newInstance
}
let newInstance = Functions(app: app, region: region, customDomain: customDomain)
let existingInstances = instances[app.name, default: []]
instances[app.name] = existingInstances + [newInstance]
return newInstance
}

@objc init(projectID: String,
Expand Down Expand Up @@ -576,34 +606,65 @@ enum FunctionsConstants {
}
}

@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
private func callableStreamResult(fromResponseData data: Data,
endpointURL url: URL) throws -> JSONStreamResponse {
let data = try processedData(fromResponseData: data, endpointURL: url)
#if compiler(>=6.0)
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
private func callableStreamResult(fromResponseData data: Data,
endpointURL url: URL) throws -> sending JSONStreamResponse {
let data = try processedData(fromResponseData: data, endpointURL: url)

let responseJSONObject: Any
do {
responseJSONObject = try JSONSerialization.jsonObject(with: data)
} catch {
throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
}

let responseJSONObject: Any
do {
responseJSONObject = try JSONSerialization.jsonObject(with: data)
} catch {
throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
}
guard let responseJSON = responseJSONObject as? [String: Any] else {
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
throw FunctionsError(.dataLoss, userInfo: userInfo)
}

guard let responseJSON = responseJSONObject as? [String: Any] else {
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
throw FunctionsError(.dataLoss, userInfo: userInfo)
if let _ = responseJSON["result"] {
return .result(responseJSON)
} else if let _ = responseJSON["message"] {
return .message(responseJSON)
} else {
throw FunctionsError(
.dataLoss,
userInfo: [NSLocalizedDescriptionKey: "Response is missing result or message field."]
)
}
}
#else
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
private func callableStreamResult(fromResponseData data: Data,
endpointURL url: URL) throws -> JSONStreamResponse {
let data = try processedData(fromResponseData: data, endpointURL: url)

let responseJSONObject: Any
do {
responseJSONObject = try JSONSerialization.jsonObject(with: data)
} catch {
throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
}

if let _ = responseJSON["result"] {
return .result(responseJSON)
} else if let _ = responseJSON["message"] {
return .message(responseJSON)
} else {
throw FunctionsError(
.dataLoss,
userInfo: [NSLocalizedDescriptionKey: "Response is missing result or message field."]
)
guard let responseJSON = responseJSONObject as? [String: Any] else {
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
throw FunctionsError(.dataLoss, userInfo: userInfo)
}

if let _ = responseJSON["result"] {
return .result(responseJSON)
} else if let _ = responseJSON["message"] {
return .message(responseJSON)
} else {
throw FunctionsError(
.dataLoss,
userInfo: [NSLocalizedDescriptionKey: "Response is missing result or message field."]
)
}
}
}
#endif // compiler(>=6.0)

private func jsonData(jsonText: String) throws -> Data {
guard let data = jsonText.data(using: .utf8) else {
Expand Down
2 changes: 1 addition & 1 deletion FirebaseFunctions/Sources/FunctionsError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public let FunctionsErrorDetailsKey: String = "details"
* canonical error codes for Google APIs, as documented here:
* https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto#L26
*/
@objc(FIRFunctionsErrorCode) public enum FunctionsErrorCode: Int {
@objc(FIRFunctionsErrorCode) public enum FunctionsErrorCode: Int, Sendable {
/** The operation completed successfully. */
case OK = 0

Expand Down
2 changes: 1 addition & 1 deletion FirebaseFunctions/Sources/HTTPSCallableOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import Foundation

/// Configuration options for a ``HTTPSCallable`` instance.
@objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject {
@objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject, @unchecked Sendable {
/// Whether or not to protect the callable function with a limited-use App Check token.
@objc public let requireLimitedUseAppCheckTokens: Bool

Expand Down
8 changes: 4 additions & 4 deletions FirebaseFunctions/Sources/Internal/FunctionsContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import FirebaseAppCheckInterop
import FirebaseAuthInterop
import FirebaseMessagingInterop
@preconcurrency import FirebaseAppCheckInterop
@preconcurrency import FirebaseAuthInterop
@preconcurrency import FirebaseMessagingInterop
import Foundation

/// `FunctionsContext` is a helper object that holds metadata for a function call.
Expand All @@ -25,7 +25,7 @@ struct FunctionsContext {
let limitedUseAppCheckToken: String?
}

struct FunctionsContextProvider {
struct FunctionsContextProvider: Sendable {
private let auth: AuthInterop?
private let messaging: MessagingInterop?
private let appCheck: AppCheckInterop?
Expand Down
Loading