Skip to content

Commit 10a43e4

Browse files
authored
Merge pull request #1826 from gpambrozio/fix_cycle
Fix retain cycle
2 parents 4e2a495 + e4fc7ce commit 10a43e4

2 files changed

Lines changed: 80 additions & 4 deletions

File tree

Sources/Apollo/GraphQLQueryWatcher.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo
1515

1616
private let contextIdentifier = UUID()
1717

18-
private var fetching: Atomic<Cancellable?> = Atomic(nil)
18+
private class WeakCancellableContainer {
19+
weak var cancellable: Cancellable?
20+
fileprivate init(_ cancellable: Cancellable?) {
21+
self.cancellable = cancellable
22+
}
23+
}
24+
private var fetching: Atomic<WeakCancellableContainer> = Atomic(.init(nil))
1925

2026
private var dependentKeys: Atomic<Set<CacheKey>?> = Atomic(nil)
2127

@@ -46,8 +52,8 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo
4652
func fetch(cachePolicy: CachePolicy) {
4753
fetching.mutate {
4854
// Cancel anything already in flight before starting a new fetch
49-
$0?.cancel()
50-
$0 = client?.fetch(query: query, cachePolicy: cachePolicy, contextIdentifier: self.contextIdentifier, queue: callbackQueue) { [weak self] result in
55+
$0.cancellable?.cancel()
56+
$0.cancellable = client?.fetch(query: query, cachePolicy: cachePolicy, contextIdentifier: self.contextIdentifier, queue: callbackQueue) { [weak self] result in
5157
guard let self = self else { return }
5258

5359
switch result {
@@ -66,7 +72,7 @@ public final class GraphQLQueryWatcher<Query: GraphQLQuery>: Cancellable, Apollo
6672

6773
/// Cancel any in progress fetching operations and unsubscribe from the store.
6874
public func cancel() {
69-
fetching.value?.cancel()
75+
fetching.value.cancellable?.cancel()
7076
client?.store.unsubscribe(self)
7177
}
7278

Tests/ApolloTests/Cache/WatchQueryTests.swift

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,4 +1010,74 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting {
10101010
wait(for: [serverRequestExpectation, otherFetchCompletedExpectation, updatedWatcherResultExpectation], timeout: defaultWaitTimeout)
10111011
}
10121012
}
1013+
1014+
func testQueryWatcherDoesNotHaveARetainCycle() {
1015+
client.store.cacheKeyForObject = { $0["id"] }
1016+
1017+
let watchedQuery = HeroAndFriendsNamesWithIDsQuery()
1018+
1019+
let resultObserver = makeResultObserver(for: watchedQuery)
1020+
1021+
var watcher: GraphQLQueryWatcher<HeroAndFriendsNamesWithIDsQuery>? = GraphQLQueryWatcher(client: client, query: watchedQuery, resultHandler: resultObserver.handler)
1022+
weak var weakWatcher: GraphQLQueryWatcher<HeroAndFriendsNamesWithIDsQuery>? = watcher
1023+
1024+
runActivity("Initial fetch from server") { _ in
1025+
let serverRequestExpectation = server.expect(HeroAndFriendsNamesWithIDsQuery.self) { request in
1026+
[
1027+
"data": [
1028+
"hero": [
1029+
"id": "2001",
1030+
"name": "R2-D2",
1031+
"__typename": "Droid",
1032+
"friends": [
1033+
["__typename": "Human", "id": "1000", "name": "Luke Skywalker"],
1034+
]
1035+
]
1036+
]
1037+
]
1038+
}
1039+
1040+
let initialWatcherResultExpectation = resultObserver.expectation(description: "Watcher received initial result from server") { result in
1041+
try XCTAssertSuccessResult(result) { graphQLResult in
1042+
XCTAssertEqual(graphQLResult.source, .server)
1043+
XCTAssertNil(graphQLResult.errors)
1044+
1045+
let data = try XCTUnwrap(graphQLResult.data)
1046+
1047+
XCTAssertEqual(data.hero?.name, "R2-D2")
1048+
1049+
let friendsNames = data.hero?.friends?.compactMap { $0?.name }
1050+
XCTAssertEqual(friendsNames, ["Luke Skywalker"])
1051+
1052+
let expectedDependentKeys: Set = [
1053+
"2001.__typename",
1054+
"2001.friends",
1055+
"2001.id",
1056+
"2001.name",
1057+
"1000.__typename",
1058+
"1000.id",
1059+
"1000.name",
1060+
"QUERY_ROOT.hero",
1061+
]
1062+
let actualDependentKeys = try XCTUnwrap(graphQLResult.dependentKeys)
1063+
XCTAssertEqual(actualDependentKeys, expectedDependentKeys)
1064+
}
1065+
}
1066+
1067+
watcher?.fetch(cachePolicy: .fetchIgnoringCacheData)
1068+
1069+
wait(for: [serverRequestExpectation, initialWatcherResultExpectation], timeout: defaultWaitTimeout)
1070+
}
1071+
1072+
runActivity("make sure it gets released") { _ in
1073+
watcher = nil
1074+
cache = nil
1075+
server = nil
1076+
client = nil
1077+
1078+
// Need to allow for autorelease pools to do their job
1079+
RunLoop.current.run(until: .init(timeIntervalSinceNow: 0.01))
1080+
XCTAssertNil(weakWatcher)
1081+
}
1082+
}
10131083
}

0 commit comments

Comments
 (0)