Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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), <ievgen.samoilyk@futured.app>.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
126 changes: 126 additions & 0 deletions Sources/GraphQLAPIKit/GraphQLAPIAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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: GraphQLQuery>(
query: Query,
configuration: GraphQLRequestConfiguration
) throws -> AsyncThrowingStream<Query.Data, Error> 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: GraphQLMutation>(
mutation: Mutation,
configuration: GraphQLRequestConfiguration
) throws -> AsyncThrowingStream<Mutation.Data, Error> 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: GraphQLSubscription>(
subscription: Subscription,
configuration: GraphQLSubscriptionConfiguration
) async throws -> AsyncThrowingStream<Subscription.Data, Error>
}

public final class GraphQLAPIAdapter: GraphQLAPIAdapterProtocol, Sendable {
Expand Down Expand Up @@ -117,4 +163,84 @@ public final class GraphQLAPIAdapter: GraphQLAPIAdapterProtocol, Sendable {

return data
}

// MARK: - Incremental/Deferred Response

public func fetch<Query: GraphQLQuery>(
query: Query,
configuration: GraphQLRequestConfiguration = GraphQLRequestConfiguration()
) throws -> AsyncThrowingStream<Query.Data, Error> 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: GraphQLMutation>(
mutation: Mutation,
configuration: GraphQLRequestConfiguration = GraphQLRequestConfiguration()
) throws -> AsyncThrowingStream<Mutation.Data, Error> 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: GraphQLSubscription>(
subscription: Subscription,
configuration: GraphQLSubscriptionConfiguration = GraphQLSubscriptionConfiguration()
) async throws -> AsyncThrowingStream<Subscription.Data, Error> {
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<Operation: GraphQLOperation>(
_ apolloStream: AsyncThrowingStream<GraphQLResponse<Operation>, Error>
) -> AsyncThrowingStream<Operation.Data, Error> {
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()
}
}
}
}