Skip to content

Commit

Permalink
Merge pull request #1341 from apollographql/explore/interceptors
Browse files Browse the repository at this point in the history
Network Rearchitecture
  • Loading branch information
designatednerd authored Sep 11, 2020
2 parents a764aa0 + 38135ee commit 713430d
Show file tree
Hide file tree
Showing 64 changed files with 2,807 additions and 1,597 deletions.
159 changes: 144 additions & 15 deletions Apollo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ let package = Package(
.testTarget(
name: "ApolloCodegenTests",
dependencies: [
"ApolloTestSupport",
"ApolloCodegenLib"
]),
.testTarget(
Expand Down
230 changes: 41 additions & 189 deletions Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public enum CachePolicy {
case returnCacheDataDontFetch
/// Return data from the cache if available, and always fetch results from the server.
case returnCacheDataAndFetch

/// The current default cache policy.
public static var `default`: CachePolicy {
.returnCacheDataElseFetch
}
}

/// A handler for operation results.
Expand All @@ -28,9 +33,6 @@ public class ApolloClient {

public let store: ApolloStore // <- conformance to ApolloClientProtocol

private let queue: DispatchQueue
private let operationQueue: OperationQueue

public enum ApolloClientError: Error, LocalizedError {
case noUploadTransport

Expand All @@ -50,69 +52,18 @@ public class ApolloClient {
public init(networkTransport: NetworkTransport, store: ApolloStore = ApolloStore(cache: InMemoryNormalizedCache())) {
self.networkTransport = networkTransport
self.store = store

queue = DispatchQueue(label: "com.apollographql.ApolloClient")
operationQueue = OperationQueue()
operationQueue.underlyingQueue = queue
}

/// Creates a client with an HTTP network transport connecting to the specified URL.
///
/// - Parameter url: The URL of a GraphQL server to connect to.
public convenience init(url: URL) {
self.init(networkTransport: HTTPNetworkTransport(url: url))
}

fileprivate func send<Operation: GraphQLOperation>(operation: Operation,
shouldPublishResultToStore: Bool,
context: UnsafeMutableRawPointer?,
resultHandler: @escaping GraphQLResultHandler<Operation.Data>) -> Cancellable {
return networkTransport.send(operation: operation) { [weak self] result in
guard let self = self else {
return
}
self.handleOperationResult(shouldPublishResultToStore: shouldPublishResultToStore,
context: context,
result,
resultHandler: resultHandler)
}
}

private func handleOperationResult<Data: GraphQLSelectionSet>(shouldPublishResultToStore: Bool,
context: UnsafeMutableRawPointer?,
_ result: Result<GraphQLResponse<Data>, Error>,
resultHandler: @escaping GraphQLResultHandler<Data>) {
switch result {
case .failure(let error):
resultHandler(.failure(error))
case .success(let response):
// If there is no need to publish the result to the store, we can use a fast path.
if !shouldPublishResultToStore {
do {
let result = try response.parseResultFast()
resultHandler(.success(result))
} catch {
resultHandler(.failure(error))
}
return
}

firstly {
try response.parseResult(cacheKeyForObject: self.cacheKeyForObject)
}.andThen { [weak self] (result, records) in
guard let self = self else {
return
}
if let records = records {
self.store.publish(records: records, context: context).catch { error in
preconditionFailure(String(describing: error))
}
}
resultHandler(.success(result))
}.catch { error in
resultHandler(.failure(error))
}
}
let store = ApolloStore(cache: InMemoryNormalizedCache())
let provider = LegacyInterceptorProvider(store: store)
let transport = RequestChainNetworkTransport(interceptorProvider: provider,
endpointURL: url)

self.init(networkTransport: transport, store: store)
}
}

Expand All @@ -124,180 +75,81 @@ extension ApolloClient: ApolloClientProtocol {
get {
return self.store.cacheKeyForObject
}

set {
self.store.cacheKeyForObject = newValue
}
}

public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result<Void, Error>) -> Void)? = nil) {
public func clearCache(callbackQueue: DispatchQueue = .main,
completion: ((Result<Void, Error>) -> Void)? = nil) {
self.store.clearCache(completion: completion)
}

@discardableResult public func fetch<Query: GraphQLQuery>(query: Query,
cachePolicy: CachePolicy = .returnCacheDataElseFetch,
context: UnsafeMutableRawPointer? = nil,
contextIdentifier: UUID? = nil,
queue: DispatchQueue = DispatchQueue.main,
resultHandler: GraphQLResultHandler<Query.Data>? = nil) -> Cancellable {
let resultHandler = wrapResultHandler(resultHandler, queue: queue)

// If we don't have to go through the cache, there is no need to create an operation
// and we can return a network task directly
if cachePolicy == .fetchIgnoringCacheData || cachePolicy == .fetchIgnoringCacheCompletely {
return self.send(operation: query, shouldPublishResultToStore: cachePolicy != .fetchIgnoringCacheCompletely, context: context, resultHandler: resultHandler)
} else {
let operation = FetchQueryOperation(client: self, query: query, cachePolicy: cachePolicy, context: context, resultHandler: resultHandler)
self.operationQueue.addOperation(operation)
return operation
return self.networkTransport.send(operation: query,
cachePolicy: cachePolicy,
contextIdentifier: contextIdentifier,
callbackQueue: queue) { result in
resultHandler?(result)
}
}

public func watch<Query: GraphQLQuery>(query: Query,
cachePolicy: CachePolicy = .returnCacheDataElseFetch,
queue: DispatchQueue = .main,
resultHandler: @escaping GraphQLResultHandler<Query.Data>) -> GraphQLQueryWatcher<Query> {
let watcher = GraphQLQueryWatcher(client: self,
query: query,
resultHandler: wrapResultHandler(resultHandler, queue: queue))
resultHandler: resultHandler)
watcher.fetch(cachePolicy: cachePolicy)
return watcher
}

@discardableResult
public func perform<Mutation: GraphQLMutation>(mutation: Mutation,
context: UnsafeMutableRawPointer? = nil,
queue: DispatchQueue = DispatchQueue.main,
queue: DispatchQueue = .main,
resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
return self.send(operation: mutation,
shouldPublishResultToStore: true,
context: context,
resultHandler: wrapResultHandler(resultHandler, queue: queue))
return self.networkTransport.send(operation: mutation,
cachePolicy: .default,
contextIdentifier: nil,
callbackQueue: queue) { result in
resultHandler?(result)
}
}

@discardableResult
public func upload<Operation: GraphQLOperation>(operation: Operation,
context: UnsafeMutableRawPointer? = nil,
files: [GraphQLFile],
queue: DispatchQueue = .main,
resultHandler: GraphQLResultHandler<Operation.Data>? = nil) -> Cancellable {
let wrappedHandler = wrapResultHandler(resultHandler, queue: queue)
guard let uploadingTransport = self.networkTransport as? UploadingNetworkTransport else {
assertionFailure("Trying to upload without an uploading transport. Please make sure your network transport conforms to `UploadingNetworkTransport`.")
wrappedHandler(.failure(ApolloClientError.noUploadTransport))
queue.async {
resultHandler?(.failure(ApolloClientError.noUploadTransport))
}
return EmptyCancellable()
}

return uploadingTransport.upload(operation: operation, files: files) { [weak self] result in
guard let self = self else {
return
}
self.handleOperationResult(shouldPublishResultToStore: true,
context: context, result,
resultHandler: wrappedHandler)
return uploadingTransport.upload(operation: operation,
files: files,
callbackQueue: queue) { result in
resultHandler?(result)
}
}

@discardableResult
public func subscribe<Subscription: GraphQLSubscription>(subscription: Subscription,
queue: DispatchQueue = .main,
resultHandler: @escaping GraphQLResultHandler<Subscription.Data>) -> Cancellable {
return self.send(operation: subscription,
shouldPublishResultToStore: true,
context: nil,
resultHandler: wrapResultHandler(resultHandler, queue: queue))
return self.networkTransport.send(operation: subscription,
cachePolicy: .default,
contextIdentifier: nil,
callbackQueue: queue,
completionHandler: resultHandler)
}
}

private func wrapResultHandler<Data>(_ resultHandler: GraphQLResultHandler<Data>?, queue handlerQueue: DispatchQueue) -> GraphQLResultHandler<Data> {
guard let resultHandler = resultHandler else {
return { _ in }
}

return { result in
handlerQueue.async {
resultHandler(result)
}
}
}

private final class FetchQueryOperation<Query: GraphQLQuery>: AsynchronousOperation, Cancellable {
weak var client: ApolloClient?
let query: Query
let cachePolicy: CachePolicy
let context: UnsafeMutableRawPointer?
let resultHandler: GraphQLResultHandler<Query.Data>

private var networkTask: Cancellable?

init(client: ApolloClient,
query: Query,
cachePolicy: CachePolicy,
context: UnsafeMutableRawPointer?,
resultHandler: @escaping GraphQLResultHandler<Query.Data>) {
self.client = client
self.query = query
self.cachePolicy = cachePolicy
self.context = context
self.resultHandler = resultHandler
}

override public func start() {
if isCancelled {
state = .finished
return
}

state = .executing

if cachePolicy == .fetchIgnoringCacheData {
fetchFromNetwork()
return
}

client?.store.load(query: query) { [weak self] result in
guard let self = self else {
return
}
if self.isCancelled {
self.state = .finished
return
}

switch result {
case .success:
self.resultHandler(result)

if self.cachePolicy != .returnCacheDataAndFetch {
self.state = .finished
return
}
case .failure:
if self.cachePolicy == .returnCacheDataDontFetch {
self.resultHandler(result)
self.state = .finished
return
}
}

self.fetchFromNetwork()
}
}

func fetchFromNetwork() {
networkTask = client?.send(operation: query,
shouldPublishResultToStore: true,
context: context) { [weak self] result in
guard let self = self else {
return
}
self.resultHandler(result)
self.state = .finished
return
}
}

override public func cancel() {
super.cancel()
networkTask?.cancel()
}
}
14 changes: 3 additions & 11 deletions Sources/Apollo/ApolloClientProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,55 +22,47 @@ public protocol ApolloClientProtocol: class {
/// - Parameters:
/// - query: The query to fetch.
/// - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache.
/// - context: [optional] A context to use for the cache to work with results. Should default to nil.
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
/// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`.
/// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs.
/// - Returns: An object that can be used to cancel an in progress fetch.
func fetch<Query: GraphQLQuery>(query: Query,
cachePolicy: CachePolicy,
context: UnsafeMutableRawPointer?,
contextIdentifier: UUID?,
queue: DispatchQueue,
resultHandler: GraphQLResultHandler<Query.Data>?) -> Cancellable

/// Watches a query by first fetching an initial result from the server or from the local cache, depending on the current contents of the cache and the specified cache policy. After the initial fetch, the returned query watcher object will get notified whenever any of the data the query result depends on changes in the local cache, and calls the result handler again with the new result.
///
/// - Parameters:
/// - query: The query to fetch.
/// - fetchHTTPMethod: The HTTP Method to be used.
/// - cachePolicy: A cache policy that specifies when results should be fetched from the server or from the local cache.
/// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue.
/// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs.
/// - Returns: A query watcher object that can be used to control the watching behavior.
func watch<Query: GraphQLQuery>(query: Query,
cachePolicy: CachePolicy,
queue: DispatchQueue,
resultHandler: @escaping GraphQLResultHandler<Query.Data>) -> GraphQLQueryWatcher<Query>

/// Performs a mutation by sending it to the server.
///
/// - Parameters:
/// - mutation: The mutation to perform.
/// - context: [optional] A context to use for the cache to work with results. Should default to nil.
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
/// - resultHandler: An optional closure that is called when mutation results are available or when an error occurs.
/// - Returns: An object that can be used to cancel an in progress mutation.
func perform<Mutation: GraphQLMutation>(mutation: Mutation,
context: UnsafeMutableRawPointer?,
queue: DispatchQueue,
resultHandler: GraphQLResultHandler<Mutation.Data>?) -> Cancellable

/// Uploads the given files with the given operation.
///
/// - Parameters:
/// - operation: The operation to send
/// - context: [optional] A context to use for the cache to work with results. Should default to nil.
/// - files: An array of `GraphQLFile` objects to send.
/// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue.
/// - completionHandler: The completion handler to execute when the request completes or errors
/// - completionHandler: The completion handler to execute when the request completes or errors. Note that an error will be returned If your `networkTransport` does not also conform to `UploadingNetworkTransport`.
/// - Returns: An object that can be used to cancel an in progress request.
/// - Throws: If your `networkTransport` does not also conform to `UploadingNetworkTransport`.
func upload<Operation: GraphQLOperation>(operation: Operation,
context: UnsafeMutableRawPointer?,
files: [GraphQLFile],
queue: DispatchQueue,
resultHandler: GraphQLResultHandler<Operation.Data>?) -> Cancellable
Expand Down
20 changes: 20 additions & 0 deletions Sources/Apollo/ApolloErrorInterceptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

/// An error interceptor called to allow further examination of error data when an error occurs in the chain.
public protocol ApolloErrorInterceptor {

/// Asynchronously handles the receipt of an error at any point in the chain.
///
/// - Parameters:
/// - error: The received error
/// - chain: The chain the error was received on
/// - request: The request, as far as it was constructed
/// - response: [optional] The response, if one was received
/// - completion: The completion closure to fire when the operation has completed. Note that if you call `retry` on the chain, you will not want to call the completion block in this method.
func handleErrorAsync<Operation: GraphQLOperation>(
error: Error,
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void)
}
Loading

0 comments on commit 713430d

Please sign in to comment.