From 6a17912ee065781f04a48ecece1d52d338ee31df Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Thu, 15 May 2025 17:21:30 -0700 Subject: [PATCH] WIP: Network fetch returns data as chunks --- apollo-ios/Sources/Apollo/ApolloClient.swift | 13 +- .../Apollo/ClientAwarenessMetadata.swift | 143 ++++++++++++++++++ .../Sources/Apollo/GraphQLRequest.swift | 84 +++------- apollo-ios/Sources/Apollo/JSONRequest.swift | 30 ++-- .../Sources/Apollo/RequestBodyCreator.swift | 13 +- apollo-ios/Sources/Apollo/RequestChain.swift | 75 ++++----- .../Apollo/RequestChainNetworkTransport.swift | 22 ++- .../Apollo/RequestClientMetadata.swift | 34 ----- apollo-ios/Sources/Apollo/UploadRequest.swift | 19 +-- .../OperationMessageIdCreator.swift | 2 +- .../SplitNetworkTransport.swift | 20 --- .../ApolloWebSocket/WebSocketTransport.swift | 92 +++++++---- 12 files changed, 291 insertions(+), 256 deletions(-) create mode 100644 apollo-ios/Sources/Apollo/ClientAwarenessMetadata.swift delete mode 100644 apollo-ios/Sources/Apollo/RequestClientMetadata.swift diff --git a/apollo-ios/Sources/Apollo/ApolloClient.swift b/apollo-ios/Sources/Apollo/ApolloClient.swift index ee47ffca5..25719b3e3 100644 --- a/apollo-ios/Sources/Apollo/ApolloClient.swift +++ b/apollo-ios/Sources/Apollo/ApolloClient.swift @@ -36,8 +36,6 @@ public class ApolloClient: ApolloClientProtocol, @unchecked Sendable { public let store: ApolloStore - private let sendEnhancedClientAwareness: Bool - public enum ApolloClientError: Error, LocalizedError, Hashable { case noUploadTransport case noSubscriptionTransport @@ -62,12 +60,10 @@ public class ApolloClient: ApolloClientProtocol, @unchecked Sendable { /// key. Client library metadata is the Apollo iOS library name and version. Defaults to `true`. public init( networkTransport: any NetworkTransport, - store: ApolloStore, - sendEnhancedClientAwareness: Bool = true + store: ApolloStore ) { self.networkTransport = networkTransport self.store = store - self.sendEnhancedClientAwareness = sendEnhancedClientAwareness } /// Creates a client with a `RequestChainNetworkTransport` connecting to the specified URL. @@ -75,20 +71,19 @@ public class ApolloClient: ApolloClientProtocol, @unchecked Sendable { /// - Parameter url: The URL of a GraphQL server to connect to. public convenience init( url: URL, - sendEnhancedClientAwareness: Bool = true + clientAwarenessMetadata: ClientAwarenessMetadata = ClientAwarenessMetadata() ) { let store = ApolloStore(cache: InMemoryNormalizedCache()) let provider = DefaultInterceptorProvider(store: store) let transport = RequestChainNetworkTransport( interceptorProvider: provider, endpointURL: url, - sendEnhancedClientAwareness: sendEnhancedClientAwareness + clientAwarenessMetadata: clientAwarenessMetadata ) self.init( networkTransport: transport, - store: store, - sendEnhancedClientAwareness: sendEnhancedClientAwareness + store: store ) } diff --git a/apollo-ios/Sources/Apollo/ClientAwarenessMetadata.swift b/apollo-ios/Sources/Apollo/ClientAwarenessMetadata.swift new file mode 100644 index 000000000..d2733988a --- /dev/null +++ b/apollo-ios/Sources/Apollo/ClientAwarenessMetadata.swift @@ -0,0 +1,143 @@ +import Foundation +#if !COCOAPODS +import ApolloAPI +#endif + +/// A data structure containing telemetry metadata about the client. This is used by GraphOS Studio's +/// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation) +/// feature. +public struct ClientAwarenessMetadata: Sendable { + + /// The name of the application. This value is sent for the header "apollographql-client-name". + /// + /// Defaults to `nil`. + public var clientApplicationName: String? + + /// The version of the application. This value is sent for the header "apollographql-client-version". + /// + /// Defaults to `nil`. + public var clientApplicationVersion: String? + + /// Determines if the Apollo iOS library name and version should be sent with the telemetry data. + /// + /// If `true`, the JSON body of the request will include a "clientLibrary" extension containing + /// the name of the Apollo iOS library and the version of Apollo iOS being used by the client + /// application. + /// + /// Defaults to `true`. + public var includeApolloLibraryAwareness: Bool = true + + public init( + clientApplicationName: String? = nil, + clientApplicationVersion: String? = nil, + includeApolloLibraryAwareness: Bool = true + ) { + self.clientApplicationName = clientApplicationName + self.clientApplicationVersion = clientApplicationVersion + self.includeApolloLibraryAwareness = includeApolloLibraryAwareness + } + + /// Disables all client awareness metadata. + public static var none: ClientAwarenessMetadata { + .init( + clientApplicationName: nil, + clientApplicationVersion: nil, + includeApolloLibraryAwareness: false + ) + } + + /// Enables all client awareness metadata with the following default values: + /// + /// - `clientApplicationName`: The application's bundle identifier + "-apollo-ios". + /// - `clientApplicationVersion`: The bundle's short version string if available, + /// otherwise the build number. + /// - `includeApolloiOSLibraryVersion`: `true` + public static var enabledWithDefaults: ClientAwarenessMetadata { + .init( + clientApplicationName: defaultClientName, + clientApplicationVersion: defaultClientVersion, + includeApolloLibraryAwareness: true + ) + } + + /// The default client name to use when setting up the `clientName` property + public static var defaultClientName: String { + guard let identifier = Bundle.main.bundleIdentifier else { + return "apollo-ios-client" + } + + return "\(identifier)-apollo-ios" + } + + /// The default client version to use when setting up the `clientVersion` property. + public static var defaultClientVersion: String { + var version = String() + if let shortVersion = Bundle.main.shortVersion { + version.append(shortVersion) + } + + if let buildNumber = Bundle.main.buildNumber { + if version.isEmpty { + version.append(buildNumber) + } else { + version.append("-\(buildNumber)") + } + } + + if version.isEmpty { + version = "(unknown)" + } + + return version + } + + struct Constants { + /// The field name for the Apollo Client Name header + static let clientApplicationNameKey: StaticString = "apollographql-client-name" + + /// The field name for the Apollo Client Version header + static let clientApplicationVersionKey: StaticString = "apollographql-client-version" + } + + /// A helper method that adds the client awareness headers to the given request + /// This is used by GraphOS Studio's + /// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation) + /// feature. + /// + /// - Parameters: + /// - clientAwarenessMetadata: The client name. The telemetry metadata about the client. + public func applyHeaders( + to request: inout URLRequest + ) { + if let clientApplicationName = self.clientApplicationName { + request.addValue( + clientApplicationName, + forHTTPHeaderField: ClientAwarenessMetadata.Constants.clientApplicationNameKey.description + ) + } + + if let clientApplicationVersion = self.clientApplicationVersion { + request.addValue( + clientApplicationVersion, + forHTTPHeaderField: ClientAwarenessMetadata.Constants.clientApplicationVersionKey.description + ) + } + } + + /// Adds client metadata to the request body in the `extensions` key. + /// + /// - Parameter body: The previously generated JSON body. + func applyExtension(to body: inout JSONEncodableDictionary) { + if self.includeApolloLibraryAwareness { + let clientLibraryMetadata: JSONEncodableDictionary = [ + "name": Apollo.Constants.ApolloClientName, + "version": Apollo.Constants.ApolloClientVersion + ] + + var extensions = body["extensions"] as? JSONEncodableDictionary ?? JSONEncodableDictionary() + extensions["clientLibrary"] = clientLibraryMetadata + + body["extensions"] = extensions + } + } +} diff --git a/apollo-ios/Sources/Apollo/GraphQLRequest.swift b/apollo-ios/Sources/Apollo/GraphQLRequest.swift index 9aac09868..3ed3524ce 100644 --- a/apollo-ios/Sources/Apollo/GraphQLRequest.swift +++ b/apollo-ios/Sources/Apollo/GraphQLRequest.swift @@ -21,36 +21,52 @@ public protocol GraphQLRequest: Sendable { /// [optional] A context that is being passed through the request chain. var context: (any RequestContext)? { get set } + /// The telemetry metadata about the client. This is used by GraphOS Studio's + /// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation) + /// feature. + var clientAwarenessMetadata: ClientAwarenessMetadata { get } + + /// Converts the receiver into a `URLRequest` to be used for networking operations. + /// + /// - Note: This function should call `createDefaultRequest()` to obtain a request with the + /// default configuration. The implementation may then modify that request. See the documentation + /// for ``GraphQLRequest/createDefaultRequest()`` for more information. func toURLRequest() throws -> URLRequest } +public extension GraphQLRequest { + var clientAwarenessMetadata: ClientAwarenessMetadata { .init() } +} + // MARK: - Helper Functions extension GraphQLRequest { /// Creates a default `URLRequest` for the receiver. /// - /// This can be called within the implementation of `toURLRequest()` and the returned request - /// can then be modified as necessary before being returned. - /// /// This function creates a `URLRequest` with the following behaviors: /// - `url` set to the receiver's `graphQLEndpoint` /// - `httpMethod` set to POST + /// - Client awareness headers from `clientAwarenessMetadata` added to `allHTTPHeaderFields` /// - All header's from `additionalHeaders` added to `allHTTPHeaderFields` /// - If the `context` conforms to `RequestContextTimeoutConfigurable`, the `timeoutInterval` is /// set to the context's `requestTimeout`. /// + /// - Note: This should be called within the implementation of `toURLRequest()` and the returned request + /// can then be modified as necessary before being returned. + /// /// - Returns: A `URLRequest` configured as described above. public func createDefaultRequest() -> URLRequest { var request = URLRequest(url: self.graphQLEndpoint) request.httpMethod = GraphQLHTTPMethod.POST.rawValue - for (fieldName, value) in additionalHeaders { + clientAwarenessMetadata.applyHeaders(to: &request) + for (fieldName, value) in self.additionalHeaders { request.addValue(value, forHTTPHeaderField: fieldName) } - if let configContext = context as? any RequestContextTimeoutConfigurable { + if let configContext = self.context as? any RequestContextTimeoutConfigurable { request.timeoutInterval = configContext.requestTimeout } @@ -65,62 +81,4 @@ extension GraphQLRequest { self.additionalHeaders.merge(headers) { (_, new) in new } } - /// A helper method that dds the Apollo client headers to the given request - /// These header values are used for telemetry to track the source of client requests. - /// - /// This should be called during setup of any implementation of `GraphQLRequest` to provide these - /// header values. - /// - /// - Parameters: - /// - clientName: The client name. Defaults to the application's bundle identifier + "-apollo-ios". - /// - clientVersion: The client version. Defaults to the bundle's short version or build number. - public mutating func addApolloClientHeaders( - clientName: String? = Self.defaultClientName, - clientVersion: String? = Self.defaultClientVersion - ) { - additionalHeaders[Self.headerFieldNameApolloClientName] = clientName - additionalHeaders[Self.headerFieldNameApolloClientVersion] = clientVersion - } - - /// The field name for the Apollo Client Name header - static var headerFieldNameApolloClientName: String { - return "apollographql-client-name" - } - - /// The field name for the Apollo Client Version header - static var headerFieldNameApolloClientVersion: String { - return "apollographql-client-version" - } - - /// The default client name to use when setting up the `clientName` property - public static var defaultClientName: String { - guard let identifier = Bundle.main.bundleIdentifier else { - return "apollo-ios-client" - } - - return "\(identifier)-apollo-ios" - } - - /// The default client version to use when setting up the `clientVersion` property. - public static var defaultClientVersion: String { - var version = String() - if let shortVersion = Bundle.main.shortVersion { - version.append(shortVersion) - } - - if let buildNumber = Bundle.main.buildNumber { - if version.isEmpty { - version.append(buildNumber) - } else { - version.append("-\(buildNumber)") - } - } - - if version.isEmpty { - version = "(unknown)" - } - - return version - } - } diff --git a/apollo-ios/Sources/Apollo/JSONRequest.swift b/apollo-ios/Sources/Apollo/JSONRequest.swift index c5498d173..da7b21a88 100644 --- a/apollo-ios/Sources/Apollo/JSONRequest.swift +++ b/apollo-ios/Sources/Apollo/JSONRequest.swift @@ -38,7 +38,10 @@ public struct JSONRequest: GraphQLRequest, AutoPers /// Mutation operations always use POST, even when this is `false` public let useGETForQueries: Bool - private let sendEnhancedClientAwareness: Bool + /// The telemetry metadata about the client. This is used by GraphOS Studio's + /// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation) + /// feature. + public var clientAwarenessMetadata: ClientAwarenessMetadata /// Designated initializer /// @@ -59,14 +62,12 @@ public struct JSONRequest: GraphQLRequest, AutoPers operation: Operation, graphQLEndpoint: URL, contextIdentifier: UUID? = nil, - clientName: String? = Self.defaultClientName, - clientVersion: String? = Self.defaultClientVersion, cachePolicy: CachePolicy = .default, context: (any RequestContext)? = nil, apqConfig: AutoPersistedQueryConfiguration = .init(), useGETForQueries: Bool = false, requestBodyCreator: any JSONRequestBodyCreator = DefaultRequestBodyCreator(), - sendEnhancedClientAwareness: Bool = true + clientAwarenessMetadata: ClientAwarenessMetadata = ClientAwarenessMetadata() ) { self.operation = operation self.graphQLEndpoint = graphQLEndpoint @@ -77,20 +78,13 @@ public struct JSONRequest: GraphQLRequest, AutoPers self.apqConfig = apqConfig self.useGETForQueries = useGETForQueries - self.sendEnhancedClientAwareness = sendEnhancedClientAwareness + self.clientAwarenessMetadata = clientAwarenessMetadata - self.setupDefaultHeaders( - clientName: clientName, - clientVersion: clientVersion - ) + self.setupDefaultHeaders() } - private mutating func setupDefaultHeaders( - clientName: String? = Self.defaultClientName, - clientVersion: String? = Self.defaultClientVersion - ) { - self.addHeader(name: "Content-Type", value: "application/json") - self.addApolloClientHeaders(clientName: clientName, clientVersion: clientVersion) + private mutating func setupDefaultHeaders() { + self.addHeader(name: "Content-Type", value: "application/json") if Operation.operationType == .subscription { self.addHeader( @@ -181,11 +175,7 @@ public struct JSONRequest: GraphQLRequest, AutoPers for: self, sendQueryDocument: sendQueryDocument, autoPersistQuery: autoPersistQueries - ) - - if self.sendEnhancedClientAwareness { - addEnhancedClientAwarenessExtension(to: &body) - } + ) return body } diff --git a/apollo-ios/Sources/Apollo/RequestBodyCreator.swift b/apollo-ios/Sources/Apollo/RequestBodyCreator.swift index 7ab7a5b28..446175e9f 100644 --- a/apollo-ios/Sources/Apollo/RequestBodyCreator.swift +++ b/apollo-ios/Sources/Apollo/RequestBodyCreator.swift @@ -2,6 +2,11 @@ import ApolloAPI #endif +public struct DefaultRequestBodyCreator: JSONRequestBodyCreator { + // Internal init methods cannot be used in public methods + public init() { } +} + public protocol JSONRequestBodyCreator: Sendable { #warning("TODO: replace with version that takes request after rewriting websocket") /// Creates a `JSONEncodableDictionary` out of the passed-in operation @@ -24,6 +29,7 @@ public protocol JSONRequestBodyCreator: Sendable { autoPersistQuery: Bool, clientAwarenessMetadata: ClientAwarenessMetadata ) -> JSONEncodableDictionary + } // MARK: - Default Implementation @@ -81,15 +87,12 @@ extension JSONRequestBodyCreator { ] } + clientAwarenessMetadata.applyExtension(to: &body) + return body } } -public struct DefaultRequestBodyCreator: JSONRequestBodyCreator { - // Internal init methods cannot be used in public methods - public init() { } -} - // MARK: - Deprecations @available(*, deprecated, renamed: "JSONRequestBodyCreator") diff --git a/apollo-ios/Sources/Apollo/RequestChain.swift b/apollo-ios/Sources/Apollo/RequestChain.swift index 6193b6b6d..89a777fe0 100644 --- a/apollo-ios/Sources/Apollo/RequestChain.swift +++ b/apollo-ios/Sources/Apollo/RequestChain.swift @@ -4,13 +4,17 @@ import Foundation import ApolloAPI #endif +#warning("TODO: Implement retrying based on catching error") public struct RequestChainRetry: Swift.Error { public let request: Request + public let underlyingError: (any Swift.Error)? public init( request: Request, + underlyingError: (any Error)? = nil ) { self.request = request + self.underlyingError = underlyingError } } @@ -66,8 +70,22 @@ struct RequestChain: Sendable { func kickoff( request: Request ) -> ResultStream where Operation: GraphQLQuery { - return doInRetryingAsyncThrowingStream(request: request) { request, continuation in - let didYieldCacheData = try await handleCacheRead(request: request, continuation: continuation) + return doInAsyncThrowingStream { continuation in + + var didYieldCacheData = false + + if request.cachePolicy.shouldAttemptCacheRead { + do { + let cacheData = try await cacheInterceptor.readCacheData(for: request.operation) + continuation.yield(cacheData) + didYieldCacheData = true + + } catch { + if case .returnCacheDataDontFetch = request.cachePolicy { + throw error + } + } + } if request.cachePolicy.shouldFetchFromNetwork(hadSuccessfulCacheRead: didYieldCacheData) { try await kickoffRequestInterceptors(for: request, continuation: continuation) @@ -78,27 +96,22 @@ struct RequestChain: Sendable { func kickoff( request: Request ) -> ResultStream { - return doInRetryingAsyncThrowingStream(request: request) { request, continuation in - try await kickoffRequestInterceptors(for: request, continuation: continuation) + return doInAsyncThrowingStream { continuation in + } } - private func doInRetryingAsyncThrowingStream( - request: Request, - _ body: @escaping @Sendable (Request, ResultStream.Continuation) async throws -> Void + private func doInAsyncThrowingStream( + _ body: @escaping @Sendable (ResultStream.Continuation) async throws -> Void ) -> ResultStream { return AsyncThrowingStream { continuation in let task = Task { do { - try await doHandlingRetries(request: request) { request in - try await body(request, continuation) - } - + try await body(continuation) + continuation.finish() } catch { continuation.finish(throwing: error) } - - continuation.finish() } continuation.onTermination = { _ in @@ -107,39 +120,6 @@ struct RequestChain: Sendable { } } - private func doHandlingRetries( - request: Request, - _ body: @escaping @Sendable (Request) async throws -> Void - ) async throws { - do { - try await body(request) - - } catch let error as RequestChainRetry { - try await self.doHandlingRetries(request: error.request, body) - } - } - - private func handleCacheRead( - request: Request, - continuation: ResultStream.Continuation - ) async throws -> Bool where Operation: GraphQLQuery { - guard request.cachePolicy.shouldAttemptCacheRead else { - return false - } - - do { - let cacheData = try await cacheInterceptor.readCacheData(for: request.operation) - continuation.yield(cacheData) - return true - - } catch { - if case .returnCacheDataDontFetch = request.cachePolicy { - throw error - } - return false - } - } - private func kickoffRequestInterceptors( for initialRequest: Request, continuation: ResultStream.Continuation @@ -174,7 +154,8 @@ struct RequestChain: Sendable { } guard didEmitResult else { - throw RequestChainError.noResults + continuation.finish(throwing: RequestChainError.noResults) + return } } diff --git a/apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift b/apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift index ac0583462..9c933ba02 100644 --- a/apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift +++ b/apollo-ios/Sources/Apollo/RequestChainNetworkTransport.swift @@ -14,8 +14,6 @@ public final class RequestChainNetworkTransport: NetworkTransport, Sendable { /// The GraphQL endpoint URL to use. public let endpointURL: URL - /// Any additional HTTP headers that should be added to **every** request, such as an API key or a language setting. - /// /// If a header should only be added to _certain_ requests, or if its value might differ between /// requests, you should add that header in an interceptor instead. /// @@ -42,7 +40,11 @@ public final class RequestChainNetworkTransport: NetworkTransport, Sendable { /// Defaults to a ``DefaultRequestBodyCreator`` initialized with the default configuration. public let requestBodyCreator: any JSONRequestBodyCreator - private let sendEnhancedClientAwareness: Bool + /// Any additional HTTP headers that should be added to **every** request, such as an API key or a language setting. + ////// The telemetry metadata about the client. This is used by GraphOS Studio's + /// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation) + /// feature. + public let clientAwarenessMetadata: ClientAwarenessMetadata /// Designated initializer /// @@ -64,7 +66,7 @@ public final class RequestChainNetworkTransport: NetworkTransport, Sendable { apqConfig: AutoPersistedQueryConfiguration = .init(), requestBodyCreator: any JSONRequestBodyCreator = DefaultRequestBodyCreator(), useGETForQueries: Bool = false, - sendEnhancedClientAwareness: Bool = true + clientAwarenessMetadata: ClientAwarenessMetadata = ClientAwarenessMetadata() ) { self.interceptorProvider = interceptorProvider self.endpointURL = endpointURL @@ -73,7 +75,7 @@ public final class RequestChainNetworkTransport: NetworkTransport, Sendable { self.apqConfig = apqConfig self.requestBodyCreator = requestBodyCreator self.useGETForQueries = useGETForQueries - self.sendEnhancedClientAwareness = sendEnhancedClientAwareness + self.clientAwarenessMetadata = clientAwarenessMetadata } /// Constructs a GraphQL request for the given operation. @@ -96,14 +98,12 @@ public final class RequestChainNetworkTransport: NetworkTransport, Sendable { operation: operation, graphQLEndpoint: self.endpointURL, contextIdentifier: contextIdentifier, - clientName: self.clientName, - clientVersion: self.clientVersion, cachePolicy: cachePolicy, context: context, apqConfig: self.apqConfig, useGETForQueries: self.useGETForQueries, requestBodyCreator: self.requestBodyCreator, - sendEnhancedClientAwareness: self.sendEnhancedClientAwareness + clientAwarenessMetadata: self.clientAwarenessMetadata ) request.addHeaders(self.additionalHeaders) return request @@ -183,15 +183,13 @@ extension RequestChainNetworkTransport: UploadingNetworkTransport { var request = UploadRequest( operation: operation, graphQLEndpoint: self.endpointURL, - clientName: self.clientName, - clientVersion: self.clientVersion, files: files, multipartBoundary: manualBoundary, context: context, requestBodyCreator: self.requestBodyCreator, - sendEnhancedClientAwareness: self.sendEnhancedClientAwareness + clientAwarenessMetadata: self.clientAwarenessMetadata ) - request.additionalHeaders = self.additionalHeaders + request.addHeaders(self.additionalHeaders) return request } diff --git a/apollo-ios/Sources/Apollo/RequestClientMetadata.swift b/apollo-ios/Sources/Apollo/RequestClientMetadata.swift deleted file mode 100644 index 038579c32..000000000 --- a/apollo-ios/Sources/Apollo/RequestClientMetadata.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation -#if !COCOAPODS -import ApolloAPI -#endif - -extension JSONRequest { - /// Adds client metadata to the request body in the `extensions` key. - /// - /// - Parameter body: The previously generated JSON body. - func addEnhancedClientAwarenessExtension(to body: inout JSONEncodableDictionary) { - _addEnhancedClientAwarenessExtension(to: &body) - } -} - -extension UploadRequest { - /// Adds client metadata to the request body in the `extensions` key. - /// - /// - Parameter body: The previously generated JSON body. - func addEnhancedClientAwarenessExtension(to body: inout JSONEncodableDictionary) { - _addEnhancedClientAwarenessExtension(to: &body) - } -} - -fileprivate func _addEnhancedClientAwarenessExtension(to body: inout JSONEncodableDictionary) { - let clientLibraryMetadata: JSONEncodableDictionary = [ - "name": Constants.ApolloClientName, - "version": Constants.ApolloClientVersion - ] - - var extensions = body["extensions"] as? JSONEncodableDictionary ?? JSONEncodableDictionary() - extensions["clientLibrary"] = clientLibraryMetadata - - body["extensions"] = extensions -} diff --git a/apollo-ios/Sources/Apollo/UploadRequest.swift b/apollo-ios/Sources/Apollo/UploadRequest.swift index c85f978e3..9088c632b 100644 --- a/apollo-ios/Sources/Apollo/UploadRequest.swift +++ b/apollo-ios/Sources/Apollo/UploadRequest.swift @@ -29,15 +29,16 @@ public struct UploadRequest: GraphQLRequest { public let serializationFormat = JSONSerializationFormat.self - private let sendEnhancedClientAwareness: Bool + /// The telemetry metadata about the client. This is used by GraphOS Studio's + /// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation) + /// feature. + public var clientAwarenessMetadata: ClientAwarenessMetadata /// Designated Initializer /// /// - Parameters: /// - operation: The GraphQL Operation to execute /// - graphQLEndpoint: The endpoint to make a GraphQL request to - /// - clientName: The name of the client to send with the `"apollographql-client-name"` header - /// - clientVersion: The version of the client to send with the `"apollographql-client-version"` header /// - files: The array of files to upload for all `Upload` parameters in the mutation. /// - multipartBoundary: [optional] A boundary to use for the multipart request. /// - context: [optional] A context that is being passed through the request chain. Should default to `nil`. @@ -45,13 +46,11 @@ public struct UploadRequest: GraphQLRequest { public init( operation: Operation, graphQLEndpoint: URL, - clientName: String? = Self.defaultClientName, - clientVersion: String? = Self.defaultClientVersion, files: [GraphQLFile], multipartBoundary: String? = nil, context: (any RequestContext)? = nil, requestBodyCreator: any JSONRequestBodyCreator = DefaultRequestBodyCreator(), - sendEnhancedClientAwareness: Bool = true + clientAwarenessMetadata: ClientAwarenessMetadata = ClientAwarenessMetadata() ) { self.operation = operation self.graphQLEndpoint = graphQLEndpoint @@ -60,8 +59,8 @@ public struct UploadRequest: GraphQLRequest { self.requestBodyCreator = requestBodyCreator self.files = files self.multipartBoundary = multipartBoundary ?? "apollo-ios.boundary.\(UUID().uuidString)" + self.clientAwarenessMetadata = clientAwarenessMetadata - self.addApolloClientHeaders(clientName: clientName, clientVersion: clientVersion) self.addHeader(name: "Content-Type", value: "multipart/form-data; boundary=\(self.multipartBoundary)") } @@ -98,11 +97,7 @@ public struct UploadRequest: GraphQLRequest { variables.updateValue(NSNull(), forKey: fieldName) } } - fields["variables"] = variables - - if self.sendEnhancedClientAwareness { - addEnhancedClientAwarenessExtension(to: &fields) - } + fields["variables"] = variables let operationData = try JSONSerializationFormat.serialize(value: fields) formData.appendPart(data: operationData, name: "operations") diff --git a/apollo-ios/Sources/ApolloWebSocket/OperationMessageIdCreator.swift b/apollo-ios/Sources/ApolloWebSocket/OperationMessageIdCreator.swift index cf01b3831..bd69cb712 100644 --- a/apollo-ios/Sources/ApolloWebSocket/OperationMessageIdCreator.swift +++ b/apollo-ios/Sources/ApolloWebSocket/OperationMessageIdCreator.swift @@ -3,7 +3,7 @@ import Foundation import Apollo #endif -public protocol OperationMessageIdCreator { +public protocol OperationMessageIdCreator: Sendable { func requestId() -> String } diff --git a/apollo-ios/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/apollo-ios/Sources/ApolloWebSocket/SplitNetworkTransport.swift index 5121cfdfe..43cfc3768 100644 --- a/apollo-ios/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/apollo-ios/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -16,26 +16,6 @@ public final class SplitNetworkTransport: Sendable { private let uploadingNetworkTransport: any UploadingNetworkTransport private let webSocketNetworkTransport: any SubscriptionNetworkTransport - public var clientName: String { - let httpName = self.uploadingNetworkTransport.clientName - let websocketName = self.webSocketNetworkTransport.clientName - if httpName == websocketName { - return httpName - } else { - return "SPLIT_HTTPNAME_\(httpName)_WEBSOCKETNAME_\(websocketName)" - } - } - - public var clientVersion: String { - let httpVersion = self.uploadingNetworkTransport.clientVersion - let websocketVersion = self.webSocketNetworkTransport.clientVersion - if httpVersion == websocketVersion { - return httpVersion - } else { - return "SPLIT_HTTPVERSION_\(httpVersion)_WEBSOCKETVERSION_\(websocketVersion)" - } - } - /// Designated initializer /// /// - Parameters: diff --git a/apollo-ios/Sources/ApolloWebSocket/WebSocketTransport.swift b/apollo-ios/Sources/ApolloWebSocket/WebSocketTransport.swift index dc2201e57..34f6c4128 100644 --- a/apollo-ios/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/apollo-ios/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -62,29 +62,7 @@ public class WebSocketTransport: @unchecked Sendable { set { config.$reconnect.mutate { $0 = newValue } } } - /// - NOTE: Setting this won't override immediately if the socket is still connected, only on reconnection. - public var clientName: String { - get { config.clientName } - set { - config.clientName = newValue - self.addApolloClientHeaders(to: &self.websocket.request) - } - } - - /// - NOTE: Setting this won't override immediately if the socket is still connected, only on reconnection. - public var clientVersion: String { - get { config.clientVersion } - set { - config.clientVersion = newValue - self.addApolloClientHeaders(to: &self.websocket.request) - } - } - - public struct Configuration { - /// The client name to use for this client. Defaults to `Self.defaultClientName` - public fileprivate(set) var clientName: String - /// The client version to use for this client. Defaults to `Self.defaultClientVersion`. - public fileprivate(set) var clientVersion: String + public struct Configuration: Sendable { /// Whether to auto reconnect when websocket looses connection. Defaults to true. @Atomic public var reconnect: Bool /// How long to wait before attempting to reconnect. Defaults to half a second. @@ -98,25 +76,26 @@ public class WebSocketTransport: @unchecked Sendable { /// [optional]The payload to send on connection. Defaults to an empty `JSONEncodableDictionary`. public fileprivate(set) var connectingPayload: JSONEncodableDictionary? /// The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. - public let requestBodyCreator: any RequestBodyCreator + public let requestBodyCreator: any JSONRequestBodyCreator /// The `OperationMessageIdCreator` used to generate a unique message identifier per request. /// Defaults to `ApolloSequencedOperationMessageIdCreator`. public let operationMessageIdCreator: any OperationMessageIdCreator + /// The telemetry metadata about the client. This is used by GraphOS Studio's + /// [client awareness](https://www.apollographql.com/docs/graphos/platform/insights/client-segmentation) + /// feature. + public let clientAwarenessMetadata: ClientAwarenessMetadata /// The designated initializer public init( - clientName: String = WebSocketTransport.defaultClientName, - clientVersion: String = WebSocketTransport.defaultClientVersion, reconnect: Bool = true, reconnectionInterval: TimeInterval = 0.5, allowSendingDuplicates: Bool = true, connectOnInit: Bool = true, connectingPayload: JSONEncodableDictionary? = [:], - requestBodyCreator: any RequestBodyCreator = ApolloRequestBodyCreator(), - operationMessageIdCreator: any OperationMessageIdCreator = ApolloSequencedOperationMessageIdCreator() + requestBodyCreator: any JSONRequestBodyCreator = DefaultRequestBodyCreator(), + operationMessageIdCreator: any OperationMessageIdCreator = ApolloSequencedOperationMessageIdCreator(), + clientAwarenessMetadata: ClientAwarenessMetadata = ClientAwarenessMetadata() ) { - self.clientName = clientName - self.clientVersion = clientVersion self._reconnect = Atomic(wrappedValue: reconnect) self.reconnectionInterval = reconnectionInterval self.allowSendingDuplicates = allowSendingDuplicates @@ -124,6 +103,7 @@ public class WebSocketTransport: @unchecked Sendable { self.connectingPayload = connectingPayload self.requestBodyCreator = requestBodyCreator self.operationMessageIdCreator = operationMessageIdCreator + self.clientAwarenessMetadata = clientAwarenessMetadata } } @@ -166,7 +146,7 @@ public class WebSocketTransport: @unchecked Sendable { self.store = store self.config = config - self.addApolloClientHeaders(to: &self.websocket.request) + config.clientAwarenessMetadata.applyHeaders(to: &self.websocket.request) self.websocket.delegate = self // Keep the assignment of the callback queue before attempting to connect. There is the @@ -337,7 +317,11 @@ public class WebSocketTransport: @unchecked Sendable { self.websocket.delegate = nil } +<<<<<<< HEAD func sendHelper(operation: Operation, resultHandler: @escaping @Sendable (_ result: Result) async -> Void) -> String? { +======= + func sendHelper(operation: Operation, resultHandler: @escaping @Sendable (_ result: Result) -> Void) -> String? { +>>>>>>> b42f6fc6 (Apply Client awarness configuration to other network transports) let body = config.requestBodyCreator.requestBody( for: operation, sendQueryDocument: true, @@ -450,8 +434,35 @@ extension URLRequest { // MARK: - NetworkTransport conformance -extension WebSocketTransport: NetworkTransport { - public func send( +extension WebSocketTransport: SubscriptionNetworkTransport { + public func send( + query: Query, + cachePolicy: CachePolicy, + contextIdentifier: UUID?, + context: (any RequestContext)? + ) throws -> AsyncThrowingStream, any Error> { + try send(operation: query, cachePolicy: cachePolicy, contextIdentifier: contextIdentifier, context: context) + } + + public func send( + mutation: Mutation, + cachePolicy: CachePolicy, + contextIdentifier: UUID?, + context: (any RequestContext)? + ) throws -> AsyncThrowingStream, any Error> { + try send(operation: mutation, cachePolicy: cachePolicy, contextIdentifier: contextIdentifier, context: context) + } + + public func send( + subscription: Subscription, + cachePolicy: CachePolicy, + contextIdentifier: UUID?, + context: (any RequestContext)? + ) throws -> AsyncThrowingStream, any Error> { + try send(operation: subscription, cachePolicy: cachePolicy, contextIdentifier: contextIdentifier, context: context) + } + + private func send( operation: Operation, cachePolicy: CachePolicy, contextIdentifier: UUID? = nil, @@ -611,3 +622,18 @@ extension WebSocketTransport: WebSocketClientDelegate { } } + +// MARK: - Deprecations +extension WebSocketTransport { + + @available(*, deprecated, renamed: "config.clientAwarenessMetadata.clientApplicationName") + public var clientName: String { + config.clientAwarenessMetadata.clientApplicationName ?? "" + } + + @available(*, deprecated, renamed: "config.clientAwarenessMetadata.clientApplicationVersion") + public var clientVersion: String { + config.clientAwarenessMetadata.clientApplicationVersion ?? "" + } + +}