Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/// A `GraphQLExecutionSource` configured to execute upon the data stored in a ``NormalizedCache``.
///
/// Each object exposed by the cache is represented as a `Record`.
/// Each object exposed by the cache is represented as a ``Record``.
struct CacheDataExecutionSource: GraphQLExecutionSource {
typealias RawObjectData = Record
typealias FieldCollector = CacheDataFieldSelectionCollector
Expand Down
5 changes: 4 additions & 1 deletion apollo-ios/Sources/Apollo/GraphQLResult.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
@_spi(Internal) @_spi(Unsafe) import ApolloAPI

/// Represents the result of a GraphQL operation.
/// Represents the result of a GraphQL operation, including the response data as well as any ``GraphQLError``s
/// or extension data included in the response.
public struct GraphQLResponse<Operation: GraphQLOperation>: Sendable {

/// Represents source of data
public enum Source: Sendable, Hashable {
/// Indicates response data was fetched from a local cache
case cache
/// Indicates response data was fetched from a remote server
case server
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Foundation
import ApolloAPI

/// A ``GraphQLInterceptor`` that handles the functionality of automatic persisted queries (APQs).
///
/// **Prerequisite:** The `Request` used with this interceptor must be a ``JSONRequest``, otherwise this
/// interceptor will forward the request without performing any operations.
public struct AutomaticPersistedQueryInterceptor: GraphQLInterceptor {

public enum APQError: LocalizedError, Equatable {
Expand Down
35 changes: 34 additions & 1 deletion apollo-ios/Sources/Apollo/Interceptors/CacheInterceptor.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,43 @@
import ApolloAPI

/// A protocol for an interceptor in a ``RequestChain`` that handles cache reads and writes.
///
/// For most use cases, the ``DefaultCacheInterceptor`` will be sufficient to perform direct cache reads and writes. If
/// you require custom logic for manipulating cache data, that cannot be achieved by using the
/// [`@typePolicy` and `@fieldPolicy` directives](https://www.apollographql.com/docs/ios/caching/cache-key-resolution)
/// or [programmatic cache key configuration](https://www.apollographql.com/docs/ios/caching/programmatic-cache-keys),
/// you may need to implement a custom ``CacheInterceptor``.
public protocol CacheInterceptor: Sendable {

/// Reads cache data from the given ``ApolloStore`` for the request.
///
/// This function will be called after the pre-flight steps of the ``GraphQLInterceptor``s in the ``RequestChain``
/// are completed if the `request`'s ``GraphQLRequest/fetchBehavior`` indicates that a pre-fetch cache read should be
/// attempted.
///
/// Additionally, this function will be called after a failed network fetch if the `request`'s
/// ``GraphQLRequest/fetchBehavior`` indicates that a cache read should be attempted on a network failure.
///
/// - Parameters:
/// - store: The ``ApolloStore`` to read cache data from
/// - request: The ``GraphQLRequest`` to read cache data for
/// - Returns: A ``GraphQLResponse`` read from the cache if the data exists. Should return `nil` on a cache miss.
func readCacheData<Request: GraphQLRequest>(
from store: ApolloStore,
request: Request
) async throws -> GraphQLResponse<Request.Operation>?


/// Writes response data for a request to the given ``ApolloStore``.
///
/// The `response`'s ``ParsedResult/cacheRecords`` field contains the record set that should be written to the cache.
///
/// This function will be called after the post-flight response data has been parsed and successfully processed
/// through all of the ``RequestChain``'s ``GraphQLInterceptor``s.
///
/// - Parameters:
/// - store: The ``ApolloStore`` to write cache data to
/// - request: The ``GraphQLRequest`` used to fetch the data in the `response`
/// - response: The parsed response data for the `request`
func writeCacheData<Request: GraphQLRequest>(
to store: ApolloStore,
request: Request,
Expand All @@ -15,6 +46,8 @@ public protocol CacheInterceptor: Sendable {

}

/// A default implementation of a ``CacheInterceptor`` which performs direct cache reads and writes to the given
/// ``ApolloStore``.
public struct DefaultCacheInterceptor: CacheInterceptor {

public init() {}
Expand Down
101 changes: 67 additions & 34 deletions apollo-ios/Sources/Apollo/Interceptors/GraphQLInterceptor.swift
Original file line number Diff line number Diff line change
@@ -1,72 +1,105 @@
import Foundation
import ApolloAPI
import Foundation

/// The stream of results passed through a series of ``GraphQLInterceptor``s by a ``RequestChain``.
///
/// This is a stream of ``ParsedResult``s wrapped in a ``NonCopyableAsyncThrowingStream`` to ensure the stream's values
/// are not consumed by intermediary interceptors.
///
/// Because some requests may have a multi-part response, such as subscriptions or operations using `@defer`, the
/// results of a ``RequestChain`` are processed as a stream. For requests that should have a single response, the stream
/// will emit a single value and then terminate.
public typealias InterceptorResultStream<Request: GraphQLRequest> =
NonCopyableAsyncThrowingStream<ParsedResult<Request.Operation>>

public struct ParsedResult<Operation: GraphQLOperation>: Sendable, Hashable {
public let result: GraphQLResponse<Operation>
public let cacheRecords: RecordSet?
NonCopyableAsyncThrowingStream<ParsedResult<Request.Operation>>

public init(result: GraphQLResponse<Operation>, cacheRecords: RecordSet?) {
self.result = result
self.cacheRecords = cacheRecords
}
}

/// A protocol to set up a chainable unit of networking work that operates on a `GraphQLRequest` and `GraphQLResponse`.
/// A protocol for an interceptor in a ``RequestChain`` that can perform a unit of work that operates on a
/// ``GraphQLRequest`` and ``ParsedResult``.
///
/// Each ``GraphQLInterceptor`` provided by an ``InterceptorProvider`` will have it's intercept function called in
/// sequential order prior to beginning execution of the request. After request execution is complete, the
/// ``ParsedResult`` (which includes the ``GraphQLResponse``) is passed back up the interceptor chain in reverse order
/// such that the first interceptor called will be the last to receive the response.
/// The interceptor can perform pre-flight work on the ``GraphQLRequest`` and post-flight work on the ``ParsedResult``.
///
/// ## Pre-Flight
/// When the `intercept(request:next:)` function is called, the interceptor may inspect or modify the provided request.
/// The request must then be passed into the `next` closure to continue through the ``RequestChain``
/// Each ``GraphQLInterceptor`` provided by an ``InterceptorProvider`` will have it's ``intercept(request:next:)``
/// function called in sequential order prior to fetching the request.
///
/// The interceptor may inspect or modify the provided `request`, which must then be passed into the `next` closure to
/// continue through the ``RequestChain``
///
/// ## Post-Flight
/// After the request has been executed, the response will be emitted by the `InterceptorResultStream` returned by
/// the call to the `next` closure. The response may be inspected or modified by using the stream's, `.map` and
/// `.mapErrors` functions.
/// The interceptor must then return the stream to continue through the ``RequestChain``
/// After response data is fetched and parsed, the ``ParsedResult`` will be emitted by the ``InterceptorResultStream``
/// returned by the call to the `next` closure. The ``ParsedResult`` is passed back up the interceptor chain in reverse
/// order such that the first interceptor called will be the last to receive the response.
///
/// The response may be inspected or modified by using the mapping functions of ``NonCopyableAsyncThrowingStream``.
/// The interceptor must then return the stream to continue through the ``RequestChain``.
///
/// ## Error Handling
/// Both pre-flight and post-flight errors can be caught using the ``NonCopyableAsyncThrowingStream/mapErrors(_:)``
/// function of the ``InterceptorResultStream`` returned by calling the `next` closure. This will catch any errors
/// thrown in later steps of the ``RequestChain``, including:
/// - Pre-flight errors thrown by ``GraphQLInterceptor``s later in the ``RequestChain``.
/// - Networking errors thrown by the ``ApolloURLSession`` or ``HTTPInterceptor``s in the ``RequestChain``.
/// - Parsing errors thrown by the ``ResponseParsingInterceptor`` of the ``RequestChain``.
/// - Post-flight errors thrown by ``GraphQLInterceptor``s later in the request chain.
///
/// Your ``NonCopyableAsyncThrowingStream/mapErrors(_:)`` closure may rethrow the same error or a different error,
/// which will then be passed up through the rest of the request chain. If possible, you may recover from the error
/// by constructing and returning a ``ParsedResult``. Returning `nil` will suppress the error and terminate the
/// ``RequestChain``'s stream without emitting a result.
///
/// It is not required that every interceptor implement error handling. A ``GraphQLInterceptor`` that does not call
/// ``NonCopyableAsyncThrowingStream/mapErrors(_:)`` will be skipped if an error is emitted.
///
/// ## Example
/// As an example, a simple logging interceptor might look like this:
/// ```swift
/// struct LoggingInterceptor: GraphQLInterceptor {
///
///
/// let logger: Logger
///
///
/// func intercept<Request: GraphQLRequest>(
/// request: Request,
/// next: NextInterceptorFunction<Request>
/// ) async throws -> InterceptorResultStream<Request> {
/// // Pre-flight work
/// logger.log(request: request)
///
/// return await next(request).map { response in
///
/// // Proceed to next interceptor
/// return await next(request)
/// .map { response in
/// // Post-flight work
/// logger.log(response: response)
/// return response
///
/// }.mapErrors { error in
/// // Handle errors from later steps of the `RequestChain`
/// logger.log(error: error)
///
/// // Rethrows the error to the next interceptor.
/// throw error
/// }
/// }
/// ```
/// Additionally, the interceptor could modify the `request` passed into the `next(request)` closure; the `response`
/// returned from the `.map` closure, or the error thrown from the `mapErrors` closure.
public protocol GraphQLInterceptor: Sendable {

/// A closure called to proceed to the next step in the ``RequestChain`` after performing pre-flight work.
///
/// - Parameters:
/// - Request: The ``GraphQLRequest`` to send to the next step in the ``RequestChain``.
///
/// - Returns: An ``InterceptorResultStream`` used to intercept response data and perform post-flight work.
typealias NextInterceptorFunction<Request: GraphQLRequest> = @Sendable (Request) async ->
InterceptorResultStream<Request>

/// Called when this interceptor should do its work.
/// The entry point used to intercept the ``GraphQLRequest``.
///
/// This function is called by the ``RequestChain`` during pre-flight operations. Post-flight work can be performed
/// in the `map` functions of the ``InterceptorResultStream`` returned by calling the `next` closure.
///
/// - Parameters:
/// - chain: The chain the interceptor is a part of.
/// - request: The request, as far as it has been constructed
/// - response: [optional] The response, if received
/// - completion: The completion block to fire when data needs to be returned to the UI.
/// - request: The current pre-flight state of the request, may be modified by subsequent interceptors after
/// calling the `next` closure.
/// - next: The ``NextInterceptorFunction`` that should be called to proceed to the next step in the ``RequestChain``.
/// - Returns: The stream of results to pass to the next interceptor for post-flight processing.
func intercept<Request: GraphQLRequest>(
request: Request,
next: NextInterceptorFunction<Request>
Expand Down
53 changes: 53 additions & 0 deletions apollo-ios/Sources/Apollo/Interceptors/HTTPInterceptor.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,74 @@
import Foundation

/// A protocol for an interceptor in a ``RequestChain`` that can perform a unit of work that operates on an
/// `URLRequest` and ``HTTPResponse``.
///
/// The interceptor can perform pre-flight work on the `URLRequest` and post-flight work on the
/// ``HTTPResponse``, including the raw response `Data` stream of the response's ``HTTPResponse/chunks``.
///
/// ## Pre-Flight
/// After the ``RequestChain`` proceeds through the ``GraphQLInterceptor``s provided by it's ``InterceptorProvider``,
/// it will call ``GraphQLRequest/toURLRequest()`` on the final ``GraphQLRequest``. Each ``HTTPInterceptor`` provided by
/// the ``InterceptorProvider`` will then have it's ``intercept(request:next:)`` function called in sequential order
/// prior to fetching the request. When this function is called, the interceptor may
/// inspect or modify the provided `request`, which must then be passed into the `next` closure to continue through
/// the ``RequestChain``
///
/// ## Post-Flight
/// After the ``ApolloURLSession`` receives an initial response, an ``HTTPResponse`` will be returned by the call to
/// the `next` closure. As raw response data is received, the raw chunk `Data` is passed back up the interceptor chain
/// in reverse order such that the first ``HTTPInterceptor`` called will be the last to receive the response.
///
/// The response `Data` of the ``HTTPResponse/chunks`` may be inspected or modified by using the
/// ``HTTPResponse/mapChunks(_:)`` function of the ``HTTPResponse``. The interceptor must then return the mapped
/// ``HTTPResponse`` to continue through the ``RequestChain``.
public protocol HTTPInterceptor: Sendable {

/// A closure called to proceed to the next step in the ``RequestChain`` after performing pre-flight work.
///
/// - Parameters:
/// - Request: The `URLRequest` to send to the next step in the ``RequestChain``.
///
/// - Returns: An ``HTTPResponse`` used to intercept raw response data chunks and perform post-flight work.
typealias NextHTTPInterceptorFunction = @Sendable (URLRequest) async throws -> HTTPResponse

/// The entry point used to intercept the ``URLRequest``.
///
/// This function is called by the ``RequestChain`` during pre-flight operations. Post-flight work can be performed
/// in the ``HTTPResponse/mapChunks(_:)`` function of the ``HTTPResponse`` returned by calling the `next` closure.
///
/// - Parameters:
/// - request: The current pre-flight state of the request, may be modified by subsequent interceptors after
/// calling the `next` closure.
/// - next: The ``NextHTTPInterceptorFunction`` that should be called to proceed to the next step in the
/// ``RequestChain``.
/// - Returns: The stream of response data to pass to the next interceptor for post-flight processing.
func intercept(
request: URLRequest,
next: NextHTTPInterceptorFunction
) async throws -> HTTPResponse

}

/// A response from an HTTP request that is sent through a series of ``HTTPInterceptor``s in a ``RequestChain`` after
/// being fetched by an ``ApolloURLSession``.
public struct HTTPResponse: Sendable, ~Copyable {
/// The HTTP response info received for the request
public let response: HTTPURLResponse

/// The stream of chunks received for the ``HTTPResponse/response`` as raw `Data`.
///
/// Because some requests may have a multi-part response, such as subscriptions or operations using `@defer`, the
/// response is processed as a stream of chunks. For requests that should have a single response chunk, the stream
/// will emit a single value and then terminate.
public let chunks: NonCopyableAsyncThrowingStream<Data>

/// Maps the ``HTTPResponse/chunks`` of raw response data received for the response and returns a new ``HTTPResponse``
/// with the ``HTTPResponse/chunks`` returned from the `transform` block
///
/// - Parameter transform: A block called for each element emitted by the receiver's ``HTTPResponse/chunks`` stream.
/// - Returns: An ``HTTPResponse`` with a ``HTTPResponse/chunks`` stream that emits the `Data` returned from the
/// `transform` block.
public consuming func mapChunks(
_ transform: @escaping @Sendable (HTTPURLResponse, Data) async throws -> (Data)
) async -> HTTPResponse {
Expand Down
Loading
Loading