FSTLocalStore.mm 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  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/Local/FSTLocalStore.h"
  17. #include <set>
  18. #import "FIRTimestamp.h"
  19. #import "Firestore/Source/Core/FSTListenSequence.h"
  20. #import "Firestore/Source/Core/FSTQuery.h"
  21. #import "Firestore/Source/Local/FSTLocalDocumentsView.h"
  22. #import "Firestore/Source/Local/FSTLocalViewChanges.h"
  23. #import "Firestore/Source/Local/FSTLocalWriteResult.h"
  24. #import "Firestore/Source/Local/FSTMutationQueue.h"
  25. #import "Firestore/Source/Local/FSTPersistence.h"
  26. #import "Firestore/Source/Local/FSTQueryCache.h"
  27. #import "Firestore/Source/Local/FSTQueryData.h"
  28. #import "Firestore/Source/Local/FSTReferenceSet.h"
  29. #import "Firestore/Source/Local/FSTRemoteDocumentCache.h"
  30. #import "Firestore/Source/Model/FSTDocument.h"
  31. #import "Firestore/Source/Model/FSTDocumentDictionary.h"
  32. #import "Firestore/Source/Model/FSTMutation.h"
  33. #import "Firestore/Source/Model/FSTMutationBatch.h"
  34. #import "Firestore/Source/Remote/FSTRemoteEvent.h"
  35. #include "Firestore/core/src/firebase/firestore/auth/user.h"
  36. #include "Firestore/core/src/firebase/firestore/core/target_id_generator.h"
  37. #include "Firestore/core/src/firebase/firestore/model/document_key.h"
  38. #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h"
  39. #include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
  40. #include "Firestore/core/src/firebase/firestore/util/log.h"
  41. using firebase::firestore::auth::User;
  42. using firebase::firestore::core::TargetIdGenerator;
  43. using firebase::firestore::model::DocumentKey;
  44. using firebase::firestore::model::SnapshotVersion;
  45. using firebase::firestore::model::DocumentKeySet;
  46. using firebase::firestore::model::DocumentVersionMap;
  47. NS_ASSUME_NONNULL_BEGIN
  48. @interface FSTLocalStore ()
  49. /** Manages our in-memory or durable persistence. */
  50. @property(nonatomic, strong, readonly) id<FSTPersistence> persistence;
  51. /** The set of all mutations that have been sent but not yet been applied to the backend. */
  52. @property(nonatomic, strong) id<FSTMutationQueue> mutationQueue;
  53. /** The set of all cached remote documents. */
  54. @property(nonatomic, strong) id<FSTRemoteDocumentCache> remoteDocumentCache;
  55. /** The "local" view of all documents (layering mutationQueue on top of remoteDocumentCache). */
  56. @property(nonatomic, strong) FSTLocalDocumentsView *localDocuments;
  57. /** The set of document references maintained by any local views. */
  58. @property(nonatomic, strong) FSTReferenceSet *localViewReferences;
  59. /** Maps a query to the data about that query. */
  60. @property(nonatomic, strong) id<FSTQueryCache> queryCache;
  61. /** Maps a targetID to data about its query. */
  62. @property(nonatomic, strong) NSMutableDictionary<NSNumber *, FSTQueryData *> *targetIDs;
  63. /**
  64. * A heldBatchResult is a mutation batch result (from a write acknowledgement) that arrived before
  65. * the watch stream got notified of a snapshot that includes the write.  So we "hold" it until
  66. * the watch stream catches up. It ensures that the local write remains visible (latency
  67. * compensation) and doesn't temporarily appear reverted because the watch stream is slower than
  68. * the write stream and so wasn't reflecting it.
  69. *
  70. * NOTE: Eventually we want to move this functionality into the remote store.
  71. */
  72. @property(nonatomic, strong) NSMutableArray<FSTMutationBatchResult *> *heldBatchResults;
  73. @end
  74. @implementation FSTLocalStore {
  75. /** Used to generate targetIDs for queries tracked locally. */
  76. TargetIdGenerator _targetIDGenerator;
  77. }
  78. - (instancetype)initWithPersistence:(id<FSTPersistence>)persistence
  79. initialUser:(const User &)initialUser {
  80. if (self = [super init]) {
  81. _persistence = persistence;
  82. _mutationQueue = [persistence mutationQueueForUser:initialUser];
  83. _remoteDocumentCache = [persistence remoteDocumentCache];
  84. _queryCache = [persistence queryCache];
  85. _localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache
  86. mutationQueue:_mutationQueue];
  87. _localViewReferences = [[FSTReferenceSet alloc] init];
  88. [_persistence.referenceDelegate addInMemoryPins:_localViewReferences];
  89. _targetIDs = [NSMutableDictionary dictionary];
  90. _heldBatchResults = [NSMutableArray array];
  91. _targetIDGenerator = TargetIdGenerator::LocalStoreTargetIdGenerator(0);
  92. }
  93. return self;
  94. }
  95. - (void)start {
  96. [self startMutationQueue];
  97. FSTTargetID targetID = [self.queryCache highestTargetID];
  98. _targetIDGenerator = TargetIdGenerator::LocalStoreTargetIdGenerator(targetID);
  99. }
  100. - (void)startMutationQueue {
  101. self.persistence.run("Start MutationQueue", [&]() {
  102. [self.mutationQueue start];
  103. // If we have any leftover mutation batch results from a prior run, just drop them.
  104. // TODO(http://b/33446471): We probably need to repopulate heldBatchResults or similar instead,
  105. // but that is not straightforward since we're not persisting the write ack versions.
  106. [self.heldBatchResults removeAllObjects];
  107. // TODO(mikelehen): This is the only usage of getAllMutationBatchesThroughBatchId:. Consider
  108. // removing it in favor of a getAcknowledgedBatches method.
  109. FSTBatchID highestAck = [self.mutationQueue highestAcknowledgedBatchID];
  110. if (highestAck != kFSTBatchIDUnknown) {
  111. NSArray<FSTMutationBatch *> *batches =
  112. [self.mutationQueue allMutationBatchesThroughBatchID:highestAck];
  113. if (batches.count > 0) {
  114. // NOTE: This could be more efficient if we had a removeBatchesThroughBatchID, but this set
  115. // should be very small and this code should go away eventually.
  116. [self.mutationQueue removeMutationBatches:batches];
  117. }
  118. }
  119. });
  120. }
  121. - (FSTMaybeDocumentDictionary *)userDidChange:(const User &)user {
  122. // Swap out the mutation queue, grabbing the pending mutation batches before and after.
  123. NSArray<FSTMutationBatch *> *oldBatches = self.persistence.run(
  124. "OldBatches",
  125. [&]() -> NSArray<FSTMutationBatch *> * { return [self.mutationQueue allMutationBatches]; });
  126. self.mutationQueue = [self.persistence mutationQueueForUser:user];
  127. [self startMutationQueue];
  128. return self.persistence.run("NewBatches", [&]() -> FSTMaybeDocumentDictionary * {
  129. NSArray<FSTMutationBatch *> *newBatches = [self.mutationQueue allMutationBatches];
  130. // Recreate our LocalDocumentsView using the new MutationQueue.
  131. self.localDocuments =
  132. [FSTLocalDocumentsView viewWithRemoteDocumentCache:self.remoteDocumentCache
  133. mutationQueue:self.mutationQueue];
  134. // Union the old/new changed keys.
  135. DocumentKeySet changedKeys;
  136. for (NSArray<FSTMutationBatch *> *batches in @[ oldBatches, newBatches ]) {
  137. for (FSTMutationBatch *batch in batches) {
  138. for (FSTMutation *mutation in batch.mutations) {
  139. changedKeys = changedKeys.insert(mutation.key);
  140. }
  141. }
  142. }
  143. // Return the set of all (potentially) changed documents as the result of the user change.
  144. return [self.localDocuments documentsForKeys:changedKeys];
  145. });
  146. }
  147. - (FSTLocalWriteResult *)locallyWriteMutations:(NSArray<FSTMutation *> *)mutations {
  148. return self.persistence.run("Locally write mutations", [&]() -> FSTLocalWriteResult * {
  149. FIRTimestamp *localWriteTime = [FIRTimestamp timestamp];
  150. FSTMutationBatch *batch =
  151. [self.mutationQueue addMutationBatchWithWriteTime:localWriteTime mutations:mutations];
  152. DocumentKeySet keys = [batch keys];
  153. FSTMaybeDocumentDictionary *changedDocuments = [self.localDocuments documentsForKeys:keys];
  154. return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:changedDocuments];
  155. });
  156. }
  157. - (FSTMaybeDocumentDictionary *)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult {
  158. return self.persistence.run("Acknowledge batch", [&]() -> FSTMaybeDocumentDictionary * {
  159. id<FSTMutationQueue> mutationQueue = self.mutationQueue;
  160. [mutationQueue acknowledgeBatch:batchResult.batch streamToken:batchResult.streamToken];
  161. DocumentKeySet affected;
  162. if ([self shouldHoldBatchResultWithVersion:batchResult.commitVersion]) {
  163. [self.heldBatchResults addObject:batchResult];
  164. } else {
  165. affected = [self releaseBatchResults:@[ batchResult ]];
  166. }
  167. [self.mutationQueue performConsistencyCheck];
  168. return [self.localDocuments documentsForKeys:affected];
  169. });
  170. }
  171. - (FSTMaybeDocumentDictionary *)rejectBatchID:(FSTBatchID)batchID {
  172. return self.persistence.run("Reject batch", [&]() -> FSTMaybeDocumentDictionary * {
  173. FSTMutationBatch *toReject = [self.mutationQueue lookupMutationBatch:batchID];
  174. HARD_ASSERT(toReject, "Attempt to reject nonexistent batch!");
  175. FSTBatchID lastAcked = [self.mutationQueue highestAcknowledgedBatchID];
  176. HARD_ASSERT(batchID > lastAcked, "Acknowledged batches can't be rejected.");
  177. DocumentKeySet affected = [self removeMutationBatch:toReject];
  178. [self.mutationQueue performConsistencyCheck];
  179. return [self.localDocuments documentsForKeys:affected];
  180. });
  181. }
  182. - (nullable NSData *)lastStreamToken {
  183. return [self.mutationQueue lastStreamToken];
  184. }
  185. - (void)setLastStreamToken:(nullable NSData *)streamToken {
  186. self.persistence.run("Set stream token",
  187. [&]() { [self.mutationQueue setLastStreamToken:streamToken]; });
  188. }
  189. - (const SnapshotVersion &)lastRemoteSnapshotVersion {
  190. return [self.queryCache lastRemoteSnapshotVersion];
  191. }
  192. - (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent {
  193. return self.persistence.run("Apply remote event", [&]() -> FSTMaybeDocumentDictionary * {
  194. // TODO(gsoltis): move the sequence number into the reference delegate.
  195. FSTListenSequenceNumber sequenceNumber = self.persistence.currentSequenceNumber;
  196. id<FSTQueryCache> queryCache = self.queryCache;
  197. DocumentKeySet authoritativeUpdates;
  198. for (const auto &entry : remoteEvent.targetChanges) {
  199. FSTTargetID targetID = entry.first;
  200. FSTBoxedTargetID *boxedTargetID = @(targetID);
  201. FSTTargetChange *change = entry.second;
  202. // Do not ref/unref unassigned targetIDs - it may lead to leaks.
  203. FSTQueryData *queryData = self.targetIDs[boxedTargetID];
  204. if (!queryData) {
  205. continue;
  206. }
  207. // When a global snapshot contains updates (either add or modify) we can completely trust
  208. // these updates as authoritative and blindly apply them to our cache (as a defensive measure
  209. // to promote self-healing in the unfortunate case that our cache is ever somehow corrupted /
  210. // out-of-sync).
  211. //
  212. // If the document is only updated while removing it from a target then watch isn't obligated
  213. // to send the absolute latest version: it can send the first version that caused the document
  214. // not to match.
  215. for (const DocumentKey &key : change.addedDocuments) {
  216. authoritativeUpdates = authoritativeUpdates.insert(key);
  217. }
  218. for (const DocumentKey &key : change.modifiedDocuments) {
  219. authoritativeUpdates = authoritativeUpdates.insert(key);
  220. }
  221. [queryCache removeMatchingKeys:change.removedDocuments forTargetID:targetID];
  222. [queryCache addMatchingKeys:change.addedDocuments forTargetID:targetID];
  223. // Update the resume token if the change includes one. Don't clear any preexisting value.
  224. // Bump the sequence number as well, so that documents being removed now are ordered later
  225. // than documents that were previously removed from this target.
  226. NSData *resumeToken = change.resumeToken;
  227. if (resumeToken.length > 0) {
  228. queryData = [queryData queryDataByReplacingSnapshotVersion:remoteEvent.snapshotVersion
  229. resumeToken:resumeToken
  230. sequenceNumber:sequenceNumber];
  231. self.targetIDs[boxedTargetID] = queryData;
  232. [self.queryCache updateQueryData:queryData];
  233. }
  234. }
  235. // TODO(klimt): This could probably be an NSMutableDictionary.
  236. DocumentKeySet changedDocKeys;
  237. const DocumentKeySet &limboDocuments = remoteEvent.limboDocumentChanges;
  238. for (const auto &kv : remoteEvent.documentUpdates) {
  239. const DocumentKey &key = kv.first;
  240. FSTMaybeDocument *doc = kv.second;
  241. changedDocKeys = changedDocKeys.insert(key);
  242. FSTMaybeDocument *existingDoc = [self.remoteDocumentCache entryForKey:key];
  243. // If a document update isn't authoritative, make sure we don't apply an old document version
  244. // to the remote cache. We make an exception for SnapshotVersion.MIN which can happen for
  245. // manufactured events (e.g. in the case of a limbo document resolution failing).
  246. if (!existingDoc || doc.version == SnapshotVersion::None() ||
  247. authoritativeUpdates.contains(doc.key) || doc.version >= existingDoc.version) {
  248. [self.remoteDocumentCache addEntry:doc];
  249. } else {
  250. LOG_DEBUG(
  251. "FSTLocalStore Ignoring outdated watch update for %s. "
  252. "Current version: %s Watch version: %s",
  253. key.ToString(), existingDoc.version.timestamp().ToString(),
  254. doc.version.timestamp().ToString());
  255. }
  256. // If this was a limbo resolution, make sure we mark when it was accessed.
  257. if (limboDocuments.contains(key)) {
  258. [self.persistence.referenceDelegate limboDocumentUpdated:key];
  259. }
  260. }
  261. // HACK: The only reason we allow omitting snapshot version is so we can synthesize remote
  262. // events when we get permission denied errors while trying to resolve the state of a locally
  263. // cached document that is in limbo.
  264. const SnapshotVersion &lastRemoteVersion = [self.queryCache lastRemoteSnapshotVersion];
  265. const SnapshotVersion &remoteVersion = remoteEvent.snapshotVersion;
  266. if (remoteVersion != SnapshotVersion::None()) {
  267. HARD_ASSERT(remoteVersion >= lastRemoteVersion,
  268. "Watch stream reverted to previous snapshot?? (%s < %s)",
  269. remoteVersion.timestamp().ToString(), lastRemoteVersion.timestamp().ToString());
  270. [self.queryCache setLastRemoteSnapshotVersion:remoteVersion];
  271. }
  272. DocumentKeySet releasedWriteKeys = [self releaseHeldBatchResults];
  273. // Union the two key sets.
  274. DocumentKeySet keysToRecalc = changedDocKeys;
  275. for (const DocumentKey &key : releasedWriteKeys) {
  276. keysToRecalc = keysToRecalc.insert(key);
  277. }
  278. return [self.localDocuments documentsForKeys:keysToRecalc];
  279. });
  280. }
  281. - (void)notifyLocalViewChanges:(NSArray<FSTLocalViewChanges *> *)viewChanges {
  282. self.persistence.run("NotifyLocalViewChanges", [&]() {
  283. FSTReferenceSet *localViewReferences = self.localViewReferences;
  284. for (FSTLocalViewChanges *viewChange in viewChanges) {
  285. for (const DocumentKey &key : viewChange.removedKeys) {
  286. [self->_persistence.referenceDelegate removeReference:key];
  287. }
  288. [localViewReferences addReferencesToKeys:viewChange.addedKeys forID:viewChange.targetID];
  289. [localViewReferences removeReferencesToKeys:viewChange.removedKeys forID:viewChange.targetID];
  290. }
  291. });
  292. }
  293. - (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID {
  294. FSTMutationBatch *result =
  295. self.persistence.run("NextMutationBatchAfterBatchID", [&]() -> FSTMutationBatch * {
  296. return [self.mutationQueue nextMutationBatchAfterBatchID:batchID];
  297. });
  298. return result;
  299. }
  300. - (nullable FSTMaybeDocument *)readDocument:(const DocumentKey &)key {
  301. return self.persistence.run("ReadDocument", [&]() -> FSTMaybeDocument *_Nullable {
  302. return [self.localDocuments documentForKey:key];
  303. });
  304. }
  305. - (FSTQueryData *)allocateQuery:(FSTQuery *)query {
  306. FSTQueryData *queryData = self.persistence.run("Allocate query", [&]() -> FSTQueryData * {
  307. FSTQueryData *cached = [self.queryCache queryDataForQuery:query];
  308. // TODO(mcg): freshen last accessed date if cached exists?
  309. if (!cached) {
  310. cached = [[FSTQueryData alloc] initWithQuery:query
  311. targetID:_targetIDGenerator.NextId()
  312. listenSequenceNumber:self.persistence.currentSequenceNumber
  313. purpose:FSTQueryPurposeListen];
  314. [self.queryCache addQueryData:cached];
  315. }
  316. return cached;
  317. });
  318. // Sanity check to ensure that even when resuming a query it's not currently active.
  319. FSTBoxedTargetID *boxedTargetID = @(queryData.targetID);
  320. HARD_ASSERT(!self.targetIDs[boxedTargetID], "Tried to allocate an already allocated query: %s",
  321. query);
  322. self.targetIDs[boxedTargetID] = queryData;
  323. return queryData;
  324. }
  325. - (void)releaseQuery:(FSTQuery *)query {
  326. self.persistence.run("Release query", [&]() {
  327. FSTQueryData *queryData = [self.queryCache queryDataForQuery:query];
  328. HARD_ASSERT(queryData, "Tried to release nonexistent query: %s", query);
  329. [self.localViewReferences removeReferencesForID:queryData.targetID];
  330. [self.persistence.referenceDelegate removeTarget:queryData];
  331. [self.targetIDs removeObjectForKey:@(queryData.targetID)];
  332. // If this was the last watch target, then we won't get any more watch snapshots, so we should
  333. // release any held batch results.
  334. if ([self.targetIDs count] == 0) {
  335. [self releaseHeldBatchResults];
  336. }
  337. });
  338. }
  339. - (FSTDocumentDictionary *)executeQuery:(FSTQuery *)query {
  340. return self.persistence.run("ExecuteQuery", [&]() -> FSTDocumentDictionary * {
  341. return [self.localDocuments documentsMatchingQuery:query];
  342. });
  343. }
  344. - (DocumentKeySet)remoteDocumentKeysForTarget:(FSTTargetID)targetID {
  345. return self.persistence.run("RemoteDocumentKeysForTarget", [&]() -> DocumentKeySet {
  346. return [self.queryCache matchingKeysForTargetID:targetID];
  347. });
  348. }
  349. /**
  350. * Releases all the held mutation batches up to the current remote version received, and
  351. * applies their mutations to the docs in the remote documents cache.
  352. *
  353. * @return the set of keys of docs that were modified by those writes.
  354. */
  355. - (DocumentKeySet)releaseHeldBatchResults {
  356. NSMutableArray<FSTMutationBatchResult *> *toRelease = [NSMutableArray array];
  357. for (FSTMutationBatchResult *batchResult in self.heldBatchResults) {
  358. if (![self isRemoteUpToVersion:batchResult.commitVersion]) {
  359. break;
  360. }
  361. [toRelease addObject:batchResult];
  362. }
  363. if (toRelease.count == 0) {
  364. return DocumentKeySet{};
  365. } else {
  366. [self.heldBatchResults removeObjectsInRange:NSMakeRange(0, toRelease.count)];
  367. return [self releaseBatchResults:toRelease];
  368. }
  369. }
  370. - (BOOL)isRemoteUpToVersion:(const SnapshotVersion &)version {
  371. // If there are no watch targets, then we won't get remote snapshots, and are always "up-to-date."
  372. return version <= self.queryCache.lastRemoteSnapshotVersion || self.targetIDs.count == 0;
  373. }
  374. - (BOOL)shouldHoldBatchResultWithVersion:(const SnapshotVersion &)version {
  375. // Check if watcher isn't up to date or prior results are already held.
  376. return ![self isRemoteUpToVersion:version] || self.heldBatchResults.count > 0;
  377. }
  378. - (DocumentKeySet)releaseBatchResults:(NSArray<FSTMutationBatchResult *> *)batchResults {
  379. NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array];
  380. for (FSTMutationBatchResult *batchResult in batchResults) {
  381. [self applyBatchResult:batchResult];
  382. [batches addObject:batchResult.batch];
  383. }
  384. return [self removeMutationBatches:batches];
  385. }
  386. - (DocumentKeySet)removeMutationBatch:(FSTMutationBatch *)batch {
  387. return [self removeMutationBatches:@[ batch ]];
  388. }
  389. /** Removes all the mutation batches named in the given array. */
  390. - (DocumentKeySet)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches {
  391. DocumentKeySet affectedDocs;
  392. for (FSTMutationBatch *batch in batches) {
  393. for (FSTMutation *mutation in batch.mutations) {
  394. const DocumentKey &key = mutation.key;
  395. affectedDocs = affectedDocs.insert(key);
  396. }
  397. }
  398. [self.mutationQueue removeMutationBatches:batches];
  399. return affectedDocs;
  400. }
  401. - (void)applyBatchResult:(FSTMutationBatchResult *)batchResult {
  402. FSTMutationBatch *batch = batchResult.batch;
  403. DocumentKeySet docKeys = batch.keys;
  404. const DocumentVersionMap &versions = batchResult.docVersions;
  405. for (const DocumentKey &docKey : docKeys) {
  406. FSTMaybeDocument *_Nullable remoteDoc = [self.remoteDocumentCache entryForKey:docKey];
  407. FSTMaybeDocument *_Nullable doc = remoteDoc;
  408. auto ackVersionIter = versions.find(docKey);
  409. HARD_ASSERT(ackVersionIter != versions.end(),
  410. "docVersions should contain every doc in the write.");
  411. const SnapshotVersion &ackVersion = ackVersionIter->second;
  412. if (!doc || doc.version < ackVersion) {
  413. doc = [batch applyTo:doc documentKey:docKey mutationBatchResult:batchResult];
  414. if (!doc) {
  415. HARD_ASSERT(!remoteDoc, "Mutation batch %s applied to document %s resulted in nil.", batch,
  416. remoteDoc);
  417. } else {
  418. [self.remoteDocumentCache addEntry:doc];
  419. }
  420. }
  421. }
  422. }
  423. @end
  424. NS_ASSUME_NONNULL_END