Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make cachePolicy a var on HTTPRequest #1569

Merged
merged 3 commits into from
Dec 16, 2020
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
6 changes: 5 additions & 1 deletion Apollo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
9BAEEC15234C132600808306 /* CLIExtractorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC14234C132600808306 /* CLIExtractorTests.swift */; };
9BAEEC17234C275600808306 /* ApolloSchemaTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC16234C275600808306 /* ApolloSchemaTests.swift */; };
9BAEEC19234C297800808306 /* ApolloCodegenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */; };
9BB4F5B22581AA50004F0BD6 /* CacheDependentInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BB4F5B12581AA50004F0BD6 /* CacheDependentInterceptorTests.swift */; };
9BC139A424EDCA6C00876D29 /* InterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A224EDCA4400876D29 /* InterceptorTests.swift */; };
9BC139A624EDCAD900876D29 /* BlindRetryingTestInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */; };
9BC139A824EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */; };
Expand Down Expand Up @@ -664,6 +665,7 @@
9BAEEC16234C275600808306 /* ApolloSchemaTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloSchemaTests.swift; sourceTree = "<group>"; };
9BAEEC18234C297800808306 /* ApolloCodegenTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloCodegenTests.swift; sourceTree = "<group>"; };
9BB1DAC624A66B2500396235 /* ApolloMacPlayground.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = ApolloMacPlayground.playground; path = Playgrounds/ApolloMacPlayground.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
9BB4F5B12581AA50004F0BD6 /* CacheDependentInterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheDependentInterceptorTests.swift; sourceTree = "<group>"; };
9BC139A224EDCA4400876D29 /* InterceptorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterceptorTests.swift; sourceTree = "<group>"; };
9BC139A524EDCAD900876D29 /* BlindRetryingTestInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindRetryingTestInterceptor.swift; sourceTree = "<group>"; };
9BC139A724EDCE4F00876D29 /* RetryToCountThenSucceedInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryToCountThenSucceedInterceptor.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1496,15 +1498,16 @@
9FA6ABBD1EC0A988000017BE /* ApolloCacheDependentTests */ = {
isa = PBXGroup;
children = (
9BB4F5B12581AA50004F0BD6 /* CacheDependentInterceptorTests.swift */,
9FA6ABC51EC0A9F7000017BE /* FetchQueryTests.swift */,
9FA6ABC81EC0A9F7000017BE /* StarWarsServerCachingRoundtripTests.swift */,
9FA6ABC91EC0A9F7000017BE /* StarWarsServerTests.swift */,
9FA6ABC61EC0A9F7000017BE /* LoadQueryFromStoreTests.swift */,
9F8622F71EC2004200C38162 /* ReadWriteFromStoreTests.swift */,
9B60204E23FDFA9F00D0C8E0 /* SQLiteCacheTests.swift */,
9FD03C2D25527CE6002227DC /* StoreConcurrencyTests.swift */,
9FA6ABCB1EC0A9F7000017BE /* WatchQueryTests.swift */,
9FA6ABC01EC0A988000017BE /* Info.plist */,
9B60204E23FDFA9F00D0C8E0 /* SQLiteCacheTests.swift */,
);
path = ApolloCacheDependentTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -2552,6 +2555,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
9BB4F5B22581AA50004F0BD6 /* CacheDependentInterceptorTests.swift in Sources */,
9FD03C2E25527CE7002227DC /* StoreConcurrencyTests.swift in Sources */,
9F39101725493DDC00AF54A6 /* FetchQueryTests.swift in Sources */,
9FA6ABCD1EC0A9F7000017BE /* LoadQueryFromStoreTests.swift in Sources */,
Expand Down
2 changes: 1 addition & 1 deletion Sources/Apollo/HTTPRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ open class HTTPRequest<Operation: GraphQLOperation> {
open var additionalHeaders: [String: String]

/// The `CachePolicy` to use for this request.
public let cachePolicy: CachePolicy
open var cachePolicy: CachePolicy

/// [optional] A unique identifier for this request, to help with deduping cache hits for watchers.
public let contextIdentifier: UUID?
Expand Down
127 changes: 127 additions & 0 deletions Tests/ApolloCacheDependentTests/CacheDependentInterceptorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//
// CacheDependentInterceptorTests.swift
// ApolloCacheDependentTests
//
// Created by Ellen Shapiro on 12/9/20.
// Copyright © 2020 Apollo GraphQL. All rights reserved.
//

import XCTest
import Apollo
import ApolloTestSupport
import StarWarsAPI

class CacheDependentInterceptorTests: XCTestCase, CacheDependentTesting {
var cacheType: TestCacheProvider.Type {
InMemoryTestCacheProvider.self
}
var defaultWaitTimeout: TimeInterval = 5


var cache: NormalizedCache!
var store: ApolloStore!
var client: ApolloClient!

override func setUpWithError() throws {
try super.setUpWithError()

cache = try makeNormalizedCache()
store = ApolloStore(cache: cache)
}

func testChangingCachePolicyInErrorInterceptorWorks() {
// Set up initial cache state
mergeRecordsIntoCache([
"QUERY_ROOT": ["hero": Reference(key: "hero")],
"hero": ["__typename": "Droid", "name": "R2-D2"]
])

/// This interceptor will reroute anything that fails with a response code error to retry hitting only the cache
class RerouteToCacheErrorInterceptor: ApolloErrorInterceptor {
var handledError: Error?

func handleErrorAsync<Operation: GraphQLOperation>(
error: Error,
chain: RequestChain,
request: HTTPRequest<Operation>,
response: HTTPResponse<Operation>?,
completion: @escaping (Result<GraphQLResult<Operation.Data>, Error>) -> Void) {

self.handledError = error

switch error {
case ResponseCodeInterceptor.ResponseCodeError.invalidResponseCode:
request.cachePolicy = .returnCacheDataDontFetch
chain.retry(request: request, completion: completion)
default:
completion(.failure(error))
}
}
}


class TestProvider: LegacyInterceptorProvider {
init(store: ApolloStore) {
super.init(client: self.mockClient,
store: store)
}

let mockClient: MockURLSessionClient = {
let client = MockURLSessionClient()
client.response = HTTPURLResponse(url: TestURL.mockServer.url,
statusCode: 401,
httpVersion: nil,
headerFields: nil)
client.data = Data()
return client
}()

let additionalInterceptor = RerouteToCacheErrorInterceptor()

override func additionalErrorInterceptor<Operation: GraphQLOperation>(for operation: Operation) -> ApolloErrorInterceptor? {
self.additionalInterceptor
}
}

let testProvider = TestProvider(store: self.store)
let network = RequestChainNetworkTransport(interceptorProvider: testProvider,
endpointURL: TestURL.mockServer.url)

let expectation = self.expectation(description: "Request sent")

// Send the initial request ignoring cache data so it doesn't initially get the data from the cache,
_ = network.send(operation: HeroNameQuery(), cachePolicy: .fetchIgnoringCacheData) { result in
defer {
expectation.fulfill()
}

// Check that the final result is what we expected
switch result {
case .failure(let error):
XCTFail("Unexpected error: \(error)")
case .success(let graphQLResult):
guard let heroName = graphQLResult.data?.hero?.name else {
XCTFail("Could not access hero name from returned result")
return
}

XCTAssertEqual(heroName, "R2-D2")
}

// Validate that there was a handled error before we went to the cache and we didn't just go straight to the cache
guard let handledError = testProvider.additionalInterceptor.handledError else {
XCTFail("No error was handled!")
return
}
switch handledError {
case ResponseCodeInterceptor.ResponseCodeError.invalidResponseCode(let response, _):
XCTAssertEqual(response?.statusCode, 401)
default:
XCTFail("Unexpected error on the additional error handler: \(handledError)")
}
}

self.wait(for: [expectation], timeout: self.defaultWaitTimeout)
}
}

2 changes: 2 additions & 0 deletions docs/source/caching.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ Other cache polices which you can specify are:
- **`.returnCacheDataDontFetch`** to return data from the cache and never fetch from the server. This policy will return an error if cached data is not available.
- **`.returnCacheDataAndFetch`** to return cached data immediately, *then* perform a fetch to see if there are any updates. This is mostly useful if you're watching queries, since those will be updated when the call to the server returns.

If you're interested in returning cached data after a failed fetch, the current recommended approach is to use an `additionalErrorInterceptor` on your interceptor chain to examine if the error is one it makes sense to show old data for rather than something that needs to be passed on to the user, and then retrying with a `returnCacheDataDontFetch` retry policy. An example of this setup can be found in the [Cache-dependent interceptor tests](https://github.com/apollographql/apollo-ios/blob/main/Tests/ApolloCacheDependentTests/CacheDependentInterceptorTests.swift).

## Watching queries

Watching a query is very similar to fetching a query. The main difference is that you don't just receive an initial result, but your result handler will be invoked whenever relevant data in the cache changes:
Expand Down