FSTSyncEngine.mm 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "Firestore/Source/Core/FSTSyncEngine.h"
  17. #import <GRPCClient/GRPCCall.h>
  18. #import "FIRFirestoreErrors.h"
  19. #import "Firestore/Source/Auth/FSTUser.h"
  20. #import "Firestore/Source/Core/FSTQuery.h"
  21. #import "Firestore/Source/Core/FSTSnapshotVersion.h"
  22. #import "Firestore/Source/Core/FSTTransaction.h"
  23. #import "Firestore/Source/Core/FSTView.h"
  24. #import "Firestore/Source/Core/FSTViewSnapshot.h"
  25. #import "Firestore/Source/Local/FSTEagerGarbageCollector.h"
  26. #import "Firestore/Source/Local/FSTLocalStore.h"
  27. #import "Firestore/Source/Local/FSTLocalViewChanges.h"
  28. #import "Firestore/Source/Local/FSTLocalWriteResult.h"
  29. #import "Firestore/Source/Local/FSTQueryData.h"
  30. #import "Firestore/Source/Local/FSTReferenceSet.h"
  31. #import "Firestore/Source/Model/FSTDocument.h"
  32. #import "Firestore/Source/Model/FSTDocumentKey.h"
  33. #import "Firestore/Source/Model/FSTDocumentSet.h"
  34. #import "Firestore/Source/Model/FSTMutationBatch.h"
  35. #import "Firestore/Source/Remote/FSTRemoteEvent.h"
  36. #import "Firestore/Source/Util/FSTAssert.h"
  37. #import "Firestore/Source/Util/FSTDispatchQueue.h"
  38. #import "Firestore/Source/Util/FSTLogger.h"
  39. #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h"
  40. using firebase::firestore::core::TargetIdGenerator;
  41. NS_ASSUME_NONNULL_BEGIN
  42. // Limbo documents don't use persistence, and are eagerly GC'd. So, listens for them don't need
  43. // real sequence numbers.
  44. static const FSTListenSequenceNumber kIrrelevantSequenceNumber = -1;
  45. #pragma mark - FSTQueryView
  46. /**
  47. * FSTQueryView contains all of the info that FSTSyncEngine needs to track for a particular
  48. * query and view.
  49. */
  50. @interface FSTQueryView : NSObject
  51. - (instancetype)initWithQuery:(FSTQuery *)query
  52. targetID:(FSTTargetID)targetID
  53. resumeToken:(NSData *)resumeToken
  54. view:(FSTView *)view NS_DESIGNATED_INITIALIZER;
  55. - (instancetype)init NS_UNAVAILABLE;
  56. /** The query itself. */
  57. @property(nonatomic, strong, readonly) FSTQuery *query;
  58. /** The targetID created by the client that is used in the watch stream to identify this query. */
  59. @property(nonatomic, assign, readonly) FSTTargetID targetID;
  60. /**
  61. * An identifier from the datastore backend that indicates the last state of the results that
  62. * was received. This can be used to indicate where to continue receiving new doc changes for the
  63. * query.
  64. */
  65. @property(nonatomic, copy, readonly) NSData *resumeToken;
  66. /**
  67. * The view is responsible for computing the final merged truth of what docs are in the query.
  68. * It gets notified of local and remote changes, and applies the query filters and limits to
  69. * determine the most correct possible results.
  70. */
  71. @property(nonatomic, strong, readonly) FSTView *view;
  72. @end
  73. @implementation FSTQueryView
  74. - (instancetype)initWithQuery:(FSTQuery *)query
  75. targetID:(FSTTargetID)targetID
  76. resumeToken:(NSData *)resumeToken
  77. view:(FSTView *)view {
  78. if (self = [super init]) {
  79. _query = query;
  80. _targetID = targetID;
  81. _resumeToken = resumeToken;
  82. _view = view;
  83. }
  84. return self;
  85. }
  86. @end
  87. #pragma mark - FSTSyncEngine
  88. @interface FSTSyncEngine ()
  89. /** The local store, used to persist mutations and cached documents. */
  90. @property(nonatomic, strong, readonly) FSTLocalStore *localStore;
  91. /** The remote store for sending writes, watches, etc. to the backend. */
  92. @property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore;
  93. /** FSTQueryViews for all active queries, indexed by query. */
  94. @property(nonatomic, strong, readonly)
  95. NSMutableDictionary<FSTQuery *, FSTQueryView *> *queryViewsByQuery;
  96. /** FSTQueryViews for all active queries, indexed by target ID. */
  97. @property(nonatomic, strong, readonly)
  98. NSMutableDictionary<NSNumber *, FSTQueryView *> *queryViewsByTarget;
  99. /**
  100. * When a document is in limbo, we create a special listen to resolve it. This maps the
  101. * FSTDocumentKey of each limbo document to the FSTTargetID of the listen resolving it.
  102. */
  103. @property(nonatomic, strong, readonly)
  104. NSMutableDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *limboTargetsByKey;
  105. /** The inverse of limboTargetsByKey, a map of FSTTargetID to the key of the limbo doc. */
  106. @property(nonatomic, strong, readonly)
  107. NSMutableDictionary<FSTBoxedTargetID *, FSTDocumentKey *> *limboKeysByTarget;
  108. /** Used to track any documents that are currently in limbo. */
  109. @property(nonatomic, strong, readonly) FSTReferenceSet *limboDocumentRefs;
  110. /** The garbage collector used to collect documents that are no longer in limbo. */
  111. @property(nonatomic, strong, readonly) FSTEagerGarbageCollector *limboCollector;
  112. /** Stores user completion blocks, indexed by user and FSTBatchID. */
  113. @property(nonatomic, strong)
  114. NSMutableDictionary<FSTUser *, NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *>
  115. *mutationCompletionBlocks;
  116. @property(nonatomic, strong) FSTUser *currentUser;
  117. @end
  118. @implementation FSTSyncEngine {
  119. /** Used for creating the FSTTargetIDs for the listens used to resolve limbo documents. */
  120. TargetIdGenerator _targetIdGenerator;
  121. }
  122. - (instancetype)initWithLocalStore:(FSTLocalStore *)localStore
  123. remoteStore:(FSTRemoteStore *)remoteStore
  124. initialUser:(FSTUser *)initialUser {
  125. if (self = [super init]) {
  126. _localStore = localStore;
  127. _remoteStore = remoteStore;
  128. _queryViewsByQuery = [NSMutableDictionary dictionary];
  129. _queryViewsByTarget = [NSMutableDictionary dictionary];
  130. _limboTargetsByKey = [NSMutableDictionary dictionary];
  131. _limboKeysByTarget = [NSMutableDictionary dictionary];
  132. _limboCollector = [[FSTEagerGarbageCollector alloc] init];
  133. _limboDocumentRefs = [[FSTReferenceSet alloc] init];
  134. [_limboCollector addGarbageSource:_limboDocumentRefs];
  135. _mutationCompletionBlocks = [NSMutableDictionary dictionary];
  136. _targetIdGenerator = TargetIdGenerator::SyncEngineTargetIdGenerator(0);
  137. _currentUser = initialUser;
  138. }
  139. return self;
  140. }
  141. - (FSTTargetID)listenToQuery:(FSTQuery *)query {
  142. [self assertDelegateExistsForSelector:_cmd];
  143. FSTAssert(self.queryViewsByQuery[query] == nil, @"We already listen to query: %@", query);
  144. FSTQueryData *queryData = [self.localStore allocateQuery:query];
  145. FSTDocumentDictionary *docs = [self.localStore executeQuery:query];
  146. FSTDocumentKeySet *remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID];
  147. FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:remoteKeys];
  148. FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs];
  149. FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges];
  150. FSTAssert(viewChange.limboChanges.count == 0,
  151. @"View returned limbo docs before target ack from the server.");
  152. FSTQueryView *queryView = [[FSTQueryView alloc] initWithQuery:query
  153. targetID:queryData.targetID
  154. resumeToken:queryData.resumeToken
  155. view:view];
  156. self.queryViewsByQuery[query] = queryView;
  157. self.queryViewsByTarget[@(queryData.targetID)] = queryView;
  158. [self.delegate handleViewSnapshots:@[ viewChange.snapshot ]];
  159. [self.remoteStore listenToTargetWithQueryData:queryData];
  160. return queryData.targetID;
  161. }
  162. - (void)stopListeningToQuery:(FSTQuery *)query {
  163. [self assertDelegateExistsForSelector:_cmd];
  164. FSTQueryView *queryView = self.queryViewsByQuery[query];
  165. FSTAssert(queryView, @"Trying to stop listening to a query not found");
  166. [self.localStore releaseQuery:query];
  167. [self.remoteStore stopListeningToTargetID:queryView.targetID];
  168. [self removeAndCleanupQuery:queryView];
  169. [self.localStore collectGarbage];
  170. }
  171. - (void)writeMutations:(NSArray<FSTMutation *> *)mutations
  172. completion:(FSTVoidErrorBlock)completion {
  173. [self assertDelegateExistsForSelector:_cmd];
  174. FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations];
  175. [self addMutationCompletionBlock:completion batchID:result.batchID];
  176. [self emitNewSnapshotsWithChanges:result.changes remoteEvent:nil];
  177. [self.remoteStore fillWritePipeline];
  178. }
  179. - (void)addMutationCompletionBlock:(FSTVoidErrorBlock)completion batchID:(FSTBatchID)batchID {
  180. NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *completionBlocks =
  181. self.mutationCompletionBlocks[self.currentUser];
  182. if (!completionBlocks) {
  183. completionBlocks = [NSMutableDictionary dictionary];
  184. self.mutationCompletionBlocks[self.currentUser] = completionBlocks;
  185. }
  186. [completionBlocks setObject:completion forKey:@(batchID)];
  187. }
  188. /**
  189. * Takes an updateBlock in which a set of reads and writes can be performed atomically. In the
  190. * updateBlock, user code can read and write values using a transaction object. After the
  191. * updateBlock, all changes will be committed. If someone else has changed any of the data
  192. * referenced, then the updateBlock will be called again. If the updateBlock still fails after the
  193. * given number of retries, then the transaction will be rejected.
  194. *
  195. * The transaction object passed to the updateBlock contains methods for accessing documents
  196. * and collections. Unlike other firestore access, data accessed with the transaction will not
  197. * reflect local changes that have not been committed. For this reason, it is required that all
  198. * reads are performed before any writes. Transactions must be performed while online.
  199. */
  200. - (void)transactionWithRetries:(int)retries
  201. workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
  202. updateBlock:(FSTTransactionBlock)updateBlock
  203. completion:(FSTVoidIDErrorBlock)completion {
  204. [workerDispatchQueue verifyIsCurrentQueue];
  205. FSTAssert(retries >= 0, @"Got negative number of retries for transaction");
  206. FSTTransaction *transaction = [self.remoteStore transaction];
  207. updateBlock(transaction, ^(id _Nullable result, NSError *_Nullable error) {
  208. [workerDispatchQueue dispatchAsync:^{
  209. if (error) {
  210. completion(nil, error);
  211. return;
  212. }
  213. [transaction commitWithCompletion:^(NSError *_Nullable transactionError) {
  214. if (!transactionError) {
  215. completion(result, nil);
  216. return;
  217. }
  218. // TODO(b/35201829): Only retry on real transaction failures.
  219. if (retries == 0) {
  220. NSError *wrappedError =
  221. [NSError errorWithDomain:FIRFirestoreErrorDomain
  222. code:FIRFirestoreErrorCodeFailedPrecondition
  223. userInfo:@{
  224. NSLocalizedDescriptionKey : @"Transaction failed all retries.",
  225. NSUnderlyingErrorKey : transactionError
  226. }];
  227. completion(nil, wrappedError);
  228. return;
  229. }
  230. [workerDispatchQueue verifyIsCurrentQueue];
  231. return [self transactionWithRetries:(retries - 1)
  232. workerDispatchQueue:workerDispatchQueue
  233. updateBlock:updateBlock
  234. completion:completion];
  235. }];
  236. }];
  237. });
  238. }
  239. - (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
  240. [self assertDelegateExistsForSelector:_cmd];
  241. // Make sure limbo documents are deleted if there were no results
  242. [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^(
  243. FSTBoxedTargetID *_Nonnull targetID,
  244. FSTTargetChange *_Nonnull targetChange, BOOL *_Nonnull stop) {
  245. FSTDocumentKey *limboKey = self.limboKeysByTarget[targetID];
  246. if (limboKey && targetChange.currentStatusUpdate == FSTCurrentStatusUpdateMarkCurrent &&
  247. remoteEvent.documentUpdates[limboKey] == nil) {
  248. // When listening to a query the server responds with a snapshot containing documents
  249. // matching the query and a current marker telling us we're now in sync. It's possible for
  250. // these to arrive as separate remote events or as a single remote event. For a document
  251. // query, there will be no documents sent in the response if the document doesn't exist.
  252. //
  253. // If the snapshot arrives separately from the current marker, we handle it normally and
  254. // updateTrackedLimboDocumentsWithChanges:targetID: will resolve the limbo status of the
  255. // document, removing it from limboDocumentRefs. This works because clients only initiate
  256. // limbo resolution when a target is current and because all current targets are always at a
  257. // consistent snapshot.
  258. //
  259. // However, if the document doesn't exist and the current marker arrives, the document is
  260. // not present in the snapshot and our normal view handling would consider the document to
  261. // remain in limbo indefinitely because there are no updates to the document. To avoid this,
  262. // we specially handle this just this case here: synthesizing a delete.
  263. //
  264. // TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an
  265. // explicit delete message and we could remove this special logic.
  266. [remoteEvent
  267. addDocumentUpdate:[FSTDeletedDocument documentWithKey:limboKey
  268. version:remoteEvent.snapshotVersion]];
  269. }
  270. }];
  271. FSTMaybeDocumentDictionary *changes = [self.localStore applyRemoteEvent:remoteEvent];
  272. [self emitNewSnapshotsWithChanges:changes remoteEvent:remoteEvent];
  273. }
  274. - (void)applyChangedOnlineState:(FSTOnlineState)onlineState {
  275. NSMutableArray<FSTViewSnapshot *> *newViewSnapshots = [NSMutableArray array];
  276. [self.queryViewsByQuery
  277. enumerateKeysAndObjectsUsingBlock:^(FSTQuery *query, FSTQueryView *queryView, BOOL *stop) {
  278. FSTViewChange *viewChange = [queryView.view applyChangedOnlineState:onlineState];
  279. FSTAssert(viewChange.limboChanges.count == 0,
  280. @"OnlineState should not affect limbo documents.");
  281. if (viewChange.snapshot) {
  282. [newViewSnapshots addObject:viewChange.snapshot];
  283. }
  284. }];
  285. [self.delegate handleViewSnapshots:newViewSnapshots];
  286. }
  287. - (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error {
  288. [self assertDelegateExistsForSelector:_cmd];
  289. FSTDocumentKey *limboKey = self.limboKeysByTarget[targetID];
  290. if (limboKey) {
  291. // Since this query failed, we won't want to manually unlisten to it.
  292. // So go ahead and remove it from bookkeeping.
  293. [self.limboTargetsByKey removeObjectForKey:limboKey];
  294. [self.limboKeysByTarget removeObjectForKey:targetID];
  295. // TODO(dimond): Retry on transient errors?
  296. // It's a limbo doc. Create a synthetic event saying it was deleted. This is kind of a hack.
  297. // Ideally, we would have a method in the local store to purge a document. However, it would
  298. // be tricky to keep all of the local store's invariants with another method.
  299. NSMutableDictionary<NSNumber *, FSTTargetChange *> *targetChanges =
  300. [NSMutableDictionary dictionary];
  301. FSTDeletedDocument *doc =
  302. [FSTDeletedDocument documentWithKey:limboKey version:[FSTSnapshotVersion noVersion]];
  303. NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *docUpdate =
  304. [NSMutableDictionary dictionaryWithObject:doc forKey:limboKey];
  305. FSTRemoteEvent *event = [FSTRemoteEvent eventWithSnapshotVersion:[FSTSnapshotVersion noVersion]
  306. targetChanges:targetChanges
  307. documentUpdates:docUpdate];
  308. [self applyRemoteEvent:event];
  309. } else {
  310. FSTQueryView *queryView = self.queryViewsByTarget[targetID];
  311. FSTAssert(queryView, @"Unknown targetId: %@", targetID);
  312. [self.localStore releaseQuery:queryView.query];
  313. [self removeAndCleanupQuery:queryView];
  314. [self.delegate handleError:error forQuery:queryView.query];
  315. }
  316. }
  317. - (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult {
  318. [self assertDelegateExistsForSelector:_cmd];
  319. // The local store may or may not be able to apply the write result and raise events immediately
  320. // (depending on whether the watcher is caught up), so we raise user callbacks first so that they
  321. // consistently happen before listen events.
  322. [self processUserCallbacksForBatchID:batchResult.batch.batchID error:nil];
  323. FSTMaybeDocumentDictionary *changes = [self.localStore acknowledgeBatchWithResult:batchResult];
  324. [self emitNewSnapshotsWithChanges:changes remoteEvent:nil];
  325. }
  326. - (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error {
  327. [self assertDelegateExistsForSelector:_cmd];
  328. // The local store may or may not be able to apply the write result and raise events immediately
  329. // (depending on whether the watcher is caught up), so we raise user callbacks first so that they
  330. // consistently happen before listen events.
  331. [self processUserCallbacksForBatchID:batchID error:error];
  332. FSTMaybeDocumentDictionary *changes = [self.localStore rejectBatchID:batchID];
  333. [self emitNewSnapshotsWithChanges:changes remoteEvent:nil];
  334. }
  335. - (void)processUserCallbacksForBatchID:(FSTBatchID)batchID error:(NSError *_Nullable)error {
  336. NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *completionBlocks =
  337. self.mutationCompletionBlocks[self.currentUser];
  338. // NOTE: Mutations restored from persistence won't have completion blocks, so it's okay for
  339. // this (or the completion below) to be nil.
  340. if (completionBlocks) {
  341. NSNumber *boxedBatchID = @(batchID);
  342. FSTVoidErrorBlock completion = completionBlocks[boxedBatchID];
  343. if (completion) {
  344. completion(error);
  345. [completionBlocks removeObjectForKey:boxedBatchID];
  346. }
  347. }
  348. }
  349. - (void)assertDelegateExistsForSelector:(SEL)methodSelector {
  350. FSTAssert(self.delegate, @"Tried to call '%@' before delegate was registered.",
  351. NSStringFromSelector(methodSelector));
  352. }
  353. - (void)removeAndCleanupQuery:(FSTQueryView *)queryView {
  354. [self.queryViewsByQuery removeObjectForKey:queryView.query];
  355. [self.queryViewsByTarget removeObjectForKey:@(queryView.targetID)];
  356. [self.limboDocumentRefs removeReferencesForID:queryView.targetID];
  357. [self garbageCollectLimboDocuments];
  358. }
  359. /**
  360. * Computes a new snapshot from the changes and calls the registered callback with the new snapshot.
  361. */
  362. - (void)emitNewSnapshotsWithChanges:(FSTMaybeDocumentDictionary *)changes
  363. remoteEvent:(FSTRemoteEvent *_Nullable)remoteEvent {
  364. NSMutableArray<FSTViewSnapshot *> *newSnapshots = [NSMutableArray array];
  365. NSMutableArray<FSTLocalViewChanges *> *documentChangesInAllViews = [NSMutableArray array];
  366. [self.queryViewsByQuery
  367. enumerateKeysAndObjectsUsingBlock:^(FSTQuery *query, FSTQueryView *queryView, BOOL *stop) {
  368. FSTView *view = queryView.view;
  369. FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:changes];
  370. if (viewDocChanges.needsRefill) {
  371. // The query has a limit and some docs were removed/updated, so we need to re-run the
  372. // query against the local store to make sure we didn't lose any good docs that had been
  373. // past the limit.
  374. FSTDocumentDictionary *docs = [self.localStore executeQuery:queryView.query];
  375. viewDocChanges = [view computeChangesWithDocuments:docs previousChanges:viewDocChanges];
  376. }
  377. FSTTargetChange *_Nullable targetChange = remoteEvent.targetChanges[@(queryView.targetID)];
  378. FSTViewChange *viewChange =
  379. [queryView.view applyChangesToDocuments:viewDocChanges targetChange:targetChange];
  380. [self updateTrackedLimboDocumentsWithChanges:viewChange.limboChanges
  381. targetID:queryView.targetID];
  382. if (viewChange.snapshot) {
  383. [newSnapshots addObject:viewChange.snapshot];
  384. FSTLocalViewChanges *docChanges =
  385. [FSTLocalViewChanges changesForViewSnapshot:viewChange.snapshot];
  386. [documentChangesInAllViews addObject:docChanges];
  387. }
  388. }];
  389. [self.delegate handleViewSnapshots:newSnapshots];
  390. [self.localStore notifyLocalViewChanges:documentChangesInAllViews];
  391. [self.localStore collectGarbage];
  392. }
  393. /** Updates the limbo document state for the given targetID. */
  394. - (void)updateTrackedLimboDocumentsWithChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges
  395. targetID:(FSTTargetID)targetID {
  396. for (FSTLimboDocumentChange *limboChange in limboChanges) {
  397. switch (limboChange.type) {
  398. case FSTLimboDocumentChangeTypeAdded:
  399. [self.limboDocumentRefs addReferenceToKey:limboChange.key forID:targetID];
  400. [self trackLimboChange:limboChange];
  401. break;
  402. case FSTLimboDocumentChangeTypeRemoved:
  403. FSTLog(@"Document no longer in limbo: %@", limboChange.key);
  404. [self.limboDocumentRefs removeReferenceToKey:limboChange.key forID:targetID];
  405. break;
  406. default:
  407. FSTFail(@"Unknown limbo change type: %ld", (long)limboChange.type);
  408. }
  409. }
  410. [self garbageCollectLimboDocuments];
  411. }
  412. - (void)trackLimboChange:(FSTLimboDocumentChange *)limboChange {
  413. FSTDocumentKey *key = limboChange.key;
  414. if (!self.limboTargetsByKey[key]) {
  415. FSTLog(@"New document in limbo: %@", key);
  416. FSTTargetID limboTargetID = _targetIdGenerator.NextId();
  417. FSTQuery *query = [FSTQuery queryWithPath:key.path];
  418. FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query
  419. targetID:limboTargetID
  420. listenSequenceNumber:kIrrelevantSequenceNumber
  421. purpose:FSTQueryPurposeLimboResolution];
  422. self.limboKeysByTarget[@(limboTargetID)] = key;
  423. [self.remoteStore listenToTargetWithQueryData:queryData];
  424. self.limboTargetsByKey[key] = @(limboTargetID);
  425. }
  426. }
  427. /** Garbage collect the limbo documents that we no longer need to track. */
  428. - (void)garbageCollectLimboDocuments {
  429. NSSet<FSTDocumentKey *> *garbage = [self.limboCollector collectGarbage];
  430. for (FSTDocumentKey *key in garbage) {
  431. FSTBoxedTargetID *limboTarget = self.limboTargetsByKey[key];
  432. if (!limboTarget) {
  433. // This target already got removed, because the query failed.
  434. return;
  435. }
  436. FSTTargetID limboTargetID = limboTarget.intValue;
  437. [self.remoteStore stopListeningToTargetID:limboTargetID];
  438. [self.limboTargetsByKey removeObjectForKey:key];
  439. [self.limboKeysByTarget removeObjectForKey:limboTarget];
  440. }
  441. }
  442. // Used for testing
  443. - (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments {
  444. // Return defensive copy
  445. return [self.limboTargetsByKey copy];
  446. }
  447. - (void)userDidChange:(FSTUser *)user {
  448. self.currentUser = user;
  449. // Notify local store and emit any resulting events from swapping out the mutation queue.
  450. FSTMaybeDocumentDictionary *changes = [self.localStore userDidChange:user];
  451. [self emitNewSnapshotsWithChanges:changes remoteEvent:nil];
  452. // Notify remote store so it can restart its streams.
  453. [self.remoteStore userDidChange:user];
  454. }
  455. @end
  456. NS_ASSUME_NONNULL_END