Skip to content

Commit f49b37f

Browse files
authored
[Swift 6] Address low-hanging Swift concurrency errors in Functions (#14597)
1 parent f504fae commit f49b37f

File tree

4 files changed

+115
-54
lines changed

4 files changed

+115
-54
lines changed

FirebaseFunctions/Sources/Functions.swift

Lines changed: 109 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,36 @@ import Foundation
2727
// Avoids exposing internal FirebaseCore APIs to Swift users.
2828
@_implementationOnly import FirebaseCoreExtension
2929

30+
final class AtomicBox<T> {
31+
private var _value: T
32+
private let lock = NSLock()
33+
34+
public init(_ value: T) {
35+
_value = value
36+
}
37+
38+
public func value() -> T {
39+
lock.withLock {
40+
_value
41+
}
42+
}
43+
44+
@discardableResult
45+
public func withLock(_ mutatingBody: (_ value: inout T) -> Void) -> T {
46+
lock.withLock {
47+
mutatingBody(&_value)
48+
return _value
49+
}
50+
}
51+
52+
@discardableResult
53+
public func withLock<R>(_ mutatingBody: (_ value: inout T) throws -> R) rethrows -> R {
54+
try lock.withLock {
55+
try mutatingBody(&_value)
56+
}
57+
}
58+
}
59+
3060
/// File specific constants.
3161
private enum Constants {
3262
static let appCheckTokenHeader = "X-Firebase-AppCheck"
@@ -53,10 +83,12 @@ enum FunctionsConstants {
5383

5484
/// A map of active instances, grouped by app. Keys are FirebaseApp names and values are arrays
5585
/// containing all instances of Functions associated with the given app.
56-
private static var instances: [String: [Functions]] = [:]
57-
58-
/// Lock to manage access to the instances array to avoid race conditions.
59-
private static var instancesLock: os_unfair_lock = .init()
86+
#if compiler(>=6.0)
87+
private nonisolated(unsafe) static var instances: AtomicBox<[String: [Functions]]> =
88+
AtomicBox([:])
89+
#else
90+
private static var instances: AtomicBox<[String: [Functions]]> = AtomicBox([:])
91+
#endif
6092

6193
/// The custom domain to use for all functions references (optional).
6294
let customDomain: String?
@@ -304,30 +336,28 @@ enum FunctionsConstants {
304336
guard let app else {
305337
fatalError("`FirebaseApp.configure()` needs to be called before using Functions.")
306338
}
307-
os_unfair_lock_lock(&instancesLock)
308-
309-
// Unlock before the function returns.
310-
defer { os_unfair_lock_unlock(&instancesLock) }
311-
312-
if let associatedInstances = instances[app.name] {
313-
for instance in associatedInstances {
314-
// Domains may be nil, so handle with care.
315-
var equalDomains = false
316-
if let instanceCustomDomain = instance.customDomain {
317-
equalDomains = instanceCustomDomain == customDomain
318-
} else {
319-
equalDomains = customDomain == nil
320-
}
321-
// Check if it's a match.
322-
if instance.region == region, equalDomains {
323-
return instance
339+
340+
return instances.withLock { instances in
341+
if let associatedInstances = instances[app.name] {
342+
for instance in associatedInstances {
343+
// Domains may be nil, so handle with care.
344+
var equalDomains = false
345+
if let instanceCustomDomain = instance.customDomain {
346+
equalDomains = instanceCustomDomain == customDomain
347+
} else {
348+
equalDomains = customDomain == nil
349+
}
350+
// Check if it's a match.
351+
if instance.region == region, equalDomains {
352+
return instance
353+
}
324354
}
325355
}
356+
let newInstance = Functions(app: app, region: region, customDomain: customDomain)
357+
let existingInstances = instances[app.name, default: []]
358+
instances[app.name] = existingInstances + [newInstance]
359+
return newInstance
326360
}
327-
let newInstance = Functions(app: app, region: region, customDomain: customDomain)
328-
let existingInstances = instances[app.name, default: []]
329-
instances[app.name] = existingInstances + [newInstance]
330-
return newInstance
331361
}
332362

333363
@objc init(projectID: String,
@@ -576,34 +606,65 @@ enum FunctionsConstants {
576606
}
577607
}
578608

579-
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
580-
private func callableStreamResult(fromResponseData data: Data,
581-
endpointURL url: URL) throws -> JSONStreamResponse {
582-
let data = try processedData(fromResponseData: data, endpointURL: url)
609+
#if compiler(>=6.0)
610+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
611+
private func callableStreamResult(fromResponseData data: Data,
612+
endpointURL url: URL) throws -> sending JSONStreamResponse {
613+
let data = try processedData(fromResponseData: data, endpointURL: url)
614+
615+
let responseJSONObject: Any
616+
do {
617+
responseJSONObject = try JSONSerialization.jsonObject(with: data)
618+
} catch {
619+
throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
620+
}
583621

584-
let responseJSONObject: Any
585-
do {
586-
responseJSONObject = try JSONSerialization.jsonObject(with: data)
587-
} catch {
588-
throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
589-
}
622+
guard let responseJSON = responseJSONObject as? [String: Any] else {
623+
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
624+
throw FunctionsError(.dataLoss, userInfo: userInfo)
625+
}
590626

591-
guard let responseJSON = responseJSONObject as? [String: Any] else {
592-
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
593-
throw FunctionsError(.dataLoss, userInfo: userInfo)
627+
if let _ = responseJSON["result"] {
628+
return .result(responseJSON)
629+
} else if let _ = responseJSON["message"] {
630+
return .message(responseJSON)
631+
} else {
632+
throw FunctionsError(
633+
.dataLoss,
634+
userInfo: [NSLocalizedDescriptionKey: "Response is missing result or message field."]
635+
)
636+
}
594637
}
638+
#else
639+
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
640+
private func callableStreamResult(fromResponseData data: Data,
641+
endpointURL url: URL) throws -> JSONStreamResponse {
642+
let data = try processedData(fromResponseData: data, endpointURL: url)
643+
644+
let responseJSONObject: Any
645+
do {
646+
responseJSONObject = try JSONSerialization.jsonObject(with: data)
647+
} catch {
648+
throw FunctionsError(.dataLoss, userInfo: [NSUnderlyingErrorKey: error])
649+
}
595650

596-
if let _ = responseJSON["result"] {
597-
return .result(responseJSON)
598-
} else if let _ = responseJSON["message"] {
599-
return .message(responseJSON)
600-
} else {
601-
throw FunctionsError(
602-
.dataLoss,
603-
userInfo: [NSLocalizedDescriptionKey: "Response is missing result or message field."]
604-
)
651+
guard let responseJSON = responseJSONObject as? [String: Any] else {
652+
let userInfo = [NSLocalizedDescriptionKey: "Response was not a dictionary."]
653+
throw FunctionsError(.dataLoss, userInfo: userInfo)
654+
}
655+
656+
if let _ = responseJSON["result"] {
657+
return .result(responseJSON)
658+
} else if let _ = responseJSON["message"] {
659+
return .message(responseJSON)
660+
} else {
661+
throw FunctionsError(
662+
.dataLoss,
663+
userInfo: [NSLocalizedDescriptionKey: "Response is missing result or message field."]
664+
)
665+
}
605666
}
606-
}
667+
#endif // compiler(>=6.0)
607668

608669
private func jsonData(jsonText: String) throws -> Data {
609670
guard let data = jsonText.data(using: .utf8) else {

FirebaseFunctions/Sources/FunctionsError.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public let FunctionsErrorDetailsKey: String = "details"
2525
* canonical error codes for Google APIs, as documented here:
2626
* https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto#L26
2727
*/
28-
@objc(FIRFunctionsErrorCode) public enum FunctionsErrorCode: Int {
28+
@objc(FIRFunctionsErrorCode) public enum FunctionsErrorCode: Int, Sendable {
2929
/** The operation completed successfully. */
3030
case OK = 0
3131

FirebaseFunctions/Sources/HTTPSCallableOptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import Foundation
1616

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

FirebaseFunctions/Sources/Internal/FunctionsContext.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import FirebaseAppCheckInterop
16-
import FirebaseAuthInterop
17-
import FirebaseMessagingInterop
15+
@preconcurrency import FirebaseAppCheckInterop
16+
@preconcurrency import FirebaseAuthInterop
17+
@preconcurrency import FirebaseMessagingInterop
1818
import Foundation
1919

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

28-
struct FunctionsContextProvider {
28+
struct FunctionsContextProvider: Sendable {
2929
private let auth: AuthInterop?
3030
private let messaging: MessagingInterop?
3131
private let appCheck: AppCheckInterop?

0 commit comments

Comments
 (0)