Skip to content

Commit

Permalink
Networking Cleanup (#1847)
Browse files Browse the repository at this point in the history
* Cleanup

* Move LegacyInterceptorProvider to its own file

* Inject WebSocket into WebSocketTransport

* Rename LegacyCacheInterceptors to remove “Legacy”

* Kill FlexibleDecoder and Parseable

* Cleanup and rename LegacyParsingInterceptor to JSONResponseParsingInterceptor

* Make unneeded classes into structs

* Rename LegacyInterceptorProvider -> DefaultInterceptorProvider

* Fixed a few more instances of “Legacy”

* Fixed Integration Tests

* Fixed more references to renamed “legacy” interceptors

* minor fix
  • Loading branch information
AnthonyMDev authored Jun 24, 2021
1 parent fceb28b commit 57c07f1
Show file tree
Hide file tree
Showing 38 changed files with 294 additions and 405 deletions.
70 changes: 31 additions & 39 deletions Apollo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ let store = ApolloStore(cache: sqliteCache)

//: Next, you'll need to set up a network transport, since you will also need that to set up the client:
let serverURL = URL(string: "http://localhost:8080/graphql")!
let networkTransport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(store: store), endpointURL: serverURL)
let networkTransport = RequestChainNetworkTransport(interceptorProvider: DefaultInterceptorProvider(store: store), endpointURL: serverURL)

//: Finally, pass that into your `ApolloClient` initializer, and you're now set up to use the SQLite cache for persistent storage:
let apolloClient = ApolloClient(networkTransport: networkTransport, store: store)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ In this instance, we'll use a `SplitNetworkTransport` since we want to demonstra

let url = URL(string: "http://localhost:8080/graphql")!
let store = ApolloStore()
let normalTransport = RequestChainNetworkTransport(interceptorProvider: LegacyInterceptorProvider(store: store), endpointURL: url)
let normalTransport = RequestChainNetworkTransport(interceptorProvider: DefaultInterceptorProvider(store: store), endpointURL: url)

//: Next, set up the `WebSocketTransport` to talk to the websocket endpoint. Note that this may take a different URL, sometimes with a `ws` prefix, than your normal http endpoint:

