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

Added ability to directly update the Apollo cache using write methods #413

Merged
merged 12 commits into from
Jan 27, 2019
Merged
2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "stephencelis/SQLite.swift" ~> 0.11.5
github "daltoniam/Starscream" ~> 3.0.5
github "daltoniam/Starscream" ~> 3.0.6
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "daltoniam/Starscream" "3.0.5"
github "daltoniam/Starscream" "3.0.6"
github "stephencelis/SQLite.swift" "0.11.5"
2 changes: 1 addition & 1 deletion Carthage/Checkouts/Starscream
Submodule Starscream updated 50 files
+93 −2 .gitignore
+0 −1 .swift-version
+7 −0 .travis.yml
+4 −0 Gemfile
+201 −0 Gemfile.lock
+8 −0 Package.resolved
+9 −7 Package.swift
+7 −1 README.md
+1 −1 Sources/Starscream/Compression.swift
+92 −0 Sources/Starscream/SSLClientCertificate.swift
+5 −5 Sources/Starscream/SSLSecurity.swift
+62 −48 Sources/Starscream/WebSocket.swift
+3 −8 Starscream.podspec
+31 −42 Starscream.xcodeproj/project.pbxproj
+3 −1 Tests/CompressionTests.swift
+6 −0 build.sh
+11 −0 examples/WebSocketsOrgEcho/Podfile
+16 −0 examples/WebSocketsOrgEcho/Podfile.lock
+24 −0 examples/WebSocketsOrgEcho/Pods/Local Podspecs/Starscream.podspec.json
+16 −0 examples/WebSocketsOrgEcho/Pods/Manifest.lock
+26 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-Info.plist
+182 −0 ...tsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-acknowledgements.markdown
+5 −0 ...cketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-acknowledgements.plist
+5 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-dummy.m
+158 −0 ...les/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-frameworks.sh
+16 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho-umbrella.h
+11 −0 ...es/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho.debug.xcconfig
+6 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho.modulemap
+11 −0 .../WebSocketsOrgEcho/Pods/Target Support Files/Pods-WebSocketsOrgEcho/Pods-WebSocketsOrgEcho.release.xcconfig
+26 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream-Info.plist
+5 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream-dummy.m
+12 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream-prefix.pch
+16 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream-umbrella.h
+6 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream.modulemap
+9 −0 examples/WebSocketsOrgEcho/Pods/Target Support Files/Starscream/Starscream.xcconfig
+10 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho.xcworkspace/contents.xcworkspacedata
+8 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
+21 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho/AppDelegate.swift
+98 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Assets.xcassets/AppIcon.appiconset/Contents.json
+6 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Assets.xcassets/Contents.json
+25 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Base.lproj/LaunchScreen.storyboard
+40 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Base.lproj/Main.storyboard
+45 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho/Info.plist
+19 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho/URL+Extensions.swift
+42 −0 examples/WebSocketsOrgEcho/WebSocketsOrgEcho/ViewController.swift
+28 −0 fastlane/Fastfile
+29 −0 fastlane/README.md
+4 −0 release.sh
+0 −2 zlib/include.h
+0 −9 zlib/module.modulemap
6 changes: 4 additions & 2 deletions Sources/Apollo/ApolloClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public typealias OperationResultHandler<Operation: GraphQLOperation> = (_ result
/// The `ApolloClient` class provides the core API for Apollo. This API provides methods to fetch and watch queries, and to perform mutations.
public class ApolloClient {
let networkTransport: NetworkTransport
let store: ApolloStore

public let store: ApolloStore

public var cacheKeyForObject: CacheKeyForObject? {
get {
return store.cacheKeyForObject
Expand All @@ -39,7 +41,7 @@ public class ApolloClient {
store.cacheKeyForObject = newValue
}
}

private let queue: DispatchQueue
private let operationQueue: OperationQueue

Expand Down
40 changes: 22 additions & 18 deletions Sources/Apollo/ApolloStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public final class ApolloStore {
}
}

func load<Query: GraphQLQuery>(query: Query) -> Promise<GraphQLResult<Query.Data>> {
public func load<Query: GraphQLQuery>(query: Query) -> Promise<GraphQLResult<Query.Data>> {
return withinReadTransaction { transaction in
let mapper = GraphQLSelectionSetMapper<Query.Data>()
let dependencyTracker = GraphQLDependencyTracker()
Expand All @@ -129,7 +129,7 @@ public final class ApolloStore {
}
}

func load<Query: GraphQLQuery>(query: Query, resultHandler: @escaping OperationResultHandler<Query>) {
public func load<Query: GraphQLQuery>(query: Query, resultHandler: @escaping OperationResultHandler<Query>) {
load(query: query).andThen { result in
resultHandler(result, nil)
}.catch { error in
Expand All @@ -143,17 +143,6 @@ public final class ApolloStore {

fileprivate lazy var loader: DataLoader<CacheKey, Record?> = DataLoader(self.cache.loadRecords)

fileprivate func makeExecutor() -> GraphQLExecutor {
let executor = GraphQLExecutor { object, info in
let value = object[info.cacheKeyForField]
return self.complete(value: value)
}

executor.dispatchDataLoads = self.loader.dispatch
executor.cacheKeyForObject = self.cacheKeyForObject
return executor
}

init(cache: NormalizedCache, cacheKeyForObject: CacheKeyForObject?) {
self.cache = cache
self.cacheKeyForObject = cacheKeyForObject
Expand Down Expand Up @@ -187,7 +176,16 @@ public final class ApolloStore {

final func execute<Accumulator: GraphQLResultAccumulator>(selections: [GraphQLSelection], onObjectWithKey key: CacheKey, variables: GraphQLMap?, accumulator: Accumulator) throws -> Promise<Accumulator.FinalResult> {
return loadObject(forKey: key).flatMap { object in
try self.makeExecutor().execute(selections: selections, on: object, withKey: key, variables: variables, accumulator: accumulator)
let executor = GraphQLExecutor { object, info in
let value = object[info.cacheKeyForField]
return self.complete(value: value)
}


executor.dispatchDataLoads = self.loader.dispatch
executor.cacheKeyForObject = self.cacheKeyForObject

return try executor.execute(selections: selections, on: object, withKey: key, variables: variables, accumulator: accumulator)
}
}

Expand All @@ -206,8 +204,8 @@ public final class ApolloStore {
fileprivate var updateChangedKeysFunc: DidChangeKeysFunc?

init(cache: NormalizedCache, cacheKeyForObject: CacheKeyForObject?, updateChangedKeysFunc: @escaping DidChangeKeysFunc) {
self.updateChangedKeysFunc = updateChangedKeysFunc
super.init(cache: cache, cacheKeyForObject: cacheKeyForObject)
self.updateChangedKeysFunc = updateChangedKeysFunc
super.init(cache: cache, cacheKeyForObject: cacheKeyForObject)
}

public func update<Query: GraphQLQuery>(query: Query, _ body: (inout Query.Data) throws -> Void) throws {
Expand All @@ -232,12 +230,18 @@ public final class ApolloStore {

private func write(object: JSONObject, forSelections selections: [GraphQLSelection], withKey key: CacheKey, variables: GraphQLMap?) throws {
let normalizer = GraphQLResultNormalizer()
try self.makeExecutor().execute(selections: selections, on: object, withKey: key, variables: variables, accumulator: normalizer)
let executor = GraphQLExecutor { object, info in
return .result(.success(object[info.responseKeyForField]))
}

executor.cacheKeyForObject = self.cacheKeyForObject

try executor.execute(selections: selections, on: object, withKey: key, variables: variables, accumulator: normalizer)
.flatMap {
self.cache.merge(records: $0)
}.andThen { changedKeys in
if let didChangeKeysFunc = self.updateChangedKeysFunc {
didChangeKeysFunc(changedKeys, nil)
didChangeKeysFunc(changedKeys, nil)
}
}.wait()
}
Expand Down
41 changes: 39 additions & 2 deletions Tests/ApolloCacheDependentTests/ReadWriteFromStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class ReadWriteFromStoreTests: XCTestCase {
})
}
}

func testReadHeroNameQueryWithMissingName() throws {
let initialRecords: RecordSet = [
"QUERY_ROOT": ["hero": Reference(key: "hero")],
Expand Down Expand Up @@ -160,7 +160,44 @@ class ReadWriteFromStoreTests: XCTestCase {
XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa", "C-3PO"])
}
}


func testUpdateHeroAndFriendsNamesQueryWithVariable() throws {
let initialRecords: RecordSet = [
"QUERY_ROOT": ["hero(episode:NEWHOPE)": Reference(key: "2001")],
"2001": [
"name": "R2-D2",
"__typename": "Droid",
"friends": [
Reference(key: "1000"),
Reference(key: "1002"),
Reference(key: "1003")
]
],
"1000": ["__typename": "Human", "name": "Luke Skywalker"],
"1002": ["__typename": "Human", "name": "Han Solo"],
"1003": ["__typename": "Human", "name": "Leia Organa"],
]

try withCache(initialRecords: initialRecords) { (cache) in
let store = ApolloStore(cache: cache)

let query = HeroAndFriendsNamesQuery(episode: Episode.newhope)

try await(store.withinReadWriteTransaction { transaction in
try transaction.update(query: query) { (data: inout HeroAndFriendsNamesQuery.Data) in
data.hero?.friends?.append(.makeDroid(name: "C-3PO"))
}
})

let result = try await(store.load(query: query))
guard let data = result.data else { XCTFail(); return }

XCTAssertEqual(data.hero?.name, "R2-D2")
let friendsNames = data.hero?.friends?.compactMap { $0?.name }
XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa", "C-3PO"])
}
}

func testReadHeroDetailsFragmentWithTypeSpecificProperty() throws {
let initialRecords: RecordSet = [
"2001": ["name": "R2-D2", "__typename": "Droid", "primaryFunction": "Protocol"]
Expand Down
10 changes: 10 additions & 0 deletions Tests/ApolloPerformanceTests/NormalizedCachingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@ import XCTest
import StarWarsAPI

private final class MockBatchedNormalizedCache: NormalizedCache {

private var records: RecordSet

init(records: RecordSet) {
self.records = records
}

func clear() -> Promise<Void> {
return Promise { fulfill, reject in
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(100)) {
self.records.clear()
fulfill(())
}
}
}

func loadRecords(forKeys keys: [CacheKey]) -> Promise<[Record?]> {
return Promise { fulfill, reject in
DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(100)) {
Expand Down
4 changes: 4 additions & 0 deletions Tests/ApolloWebsocketTests/MockWebSocket.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import Starscream
@testable import ApolloWebSocket

class MockWebSocket: ApolloWebSocketClient {
var pongDelegate: WebSocketPongDelegate?

var sslClientCertificate: SSLClientCertificate?

required init(request: URLRequest, protocols: [String]?) {
}

Expand Down