Skip to content

Commit 713430d

Browse files
Merge pull request #1341 from apollographql/explore/interceptors
Network Rearchitecture
2 parents a764aa0 + 38135ee commit 713430d

64 files changed

Lines changed: 2807 additions & 1597 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Apollo.xcodeproj/project.pbxproj

Lines changed: 144 additions & 15 deletions
Large diffs are not rendered by default.

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ let package = Package(
103103
.testTarget(
104104
name: "ApolloCodegenTests",
105105
dependencies: [
106+
"ApolloTestSupport",
106107
"ApolloCodegenLib"
107108
]),
108109
.testTarget(

Sources/Apollo/ApolloClient.swift

Lines changed: 41 additions & 189 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ public enum CachePolicy {
1313
case returnCacheDataDontFetch
1414
/// Return data from the cache if available, and always fetch results from the server.
1515
case returnCacheDataAndFetch
16+
17+
/// The current default cache policy.
18+
public static var `default`: CachePolicy {
19+
.returnCacheDataElseFetch
20+
}
1621
}
1722

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

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

31-
private let queue: DispatchQueue
32-
private let operationQueue: OperationQueue
33-
3436
public enum ApolloClientError: Error, LocalizedError {
3537
case noUploadTransport
3638

@@ -50,69 +52,18 @@ public class ApolloClient {
5052
public init(networkTransport: NetworkTransport, store: ApolloStore = ApolloStore(cache: InMemoryNormalizedCache())) {
5153
self.networkTransport = networkTransport
5254
self.store = store
53-
54-
queue = DispatchQueue(label: "com.apollographql.ApolloClient")
55-
operationQueue = OperationQueue()
56-
operationQueue.underlyingQueue = queue
5755
}
5856

5957
/// Creates a client with an HTTP network transport connecting to the specified URL.
6058
///
6159
/// - Parameter url: The URL of a GraphQL server to connect to.
6260
public convenience init(url: URL) {
63-
self.init(networkTransport: HTTPNetworkTransport(url: url))
64-
}
65-
66-
fileprivate func send<Operation: GraphQLOperation>(operation: Operation,
67-
shouldPublishResultToStore: Bool,
68-
context: UnsafeMutableRawPointer?,
69-
resultHandler: @escaping GraphQLResultHandler<Operation.Data>) -> Cancellable {
70-
return networkTransport.send(operation: operation) { [weak self] result in
71-
guard let self = self else {
72-
return
73-
}
74-
self.handleOperationResult(shouldPublishResultToStore: shouldPublishResultToStore,
75-
context: context,
76-
result,
77-
resultHandler: resultHandler)
78-
}
79-
}
80-
81-
private func handleOperationResult<Data: GraphQLSelectionSet>(shouldPublishResultToStore: Bool,
82-
context: UnsafeMutableRawPointer?,
83-
_ result: Result<GraphQLResponse<Data>, Error>,
84-
resultHandler: @escaping GraphQLResultHandler<Data>) {
85-
switch result {
86-
case .failure(let error):
87-
resultHandler(.failure(error))
88-
case .success(let response):
89-
// If there is no need to publish the result to the store, we can use a fast path.
90-
if !shouldPublishResultToStore {
91-
do {
92-
let result = try response.parseResultFast()
93-
resultHandler(.success(result))
94-
} catch {
95-
resultHandler(.failure(error))
96-
}
97-
return
98-
}
99-
100-
firstly {
101-
try response.parseResult(cacheKeyForObject: self.cacheKeyForObject)
102-
}.andThen { [weak self] (result, records) in
103-
guard let self = self else {
104-
return
105-
}
106-
if let records = records {
107-
self.store.publish(records: records, context: context).catch { error in
108-
preconditionFailure(String(describing: error))
109-
}
110-
}
111-
resultHandler(.success(result))
112-
}.catch { error in
113-
resultHandler(.failure(error))
114-
}
115-
}
61+
let store = ApolloStore(cache: InMemoryNormalizedCache())
62+
let provider = LegacyInterceptorProvider(store: store)
63+
let transport = RequestChainNetworkTransport(interceptorProvider: provider,
64+
endpointURL: url)
65+
66+
self.init(networkTransport: transport, store: store)
11667
}
11768
}
11869

@@ -124,180 +75,81 @@ extension ApolloClient: ApolloClientProtocol {
12475
get {
12576
return self.store.cacheKeyForObject
12677
}
127-
12878
set {
12979
self.store.cacheKeyForObject = newValue
13080
}
13181
}
13282

133-
public func clearCache(callbackQueue: DispatchQueue = .main, completion: ((Result<Void, Error>) -> Void)? = nil) {
83+
public func clearCache(callbackQueue: DispatchQueue = .main,
84+
completion: ((Result<Void, Error>) -> Void)? = nil) {
13485
self.store.clearCache(completion: completion)
13586
}
136-
87+
13788
@discardableResult public func fetch<Query: GraphQLQuery>(query: Query,
13889
cachePolicy: CachePolicy = .returnCacheDataElseFetch,
139-
context: UnsafeMutableRawPointer? = nil,
90+
contextIdentifier: UUID? = nil,
14091
queue: DispatchQueue = DispatchQueue.main,
14192
resultHandler: GraphQLResultHandler<Query.Data>? = nil) -> Cancellable {
142-
let resultHandler = wrapResultHandler(resultHandler, queue: queue)
143-
144-
// If we don't have to go through the cache, there is no need to create an operation
145-
// and we can return a network task directly
146-
if cachePolicy == .fetchIgnoringCacheData || cachePolicy == .fetchIgnoringCacheCompletely {
147-
return self.send(operation: query, shouldPublishResultToStore: cachePolicy != .fetchIgnoringCacheCompletely, context: context, resultHandler: resultHandler)
148-
} else {
149-
let operation = FetchQueryOperation(client: self, query: query, cachePolicy: cachePolicy, context: context, resultHandler: resultHandler)
150-
self.operationQueue.addOperation(operation)
151-
return operation
93+
return self.networkTransport.send(operation: query,
94+
cachePolicy: cachePolicy,
95+
contextIdentifier: contextIdentifier,
96+
callbackQueue: queue) { result in
97+
resultHandler?(result)
15298
}
15399
}
154100

155101
public func watch<Query: GraphQLQuery>(query: Query,
156102
cachePolicy: CachePolicy = .returnCacheDataElseFetch,
157-
queue: DispatchQueue = .main,
158103
resultHandler: @escaping GraphQLResultHandler<Query.Data>) -> GraphQLQueryWatcher<Query> {
159104
let watcher = GraphQLQueryWatcher(client: self,
160105
query: query,
161-
resultHandler: wrapResultHandler(resultHandler, queue: queue))
106+
resultHandler: resultHandler)
162107
watcher.fetch(cachePolicy: cachePolicy)
163108
return watcher
164109
}
165110

166111
@discardableResult
167112
public func perform<Mutation: GraphQLMutation>(mutation: Mutation,
168-
context: UnsafeMutableRawPointer? = nil,
169-
queue: DispatchQueue = DispatchQueue.main,
113+
queue: DispatchQueue = .main,
170114
resultHandler: GraphQLResultHandler<Mutation.Data>? = nil) -> Cancellable {
171-
return self.send(operation: mutation,
172-
shouldPublishResultToStore: true,
173-
context: context,
174-
resultHandler: wrapResultHandler(resultHandler, queue: queue))
115+
return self.networkTransport.send(operation: mutation,
116+
cachePolicy: .default,
117+
contextIdentifier: nil,
118+
callbackQueue: queue) { result in
119+
resultHandler?(result)
120+
}
175121
}
176122

177123
@discardableResult
178124
public func upload<Operation: GraphQLOperation>(operation: Operation,
179-
context: UnsafeMutableRawPointer? = nil,
180125
files: [GraphQLFile],
181126
queue: DispatchQueue = .main,
182127
resultHandler: GraphQLResultHandler<Operation.Data>? = nil) -> Cancellable {
183-
let wrappedHandler = wrapResultHandler(resultHandler, queue: queue)
184128
guard let uploadingTransport = self.networkTransport as? UploadingNetworkTransport else {
185129
assertionFailure("Trying to upload without an uploading transport. Please make sure your network transport conforms to `UploadingNetworkTransport`.")
186-
wrappedHandler(.failure(ApolloClientError.noUploadTransport))
130+
queue.async {
131+
resultHandler?(.failure(ApolloClientError.noUploadTransport))
132+
}
187133
return EmptyCancellable()
188134
}
189135

190-
return uploadingTransport.upload(operation: operation, files: files) { [weak self] result in
191-
guard let self = self else {
192-
return
193-
}
194-
self.handleOperationResult(shouldPublishResultToStore: true,
195-
context: context, result,
196-
resultHandler: wrappedHandler)
136+
return uploadingTransport.upload(operation: operation,
137+
files: files,
138+
callbackQueue: queue) { result in
139+
resultHandler?(result)
197140
}
198141
}
199-
142+
200143
@discardableResult
201144
public func subscribe<Subscription: GraphQLSubscription>(subscription: Subscription,
202145
queue: DispatchQueue = .main,
203146
resultHandler: @escaping GraphQLResultHandler<Subscription.Data>) -> Cancellable {
204-
return self.send(operation: subscription,
205-
shouldPublishResultToStore: true,
206-
context: nil,
207-
resultHandler: wrapResultHandler(resultHandler, queue: queue))
147+
return self.networkTransport.send(operation: subscription,
148+
cachePolicy: .default,
149+
contextIdentifier: nil,
150+
callbackQueue: queue,
151+
completionHandler: resultHandler)
208152
}
209153
}
210154

211-
private func wrapResultHandler<Data>(_ resultHandler: GraphQLResultHandler<Data>?, queue handlerQueue: DispatchQueue) -> GraphQLResultHandler<Data> {
212-
guard let resultHandler = resultHandler else {
213-
return { _ in }
214-
}
215-
216-
return { result in
217-
handlerQueue.async {
218-
resultHandler(result)
219-
}
220-
}
221-
}
222155

223-
private final class FetchQueryOperation<Query: GraphQLQuery>: AsynchronousOperation, Cancellable {
224-
weak var client: ApolloClient?
225-
let query: Query
226-
let cachePolicy: CachePolicy
227-
let context: UnsafeMutableRawPointer?
228-
let resultHandler: GraphQLResultHandler<Query.Data>
229-
230-
private var networkTask: Cancellable?
231-
232-
init(client: ApolloClient,
233-
query: Query,
234-
cachePolicy: CachePolicy,
235-
context: UnsafeMutableRawPointer?,
236-
resultHandler: @escaping GraphQLResultHandler<Query.Data>) {
237-
self.client = client
238-
self.query = query
239-
self.cachePolicy = cachePolicy
240-
self.context = context
241-
self.resultHandler = resultHandler
242-
}
243-
244-
override public func start() {
245-
if isCancelled {
246-
state = .finished
247-
return
248-
}
249-
250-
state = .executing
251-
252-
if cachePolicy == .fetchIgnoringCacheData {
253-
fetchFromNetwork()
254-
return
255-
}
256-
257-
client?.store.load(query: query) { [weak self] result in
258-
guard let self = self else {
259-
return
260-
}
261-
if self.isCancelled {
262-
self.state = .finished
263-
return
264-
}
265-
266-
switch result {
267-
case .success:
268-
self.resultHandler(result)
269-
270-
if self.cachePolicy != .returnCacheDataAndFetch {
271-
self.state = .finished
272-
return
273-
}
274-
case .failure:
275-
if self.cachePolicy == .returnCacheDataDontFetch {
276-
self.resultHandler(result)
277-
self.state = .finished
278-
return
279-
}
280-
}
281-
282-
self.fetchFromNetwork()
283-
}
284-
}
285-
286-
func fetchFromNetwork() {
287-
networkTask = client?.send(operation: query,
288-
shouldPublishResultToStore: true,
289-
context: context) { [weak self] result in
290-
guard let self = self else {
291-
return
292-
}
293-
self.resultHandler(result)
294-
self.state = .finished
295-
return
296-
}
297-
}
298-
299-
override public func cancel() {
300-
super.cancel()
301-
networkTask?.cancel()
302-
}
303-
}

Sources/Apollo/ApolloClientProtocol.swift

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,55 +22,47 @@ public protocol ApolloClientProtocol: class {
2222
/// - Parameters:
2323
/// - query: The query to fetch.
2424
/// - cachePolicy: A cache policy that specifies when results should be fetched from the server and when data should be loaded from the local cache.
25-
/// - context: [optional] A context to use for the cache to work with results. Should default to nil.
2625
/// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue.
26+
/// - contextIdentifier: [optional] A unique identifier for this request, to help with deduping cache hits for watchers. Should default to `nil`.
2727
/// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs.
2828
/// - Returns: An object that can be used to cancel an in progress fetch.
2929
func fetch<Query: GraphQLQuery>(query: Query,
3030
cachePolicy: CachePolicy,
31-
context: UnsafeMutableRawPointer?,
31+
contextIdentifier: UUID?,
3232
queue: DispatchQueue,
3333
resultHandler: GraphQLResultHandler<Query.Data>?) -> Cancellable
3434

3535
/// 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.
3636
///
3737
/// - Parameters:
3838
/// - query: The query to fetch.
39-
/// - fetchHTTPMethod: The HTTP Method to be used.
4039
/// - cachePolicy: A cache policy that specifies when results should be fetched from the server or from the local cache.
41-
/// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue.
4240
/// - resultHandler: [optional] A closure that is called when query results are available or when an error occurs.
4341
/// - Returns: A query watcher object that can be used to control the watching behavior.
4442
func watch<Query: GraphQLQuery>(query: Query,
4543
cachePolicy: CachePolicy,
46-
queue: DispatchQueue,
4744
resultHandler: @escaping GraphQLResultHandler<Query.Data>) -> GraphQLQueryWatcher<Query>
4845

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

6257
/// Uploads the given files with the given operation.
6358
///
6459
/// - Parameters:
6560
/// - operation: The operation to send
66-
/// - context: [optional] A context to use for the cache to work with results. Should default to nil.
6761
/// - files: An array of `GraphQLFile` objects to send.
6862
/// - queue: A dispatch queue on which the result handler will be called. Should default to the main queue.
69-
/// - completionHandler: The completion handler to execute when the request completes or errors
63+
/// - 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`.
7064
/// - Returns: An object that can be used to cancel an in progress request.
71-
/// - Throws: If your `networkTransport` does not also conform to `UploadingNetworkTransport`.
7265
func upload<Operation: GraphQLOperation>(operation: Operation,
73-
context: UnsafeMutableRawPointer?,
7466
files: [GraphQLFile],
7567
queue: DispatchQueue,
7668
resultHandler: GraphQLResultHandler<Operation.Data>?) -> Cancellable
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import Foundation
2+
3+
/// An error interceptor called to allow further examination of error data when an error occurs in the chain.
4+
public protocol ApolloErrorInterceptor {
5+
6+
/// Asynchronously handles the receipt of an error at any point in the chain.
7+
///
8+
/// - Parameters:
9+
/// - error: The received error
10+
/// - chain: The chain the error was received on
11+
/// - request: The request, as far as it was constructed
12+
/// - response: [optional] The response, if one was received
13+
/// - 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.
14+
func handleErrorAsync<Operation: GraphQLOperation>(
15+
error: Error,
16+
chain: RequestChain,
17+
request: HTTPRequest<Operation>,
18+
response: HTTPResponse<Operation>?,
19+
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void)
20+
}

0 commit comments

Comments
 (0)