From c2776f422efc32f92c3794b4c80f8bb02b3ffbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Wed, 14 Jan 2026 14:11:47 +0100 Subject: [PATCH 1/2] feat: add streaming support for @defer and subscriptions Add protocol methods and implementations for: - Deferred queries/mutations with IncrementalDeferredResponseFormat - GraphQL subscriptions via AsyncThrowingStream All streaming methods return AsyncThrowingStream with proper error mapping to GraphQLAPIAdapterError and cancellation support. Co-Authored-By: Claude Opus 4.5 --- .../GraphQLRequestConfiguration.swift | 16 +++ Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift | 126 ++++++++++++++++++ 2 files changed, 142 insertions(+) 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() + } + } + } } From 5161770d687053d2e23c1f33013970e9983c9c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Wed, 14 Jan 2026 15:06:21 +0100 Subject: [PATCH 2/2] feat(docs): Document GraphQL subscriptions and deferred responses Updates README to reflect support for GraphQL subscriptions and deferred query responses, including usage examples. --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) 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), .