Skip to content

Commit

Permalink
Trigger watches when writing to cache within a transaction (#187)
Browse files Browse the repository at this point in the history
  • Loading branch information
rhishikeshj authored and martijnwalraven committed Dec 5, 2017
1 parent c80e5f7 commit a5a3e1d
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 9 deletions.
28 changes: 22 additions & 6 deletions Sources/Apollo/ApolloStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Dispatch

/// A function that returns a cache key for a particular result object. If it returns `nil`, a default cache key based on the field path will be used.
public typealias CacheKeyForObject = (_ object: JSONObject) -> JSONValue?
public typealias DidChangeKeysFunc = (Set<CacheKey>, UnsafeMutableRawPointer?) -> Void

protocol ApolloStoreSubscriber: class {
func store(_ store: ApolloStore, didChangeKeys changedKeys: Set<CacheKey>, context: UnsafeMutableRawPointer?)
Expand All @@ -26,15 +27,19 @@ public final class ApolloStore {
queue = DispatchQueue(label: "com.apollographql.ApolloStore", attributes: .concurrent)
}

fileprivate func didChangeKeys(_ changedKeys: Set<CacheKey>, context: UnsafeMutableRawPointer?) {
for subscriber in self.subscribers {
subscriber.store(self, didChangeKeys: changedKeys, context: context)
}
}

func publish(records: RecordSet, context: UnsafeMutableRawPointer? = nil) -> Promise<Void> {
return Promise<Void> { fulfill, reject in
queue.async(flags: .barrier) {
self.cacheLock.withWriteLock {
self.cache.merge(records: records)
}.andThen { changedKeys in
for subscriber in self.subscribers {
subscriber.store(self, didChangeKeys: changedKeys, context: context)
}
self.didChangeKeys(changedKeys, context: context)
fulfill(())
}
}
Expand Down Expand Up @@ -76,8 +81,7 @@ public final class ApolloStore {
return Promise<ReadWriteTransaction> { fulfill, reject in
self.queue.async(flags: .barrier) {
self.cacheLock.lockForWriting()

fulfill(ReadWriteTransaction(cache: self.cache, cacheKeyForObject: self.cacheKeyForObject))
fulfill(ReadWriteTransaction(cache: self.cache, cacheKeyForObject: self.cacheKeyForObject, updateChangedKeysFunc: self.didChangeKeys))
}
}.flatMap(body)
.finally {
Expand Down Expand Up @@ -175,6 +179,14 @@ public final class ApolloStore {
}

public final class ReadWriteTransaction: ReadTransaction {

fileprivate var updateChangedKeysFunc: DidChangeKeysFunc?

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

public func update<Query: GraphQLQuery>(query: Query, _ body: (inout Query.Data) throws -> Void) throws {
var data = try read(query: query)
try body(&data)
Expand All @@ -199,7 +211,11 @@ public final class ApolloStore {
let normalizer = GraphQLResultNormalizer()
try self.makeExecutor().execute(selections: selections, on: object, withKey: key, variables: variables, accumulator: normalizer)
.flatMap {
self.cache.merge(records: $0).map { _ in }
self.cache.merge(records: $0)
}.andThen { changedKeys in
if let didChangeKeysFunc = self.updateChangedKeysFunc {
didChangeKeysFunc(changedKeys, nil)
}
}.await()
}
}
Expand Down
73 changes: 70 additions & 3 deletions Tests/ApolloCacheDependentTests/WatchQueryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,82 @@ class WatchQueryTests: XCTestCase {
expectation = self.expectation(description: "Fetching other query")

client.fetch(query: HeroNameWithIdQuery(), cachePolicy: .fetchIgnoringCacheData)

waitForExpectations(timeout: 5, handler: nil)
}
}


func testWatchedQueryGetsUpdatedWithResultFromReadWriteTransaction() throws {
let initialRecords: RecordSet = [
"QUERY_ROOT": ["hero": Reference(key: "QUERY_ROOT.hero")],
"QUERY_ROOT.hero": [
"name": "R2-D2",
"__typename": "Droid",
"friends": [
Reference(key: "QUERY_ROOT.hero.friends.0"),
Reference(key: "QUERY_ROOT.hero.friends.1"),
Reference(key: "QUERY_ROOT.hero.friends.2")
]
],
"QUERY_ROOT.hero.friends.0": ["__typename": "Human", "name": "Luke Skywalker"],
"QUERY_ROOT.hero.friends.1": ["__typename": "Human", "name": "Han Solo"],
"QUERY_ROOT.hero.friends.2": ["__typename": "Human", "name": "Leia Organa"],
]
try withCache(initialRecords: initialRecords) { (cache) in
let networkTransport = MockNetworkTransport(body: [:])

let store = ApolloStore(cache: cache)
let client = ApolloClient(networkTransport: networkTransport, store: store)
let query = HeroAndFriendsNamesQuery()

var verifyResult: OperationResultHandler<HeroAndFriendsNamesQuery>

verifyResult = { (result, error) in
XCTAssertNil(error)
XCTAssertNil(result?.errors)

guard let data = result?.data else { XCTFail(); return }
XCTAssertEqual(data.hero?.name, "R2-D2")
let friendsNames = data.hero?.friends?.flatMap { $0?.name }
XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"])
}

var expectation = self.expectation(description: "Fetching query")

_ = client.watch(query: query) { (result, error) in
verifyResult(result, error)
expectation.fulfill()
}

waitForExpectations(timeout: 5, handler: nil)

let nameQuery = HeroNameQuery()
try await(store.withinReadWriteTransaction { transaction in
try transaction.update(query: nameQuery) { (data: inout HeroNameQuery.Data) in
data.hero?.name = "Artoo"
}
})

verifyResult = { (result, error) in
XCTAssertNil(error)
XCTAssertNil(result?.errors)

guard let data = result?.data else { XCTFail(); return }
XCTAssertEqual(data.hero?.name, "Artoo")
let friendsNames = data.hero?.friends?.flatMap { $0?.name }
XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"])
}

expectation = self.expectation(description: "Updated after fetching other query")
client.fetch(query: HeroNameQuery(), cachePolicy: .fetchIgnoringCacheData)
waitForExpectations(timeout: 5, handler: nil)
}
}

// TODO: Replace with .inverted on XCTestExpectation, which is new in Xcode 8.3
private func waitFor(timeInterval: TimeInterval) {
let untilDate = Date(timeIntervalSinceNow: timeInterval)

while untilDate.timeIntervalSinceNow > 0 {
RunLoop.current.run(mode: .defaultRunLoopMode, before: untilDate)
}
Expand Down

0 comments on commit a5a3e1d

Please sign in to comment.