diff --git a/Sources/BaseOperationRef.swift b/Sources/BaseOperationRef.swift index 3d73f0a..5b8a502 100644 --- a/Sources/BaseOperationRef.swift +++ b/Sources/BaseOperationRef.swift @@ -15,16 +15,16 @@ import Foundation @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct OperationResult { +public struct OperationResult: Sendable { public var data: ResultData } // notional protocol that denotes a variable. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public protocol OperationVariable: Encodable, Hashable, Equatable {} +public protocol OperationVariable: Encodable, Hashable, Equatable, Sendable {} @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -protocol OperationRequest: Hashable, Equatable { +protocol OperationRequest: Hashable, Equatable, Sendable { associatedtype Variable: OperationVariable var operationName: String { get } // Name within Connector definition var variables: Variable? { get } @@ -32,7 +32,7 @@ protocol OperationRequest: Hashable, Equatable { @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public protocol OperationRef { - associatedtype ResultData: Decodable + associatedtype ResultData: Decodable & Sendable func execute() async throws -> OperationResult } diff --git a/Sources/ConnectorConfig.swift b/Sources/ConnectorConfig.swift index 983cd0f..29c98fd 100644 --- a/Sources/ConnectorConfig.swift +++ b/Sources/ConnectorConfig.swift @@ -15,7 +15,7 @@ import Foundation @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct ConnectorConfig: Hashable, Equatable { +public struct ConnectorConfig: Hashable, Equatable, Sendable { public private(set) var serviceId: String public private(set) var location: String public private(set) var connector: String diff --git a/Sources/DataConnect.swift b/Sources/DataConnect.swift index 75f3113..7e7748f 100644 --- a/Sources/DataConnect.swift +++ b/Sources/DataConnect.swift @@ -14,9 +14,9 @@ import Foundation -import FirebaseAppCheck -import FirebaseAuth -import FirebaseCore +@preconcurrency import FirebaseAppCheck +@preconcurrency import FirebaseAuth +@preconcurrency import FirebaseCore @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public class DataConnect { @@ -29,7 +29,17 @@ public class DataConnect { private var callerSDKType: CallerSDKType = .base - private static var instanceStore = InstanceStore() + private let accessQueue = DispatchQueue( + label: "com.google.firebase.dataConnect.instanceAccessQueue", + autoreleaseFrequency: .workItem + ) + + // Instance store uses an internal queue to protect mutable state. + #if compiler(>=6) + private nonisolated(unsafe) static let instanceStore = InstanceStore() + #else + private static let instanceStore = InstanceStore() + #endif public enum EmulatorDefaults { public static let host = "127.0.0.1" @@ -62,26 +72,26 @@ public class DataConnect { public func useEmulator(host: String = EmulatorDefaults.host, port: Int = EmulatorDefaults.port) { - settings = DataConnectSettings(host: host, port: port, sslEnabled: false) + accessQueue.sync { + settings = DataConnectSettings(host: host, port: port, sslEnabled: false) - // TODO: - shutdown grpc client - // self.grpcClient.close - // self.operations.close + // TODO: - shutdown grpc client + // self.grpcClient.close + // self.operations.close - guard app.options.projectID != nil else { - fatalError("Firebase DataConnect requires the projectID to be set in the app options") - } + guard app.options.projectID != nil else { + fatalError("Firebase DataConnect requires the projectID to be set in the app options") + } - grpcClient = GrpcClient( - app: app, - settings: settings, - connectorConfig: connectorConfig, - auth: Auth.auth(app: app), - appCheck: AppCheck.appCheck(app: app), - callerSDKType: callerSDKType - ) + grpcClient = GrpcClient( + app: app, + settings: settings, + connectorConfig: connectorConfig, + callerSDKType: callerSDKType + ) - operationsManager = OperationsManager(grpcClient: grpcClient) + operationsManager = OperationsManager(grpcClient: grpcClient) + } } // MARK: Init @@ -101,8 +111,6 @@ public class DataConnect { app: self.app, settings: settings, connectorConfig: connectorConfig, - auth: Auth.auth(app: app), - appCheck: AppCheck.appCheck(app: app), callerSDKType: self.callerSDKType ) operationsManager = OperationsManager(grpcClient: grpcClient) @@ -111,33 +119,37 @@ public class DataConnect { // MARK: Operations /// Returns a query ref matching the name and variables. - public func query(name: String, variables: Variable, resultsDataType: ResultData .Type, publisher: ResultsPublisherType = .observableObject) -> any ObservableQueryRef { - let request = QueryRequest(operationName: name, variables: variables) - return operationsManager.queryRef(for: request, with: resultsDataType, publisher: publisher) + accessQueue.sync { + let request = QueryRequest(operationName: name, variables: variables) + return operationsManager.queryRef(for: request, with: resultsDataType, publisher: publisher) + } } /// Returns a Mutation Ref matching the name and specified variables. - public func mutation(name: String, variables: Variable, resultsDataType: ResultData .Type) -> MutationRef { - let request = MutationRequest(operationName: name, variables: variables) - return operationsManager.mutationRef(for: request, with: resultsDataType) + accessQueue.sync { + let request = MutationRequest(operationName: name, variables: variables) + return operationsManager.mutationRef(for: request, with: resultsDataType) + } } } // This enum is public so the gen sdk can access it @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public enum CallerSDKType { +public enum CallerSDKType: Sendable { case base // base sdk is directly used case generated // generated sdk is calling the base } @@ -170,7 +182,7 @@ private class InstanceStore { autoreleaseFrequency: .workItem ) - var instances = [InstanceKey: DataConnect]() + private var instances = [InstanceKey: DataConnect]() func instance(for app: FirebaseApp, config: ConnectorConfig, settings: DataConnectSettings, callerSDKType: CallerSDKType) -> DataConnect { diff --git a/Sources/DataConnectSettings.swift b/Sources/DataConnectSettings.swift index dd996c1..dda66f7 100644 --- a/Sources/DataConnectSettings.swift +++ b/Sources/DataConnectSettings.swift @@ -15,7 +15,7 @@ import Foundation @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct DataConnectSettings: Hashable, Equatable { +public struct DataConnectSettings: Hashable, Equatable, Sendable { public private(set) var host: String public private(set) var port: Int public private(set) var sslEnabled: Bool diff --git a/Sources/Internal/GrpcClient.swift b/Sources/Internal/GrpcClient.swift index b39f015..29071fc 100644 --- a/Sources/Internal/GrpcClient.swift +++ b/Sources/Internal/GrpcClient.swift @@ -14,8 +14,8 @@ import Foundation -import FirebaseAppCheck -import FirebaseAuth +@preconcurrency import FirebaseAppCheck +@preconcurrency import FirebaseAuth import FirebaseCore import GRPC import NIOCore @@ -108,8 +108,6 @@ actor GrpcClient: CustomStringConvertible { }() init(app: FirebaseApp, settings: DataConnectSettings, connectorConfig: ConnectorConfig, - auth: Auth, - appCheck: AppCheck?, callerSDKType: CallerSDKType) { self.app = app @@ -120,8 +118,8 @@ actor GrpcClient: CustomStringConvertible { serverSettings = settings self.connectorConfig = connectorConfig - self.auth = auth - self.appCheck = appCheck + auth = Auth.auth(app: app) + appCheck = AppCheck.appCheck(app: app) self.callerSDKType = callerSDKType connectorName = diff --git a/Sources/Internal/OperationsManager.swift b/Sources/Internal/OperationsManager.swift index 93b332e..b775857 100644 --- a/Sources/Internal/OperationsManager.swift +++ b/Sources/Internal/OperationsManager.swift @@ -34,7 +34,7 @@ class OperationsManager { self.grpcClient = grpcClient } - func queryRef(for request: QueryRequest, with resultType: ResultDataType .Type, diff --git a/Sources/MutationRef.swift b/Sources/MutationRef.swift index 7ea0cc9..af4fa05 100644 --- a/Sources/MutationRef.swift +++ b/Sources/MutationRef.swift @@ -27,7 +27,10 @@ struct MutationRequest: OperationRequest { /// Represents a predefined graphql mutation identified by name and variables. @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public class MutationRef: OperationRef { +public class MutationRef< + ResultData: Decodable & Sendable, + Variable: OperationVariable +>: OperationRef { private var request: any OperationRequest private var grpcClient: GrpcClient diff --git a/Sources/QueryRef.swift b/Sources/QueryRef.swift index de13fcc..afd38a4 100644 --- a/Sources/QueryRef.swift +++ b/Sources/QueryRef.swift @@ -14,7 +14,7 @@ import Foundation -import Combine +@preconcurrency import Combine import Observation /// The type of publisher to use for the Query Ref @@ -75,13 +75,13 @@ public protocol QueryRef: OperationRef { } @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -actor GenericQueryRef: QueryRef { - private var resultsPublisher = PassthroughSubject, +actor GenericQueryRef: QueryRef { + private let resultsPublisher = PassthroughSubject, Never>() - private var request: QueryRequest + private let request: QueryRequest - private var grpcClient: GrpcClient + private let grpcClient: GrpcClient init(request: QueryRequest, grpcClient: GrpcClient) { self.request = request @@ -143,7 +143,7 @@ public protocol ObservableQueryRef: QueryRef { /// If last fetch was successful, this variable is cleared @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) public class QueryRefObservableObject< - ResultData: Decodable, + ResultData: Decodable & Sendable, Variable: OperationVariable >: ObservableObject, ObservableQueryRef { private var request: QueryRequest @@ -216,7 +216,7 @@ public class QueryRefObservableObject< @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) @Observable public class QueryRefObservation< - ResultData: Decodable, + ResultData: Decodable & Sendable, Variable: OperationVariable >: ObservableQueryRef { @ObservationIgnored diff --git a/Sources/Scalars/AnyValue.swift b/Sources/Scalars/AnyValue.swift index b0efc29..6425f18 100644 --- a/Sources/Scalars/AnyValue.swift +++ b/Sources/Scalars/AnyValue.swift @@ -62,3 +62,6 @@ extension AnyValue: Hashable { hasher.combine(value) } } + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension AnyValue: Sendable {} diff --git a/Sources/Scalars/LocalDate.swift b/Sources/Scalars/LocalDate.swift index 59caab7..4356319 100644 --- a/Sources/Scalars/LocalDate.swift +++ b/Sources/Scalars/LocalDate.swift @@ -20,7 +20,7 @@ import Foundation Essentially represents: https://the-guild.dev/graphql/scalars/docs/scalars/local-date */ @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) -public struct LocalDate: Codable, Equatable, Comparable, CustomStringConvertible { +public struct LocalDate: Codable, Comparable, CustomStringConvertible, Equatable, Sendable { private var calendar = Calendar(identifier: .gregorian) private var dateFormatter = DateFormatter() private var date = Date()