Expand Down
2 changes: 1 addition & 1 deletion Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public class ApolloClient {
/// - Parameter url: The URL of a GraphQL server to connect to.
public convenience init(url: URL) {
let store = ApolloStore(cache: InMemoryNormalizedCache())
let provider = LegacyInterceptorProvider(store: store)
let provider = DefaultInterceptorProvider(store: store)
let transport = RequestChainNetworkTransport(interceptorProvider: provider,
endpointURL: url)

Expand Down
2 changes: 1 addition & 1 deletion Sources/Apollo/ApolloInterceptor.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/// A protocol to set up a chainable unit of networking work.
public protocol ApolloInterceptor: AnyObject {
public protocol ApolloInterceptor {

/// Called when this interceptor should do its work.
///
Expand Down
2 changes: 1 addition & 1 deletion Sources/Apollo/AutomaticPersistedQueryInterceptor.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

public class AutomaticPersistedQueryInterceptor: ApolloInterceptor {
public struct AutomaticPersistedQueryInterceptor: ApolloInterceptor {

public enum APQError: LocalizedError {
case noParsedResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Foundation

/// An interceptor that reads data from the legacy cache for queries, following the `HTTPRequest`'s `cachePolicy`.
public class LegacyCacheReadInterceptor: ApolloInterceptor {
/// An interceptor that reads data from the cache for queries, following the `HTTPRequest`'s `cachePolicy`.
public struct CacheReadInterceptor: ApolloInterceptor {

private let store: ApolloStore

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import Foundation

/// An interceptor which writes data to the legacy cache, following the `HTTPRequest`'s `cachePolicy`.
public class LegacyCacheWriteInterceptor: ApolloInterceptor {
/// An interceptor which writes data to the cache, following the `HTTPRequest`'s `cachePolicy`.
public struct CacheWriteInterceptor: ApolloInterceptor {

public enum LegacyCacheWriteError: Error, LocalizedError {
public enum CacheWriteError: Error, LocalizedError {
case noResponseToParse

public var errorDescription: String? {
switch self {
case .noResponseToParse:
return "The Legacy Cache Write Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors."
return "The Cache Write Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors."
}
}
}
Expand Down Expand Up @@ -40,7 +40,7 @@ public class LegacyCacheWriteInterceptor: ApolloInterceptor {
guard
let createdResponse = response,
let legacyResponse = createdResponse.legacyResponse else {
chain.handleErrorAsync(LegacyCacheWriteError.noResponseToParse,
chain.handleErrorAsync(CacheWriteError.noResponseToParse,
request: request,
response: response,
completion: completion)
Expand Down
45 changes: 45 additions & 0 deletions Sources/Apollo/DefaultInterceptorProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation

/// The default interceptor provider for typescript-generated code
open class DefaultInterceptorProvider: InterceptorProvider {

private let client: URLSessionClient
private let store: ApolloStore
private let shouldInvalidateClientOnDeinit: Bool

/// Designated initializer
///
/// - Parameters:
/// - client: The `URLSessionClient` to use. Defaults to the default setup.
/// - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances.
/// - store: The `ApolloStore` to use when reading from or writing to the cache. Make sure you pass the same store to the `ApolloClient` instance you're planning to use.
public init(client: URLSessionClient = URLSessionClient(),
shouldInvalidateClientOnDeinit: Bool = true,
store: ApolloStore) {
self.client = client
self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit
self.store = store
}

deinit {
if self.shouldInvalidateClientOnDeinit {
self.client.invalidate()
}
}

open func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
return [
MaxRetryInterceptor(),
CacheReadInterceptor(store: self.store),
NetworkFetchInterceptor(client: self.client),
ResponseCodeInterceptor(),
JSONResponseParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
AutomaticPersistedQueryInterceptor(),
CacheWriteInterceptor(store: self.store),
]
}

open func additionalErrorInterceptor<Operation: GraphQLOperation>(for operation: Operation) -> ApolloErrorInterceptor? {
return nil
}
}
17 changes: 0 additions & 17 deletions Sources/Apollo/FlexibleDecoder.swift

This file was deleted.

19 changes: 2 additions & 17 deletions Sources/Apollo/GraphQLResponse.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import Foundation

/// Represents a GraphQL response received from a server.
public final class GraphQLResponse<Data: GraphQLSelectionSet>: Parseable {

public init<T>(from data: Foundation.Data, decoder: T) throws where T : FlexibleDecoder {
// Giant hack to make all this conform to Parseable.
throw ParseableError.unsupportedInitializer
}

public final class GraphQLResponse<Data: GraphQLSelectionSet> {

public let body: JSONObject

private var rootKey: String
Expand All @@ -23,16 +18,6 @@ public final class GraphQLResponse<Data: GraphQLSelectionSet>: Parseable {
self.rootKey = rootCacheKey(for: operation)
self.variables = operation.variables
}

public func parseResultWithCompletion(cacheKeyForObject: CacheKeyForObject? = nil,
completion: (Result<(GraphQLResult<Data>, RecordSet?), Error>) -> Void) {
do {
let result = try parseResult(cacheKeyForObject: cacheKeyForObject)
completion(.success(result))
} catch {
completion(.failure(error))
}
}

func parseResult(cacheKeyForObject: CacheKeyForObject? = nil) throws -> (GraphQLResult<Data>, RecordSet?) {
let errors: [GraphQLError]?
Expand Down
21 changes: 2 additions & 19 deletions Sources/Apollo/GraphQLResult.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import Foundation

/// Represents the result of a GraphQL operation.
public struct GraphQLResult<Data>: Parseable {

public init<T: FlexibleDecoder>(from data: Foundation.Data, decoder: T) throws {
throw ParseableError.unsupportedInitializer
}

public struct GraphQLResult<Data> {

/// The typed result data, or `nil` if an error was encountered that prevented a valid response.
public let data: Data?
/// A list of errors, or `nil` if the operation completed without encountering any errors.
Expand Down Expand Up @@ -36,16 +32,3 @@ public struct GraphQLResult<Data>: Parseable {
self.dependentKeys = dependentKeys
}
}

extension GraphQLResult where Data: Decodable {

public init<T: FlexibleDecoder>(from data: Foundation.Data, decoder: T) throws {
// SWIFT CODEGEN: fix this to handle codable better
let data = try decoder.decode(Data.self, from: data)
self.init(data: data,
extensions: nil,
errors: [],
source: .server,
dependentKeys: nil)
}
}
4 changes: 2 additions & 2 deletions Sources/Apollo/HTTPResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public class HTTPResponse<Operation: GraphQLOperation> {
/// [optional] The data as parsed into a `GraphQLResult`, which can eventually be returned to the UI. Will be nil if not yet parsed.
public var parsedResponse: GraphQLResult<Operation.Data>?

/// [optional] The data as parsed into a `GraphQLResponse` for legacy caching purposes. If you're not using the `LegacyParsingInterceptor`, you probably shouldn't be using this property.
/// **NOTE:** This property will be removed when the transition to a Codable-based Codegen is complete.
/// [optional] The data as parsed into a `GraphQLResponse` for legacy caching purposes. If you're not using the `JSONResponseParsingInterceptor`, you probably shouldn't be using this property.
/// **NOTE:** This property will be removed when the transition to the Swift Codegen is complete.
public var legacyResponse: GraphQLResponse<Operation.Data>? = nil

/// Designated initializer
Expand Down
46 changes: 0 additions & 46 deletions Sources/Apollo/InterceptorProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,3 @@ public extension InterceptorProvider {
return nil
}
}

// MARK: - Default implementation for typescript codegen

/// The default interceptor provider for typescript-generated code
open class LegacyInterceptorProvider: InterceptorProvider {

private let client: URLSessionClient
private let store: ApolloStore
private let shouldInvalidateClientOnDeinit: Bool

/// Designated initializer
///
/// - Parameters:
/// - client: The `URLSessionClient` to use. Defaults to the default setup.
/// - shouldInvalidateClientOnDeinit: If the passed-in client should be invalidated when this interceptor provider is deinitialized. If you are recreating the `URLSessionClient` every time you create a new provider, you should do this to prevent memory leaks. Defaults to true, since by default we provide a `URLSessionClient` to new instances.
/// - store: The `ApolloStore` to use when reading from or writing to the cache. Make sure you pass the same store to the `ApolloClient` instance you're planning to use.
public init(client: URLSessionClient = URLSessionClient(),
shouldInvalidateClientOnDeinit: Bool = true,
store: ApolloStore) {
self.client = client
self.shouldInvalidateClientOnDeinit = shouldInvalidateClientOnDeinit
self.store = store
}

deinit {
if self.shouldInvalidateClientOnDeinit {
self.client.invalidate()
}
}

open func interceptors<Operation: GraphQLOperation>(for operation: Operation) -> [ApolloInterceptor] {
return [
MaxRetryInterceptor(),
LegacyCacheReadInterceptor(store: self.store),
NetworkFetchInterceptor(client: self.client),
ResponseCodeInterceptor(),
LegacyParsingInterceptor(cacheKeyForObject: self.store.cacheKeyForObject),
AutomaticPersistedQueryInterceptor(),
LegacyCacheWriteInterceptor(store: self.store),
]
}

open func additionalErrorInterceptor<Operation: GraphQLOperation>(for operation: Operation) -> ApolloErrorInterceptor? {
return nil
}
}
88 changes: 88 additions & 0 deletions Sources/Apollo/JSONResponseParsingInterceptor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Foundation

/// An interceptor which parses JSON response data into a `GraphQLResult` and attaches it to the `HTTPResponse`.
public struct JSONResponseParsingInterceptor: ApolloInterceptor {

public enum JSONResponseParsingError: Error, LocalizedError {
case noResponseToParse
case couldNotParseToJSON(data: Data)

public var errorDescription: String? {
switch self {
case .noResponseToParse:
return "The Codable Parsing Interceptor was called before a response was received to be parsed. Double-check the order of your interceptors."
case .couldNotParseToJSON(let data):
var errorStrings = [String]()
errorStrings.append("Could not parse data to JSON format.")
if let dataString = String(bytes: data, encoding: .utf8) {
errorStrings.append("Data received as a String was:")
errorStrings.append(dataString)
} else {
errorStrings.append("Data of count \(data.count) also could not be parsed into a String.")
}

return errorStrings.joined(separator: " ")
}
}
}

public let cacheKeyForObject: CacheKeyForObject?

/// Designated Initializer
public init(cacheKeyForObject: CacheKeyForObject? = nil) {
self.cacheKeyForObject = cacheKeyForObject
}

public func interceptAsync<Operation: GraphQLOperation>(
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void
) {
guard let createdResponse = response else {
chain.handleErrorAsync(JSONResponseParsingError.noResponseToParse,
request: request,
response: response,
completion: completion)
return
}

do {
guard let body = try? JSONSerializationFormat
.deserialize(data: createdResponse.rawData) as? JSONObject else {
throw JSONResponseParsingError.couldNotParseToJSON(data: createdResponse.rawData)
}

let graphQLResponse = GraphQLResponse(operation: request.operation, body: body)
createdResponse.legacyResponse = graphQLResponse


let result = try parseResult(from: graphQLResponse, cachePolicy: request.cachePolicy)
createdResponse.parsedResponse = result
chain.proceedAsync(request: request,
response: createdResponse,
completion: completion)

} catch {
chain.handleErrorAsync(error,
request: request,
response: createdResponse,
completion: completion)
}
}

private func parseResult<Data>(
from response: GraphQLResponse<Data>,
cachePolicy: CachePolicy
) throws -> GraphQLResult<Data> {
switch cachePolicy {
case .fetchIgnoringCacheCompletely:
// There is no cache, so we don't need to get any info on dependencies. Use fast parsing.
return try response.parseResultFast()
default:
let (parsedResult, _) = try response.parseResult(cacheKeyForObject: self.cacheKeyForObject)
return parsedResult
}
}

}
Loading

0 comments on commit 57c07f1

Please sign in to comment.