diff --git a/README.md b/README.md index afab50d..373f050 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ Developed to simplify [Futured](https://www.futured.app) in-house development of Currently there is no support for some Apollo's features: - Apollo built-in cache -- GraphQL subscriptions - Custom interceptors Network observers are available for logging and analytics. @@ -128,6 +127,25 @@ let queryResult = try await apiAdapter.fetch(query: query) let mutationResult = try await apiAdapter.perform(mutation: mutation) ``` +### Subscriptions +```swift +let subscriptionStream = try await apiAdapter.subscribe(subscription: MySubscription()) + +for try await data in subscriptionStream { + print("Received: \(data)") +} +``` + +### Deferred Responses (@defer) +```swift +let deferredStream = try apiAdapter.fetch(query: MyDeferredQuery()) + +for try await data in deferredStream { + // Data arrives progressively as deferred fragments complete + print("Received: \(data)") +} +``` + ## Contributors - [Ievgen Samoilyk](https://github.com/samoilyk), . diff --git a/Sources/GraphQLAPIKit/Configuration/GraphQLRequestConfiguration.swift b/Sources/GraphQLAPIKit/Configuration/GraphQLRequestConfiguration.swift index 5df3f99..fa95f36 100644 --- a/Sources/GraphQLAPIKit/Configuration/GraphQLRequestConfiguration.swift +++ b/Sources/GraphQLAPIKit/Configuration/GraphQLRequestConfiguration.swift @@ -22,3 +22,19 @@ public struct GraphQLRequestConfiguration: GraphQLOperationConfiguration { self.headers = headers } } + +/// Configuration for GraphQL subscriptions. +/// +/// Use this struct to customize subscription-specific options. +/// Subscriptions are long-lived connections that may require different configuration than standard requests. +public struct GraphQLSubscriptionConfiguration: GraphQLOperationConfiguration { + /// Additional headers to add to the subscription request. + public let headers: RequestHeaders? + + /// Creates a new subscription configuration. + /// + /// - Parameter headers: Additional headers to add to the request. Defaults to `nil`. + public init(headers: RequestHeaders? = nil) { + self.headers = headers + } +} diff --git a/Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift b/Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift index b45a37e..974ae64 100644 --- a/Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift +++ b/Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift @@ -3,6 +3,9 @@ import ApolloAPI import Foundation public protocol GraphQLAPIAdapterProtocol: AnyObject, Sendable { + + // MARK: - Single Response + /// Fetches a query from the server. /// Apollo cache is ignored. /// @@ -27,6 +30,49 @@ public protocol GraphQLAPIAdapterProtocol: AnyObject, Sendable { mutation: Mutation, configuration: GraphQLRequestConfiguration ) async throws -> Mutation.Data where Mutation.ResponseFormat == SingleResponseFormat + + // MARK: - Incremental/Deferred Response + + /// Fetches a query with `@defer` directive from the server. + /// Returns a stream that emits data progressively as deferred fragments arrive. + /// + /// - Parameters: + /// - query: The query to fetch (must use `@defer` directive). + /// - configuration: Additional request configuration. + /// - Returns: An async stream of query data, emitting updates as deferred data arrives. + /// - Throws: `GraphQLAPIAdapterError` on stream creation failure. + func fetch( + query: Query, + configuration: GraphQLRequestConfiguration + ) throws -> AsyncThrowingStream where Query.ResponseFormat == IncrementalDeferredResponseFormat + + /// Performs a mutation with `@defer` directive. + /// Returns a stream that emits data progressively as deferred fragments arrive. + /// + /// - Parameters: + /// - mutation: The mutation to perform (must use `@defer` directive). + /// - configuration: Additional request configuration. + /// - Returns: An async stream of mutation data, emitting updates as deferred data arrives. + /// - Throws: `GraphQLAPIAdapterError` on stream creation failure. + func perform( + mutation: Mutation, + configuration: GraphQLRequestConfiguration + ) throws -> AsyncThrowingStream where Mutation.ResponseFormat == IncrementalDeferredResponseFormat + + // MARK: - Subscriptions + + /// Subscribes to a GraphQL subscription. + /// Returns a stream that emits events as they arrive from the server. + /// + /// - Parameters: + /// - subscription: The subscription to subscribe to. + /// - configuration: Additional subscription configuration. + /// - Returns: An async stream of subscription data, emitting events as they arrive. + /// - Throws: `GraphQLAPIAdapterError` on stream creation failure. + func subscribe( + subscription: Subscription, + configuration: GraphQLSubscriptionConfiguration + ) async throws -> AsyncThrowingStream } public final class GraphQLAPIAdapter: GraphQLAPIAdapterProtocol, Sendable { @@ -117,4 +163,84 @@ public final class GraphQLAPIAdapter: GraphQLAPIAdapterProtocol, Sendable { return data } + + // MARK: - Incremental/Deferred Response + + public func fetch( + query: Query, + configuration: GraphQLRequestConfiguration = GraphQLRequestConfiguration() + ) throws -> AsyncThrowingStream where Query.ResponseFormat == IncrementalDeferredResponseFormat { + let config = RequestConfiguration(writeResultsToCache: false) + + let apolloStream = try apollo.fetch( + query: query, + cachePolicy: .networkOnly, + requestConfiguration: config + ) + + return transformStream(apolloStream) + } + + public func perform( + mutation: Mutation, + configuration: GraphQLRequestConfiguration = GraphQLRequestConfiguration() + ) throws -> AsyncThrowingStream where Mutation.ResponseFormat == IncrementalDeferredResponseFormat { + let config = RequestConfiguration(writeResultsToCache: false) + + let apolloStream = try apollo.perform( + mutation: mutation, + requestConfiguration: config + ) + + return transformStream(apolloStream) + } + + // MARK: - Subscriptions + + public func subscribe( + subscription: Subscription, + configuration: GraphQLSubscriptionConfiguration = GraphQLSubscriptionConfiguration() + ) async throws -> AsyncThrowingStream { + let config = RequestConfiguration(writeResultsToCache: false) + + let apolloStream = try await apollo.subscribe( + subscription: subscription, + requestConfiguration: config + ) + + return transformStream(apolloStream) + } + + // MARK: - Private Helpers + + /// Transforms an Apollo response stream into a data stream with error mapping. + private func transformStream( + _ apolloStream: AsyncThrowingStream, Error> + ) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + let task = Task { + do { + for try await response in apolloStream { + // Check for GraphQL errors + if let errors = response.errors, !errors.isEmpty { + continuation.finish(throwing: GraphQLAPIAdapterError(error: ApolloError(errors: errors))) + return + } + + // Yield data if present (may be partial for @defer) + if let data = response.data { + continuation.yield(data) + } + } + continuation.finish() + } catch { + continuation.finish(throwing: GraphQLAPIAdapterError(error: error)) + } + } + + continuation.onTermination = { @Sendable _ in + task.cancel() + } + } + } }