FSTView.mm 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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/FSTView.h"
  17. #include <utility>
  18. #import "Firestore/Source/Core/FSTQuery.h"
  19. #import "Firestore/Source/Core/FSTViewSnapshot.h"
  20. #import "Firestore/Source/Model/FSTDocument.h"
  21. #import "Firestore/Source/Model/FSTDocumentSet.h"
  22. #import "Firestore/Source/Model/FSTFieldValue.h"
  23. #import "Firestore/Source/Remote/FSTRemoteEvent.h"
  24. #include "Firestore/core/src/firebase/firestore/model/document_key.h"
  25. #include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
  26. using firebase::firestore::model::DocumentKey;
  27. using firebase::firestore::model::DocumentKeySet;
  28. using firebase::firestore::model::MaybeDocumentMap;
  29. using firebase::firestore::model::OnlineState;
  30. NS_ASSUME_NONNULL_BEGIN
  31. #pragma mark - FSTViewDocumentChanges
  32. /** The result of applying a set of doc changes to a view. */
  33. @interface FSTViewDocumentChanges ()
  34. - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet
  35. changeSet:(FSTDocumentViewChangeSet *)changeSet
  36. needsRefill:(BOOL)needsRefill
  37. mutatedKeys:(DocumentKeySet)mutatedKeys NS_DESIGNATED_INITIALIZER;
  38. @end
  39. @implementation FSTViewDocumentChanges {
  40. DocumentKeySet _mutatedKeys;
  41. }
  42. - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet
  43. changeSet:(FSTDocumentViewChangeSet *)changeSet
  44. needsRefill:(BOOL)needsRefill
  45. mutatedKeys:(DocumentKeySet)mutatedKeys {
  46. self = [super init];
  47. if (self) {
  48. _documentSet = documentSet;
  49. _changeSet = changeSet;
  50. _needsRefill = needsRefill;
  51. _mutatedKeys = std::move(mutatedKeys);
  52. }
  53. return self;
  54. }
  55. - (const DocumentKeySet &)mutatedKeys {
  56. return _mutatedKeys;
  57. }
  58. @end
  59. #pragma mark - FSTLimboDocumentChange
  60. @interface FSTLimboDocumentChange ()
  61. + (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(DocumentKey)key;
  62. - (instancetype)initWithType:(FSTLimboDocumentChangeType)type
  63. key:(DocumentKey)key NS_DESIGNATED_INITIALIZER;
  64. @end
  65. @implementation FSTLimboDocumentChange {
  66. DocumentKey _key;
  67. }
  68. + (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(DocumentKey)key {
  69. return [[FSTLimboDocumentChange alloc] initWithType:type key:std::move(key)];
  70. }
  71. - (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(DocumentKey)key {
  72. self = [super init];
  73. if (self) {
  74. _type = type;
  75. _key = std::move(key);
  76. }
  77. return self;
  78. }
  79. - (const DocumentKey &)key {
  80. return _key;
  81. }
  82. - (BOOL)isEqual:(id)other {
  83. if (self == other) {
  84. return YES;
  85. }
  86. if (![other isKindOfClass:[FSTLimboDocumentChange class]]) {
  87. return NO;
  88. }
  89. FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other;
  90. return self.type == otherChange.type && self.key == otherChange.key;
  91. }
  92. - (NSUInteger)hash {
  93. NSUInteger hash = self.type;
  94. hash = hash * 31u + [self.key hash];
  95. return hash;
  96. }
  97. @end
  98. #pragma mark - FSTViewChange
  99. @interface FSTViewChange ()
  100. + (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot
  101. limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges;
  102. - (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot
  103. limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges
  104. NS_DESIGNATED_INITIALIZER;
  105. @end
  106. @implementation FSTViewChange
  107. + (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot
  108. limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges {
  109. return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges];
  110. }
  111. - (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot
  112. limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges {
  113. self = [super init];
  114. if (self) {
  115. _snapshot = snapshot;
  116. _limboChanges = limboChanges;
  117. }
  118. return self;
  119. }
  120. @end
  121. #pragma mark - FSTView
  122. static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1,
  123. FSTDocumentViewChangeType c2);
  124. @interface FSTView ()
  125. @property(nonatomic, strong, readonly) FSTQuery *query;
  126. @property(nonatomic, assign) FSTSyncState syncState;
  127. /**
  128. * A flag whether the view is current with the backend. A view is considered current after it
  129. * has seen the current flag from the backend and did not lose consistency within the watch stream
  130. * (e.g. because of an existence filter mismatch).
  131. */
  132. @property(nonatomic, assign, getter=isCurrent) BOOL current;
  133. @property(nonatomic, strong) FSTDocumentSet *documentSet;
  134. @end
  135. @implementation FSTView {
  136. /** Documents included in the remote target. */
  137. DocumentKeySet _syncedDocuments;
  138. /** Documents in the view but not in the remote target */
  139. DocumentKeySet _limboDocuments;
  140. /** Document Keys that have local changes. */
  141. DocumentKeySet _mutatedKeys;
  142. }
  143. - (instancetype)initWithQuery:(FSTQuery *)query remoteDocuments:(DocumentKeySet)remoteDocuments {
  144. self = [super init];
  145. if (self) {
  146. _query = query;
  147. _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator];
  148. _syncedDocuments = std::move(remoteDocuments);
  149. }
  150. return self;
  151. }
  152. - (const DocumentKeySet &)syncedDocuments {
  153. return _syncedDocuments;
  154. }
  155. - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap &)docChanges {
  156. return [self computeChangesWithDocuments:docChanges previousChanges:nil];
  157. }
  158. - (FSTViewDocumentChanges *)computeChangesWithDocuments:(const MaybeDocumentMap &)docChanges
  159. previousChanges:
  160. (nullable FSTViewDocumentChanges *)previousChanges {
  161. FSTDocumentViewChangeSet *changeSet =
  162. previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet];
  163. FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet;
  164. DocumentKeySet newMutatedKeys = previousChanges ? previousChanges.mutatedKeys : _mutatedKeys;
  165. DocumentKeySet oldMutatedKeys = _mutatedKeys;
  166. FSTDocumentSet *newDocumentSet = oldDocumentSet;
  167. BOOL needsRefill = NO;
  168. // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an
  169. // update moving a doc past the old limit) might mean there is some other document in the local
  170. // cache that either should come (1) between the old last limit doc and the new last document,
  171. // in the case of updates, or (2) after the new last document, in the case of deletes. So we
  172. // keep this doc at the old limit to compare the updates to.
  173. //
  174. // Note that this should never get used in a refill (when previousChanges is set), because there
  175. // will only be adds -- no deletes or updates.
  176. FSTDocument *_Nullable lastDocInLimit =
  177. (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument
  178. : nil;
  179. for (const auto &kv : docChanges) {
  180. const DocumentKey &key = kv.first;
  181. FSTMaybeDocument *maybeNewDoc = kv.second;
  182. FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key];
  183. FSTDocument *_Nullable newDoc = nil;
  184. if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) {
  185. newDoc = (FSTDocument *)maybeNewDoc;
  186. }
  187. if (newDoc) {
  188. HARD_ASSERT(key == newDoc.key, "Mismatching key in document changes: %s != %s", key,
  189. newDoc.key.ToString());
  190. if (![self.query matchesDocument:newDoc]) {
  191. newDoc = nil;
  192. }
  193. }
  194. BOOL oldDocHadPendingMutations = oldDoc && oldMutatedKeys.contains(oldDoc.key);
  195. // We only consider committed mutations for documents that were mutated during the lifetime of
  196. // the view.
  197. BOOL newDocHasPendingMutations =
  198. newDoc && (newDoc.hasLocalMutations ||
  199. (oldMutatedKeys.contains(newDoc.key) && newDoc.hasCommittedMutations));
  200. BOOL changeApplied = NO;
  201. // Calculate change
  202. if (oldDoc && newDoc) {
  203. BOOL docsEqual = [oldDoc.data isEqual:newDoc.data];
  204. if (!docsEqual) {
  205. if (![self shouldWaitForSyncedDocument:newDoc oldDocument:oldDoc]) {
  206. [changeSet addChange:[FSTDocumentViewChange
  207. changeWithDocument:newDoc
  208. type:FSTDocumentViewChangeTypeModified]];
  209. changeApplied = YES;
  210. if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) {
  211. // This doc moved from inside the limit to after the limit. That means there may be
  212. // some doc in the local cache that's actually less than this one.
  213. needsRefill = YES;
  214. }
  215. }
  216. } else if (oldDocHadPendingMutations != newDocHasPendingMutations) {
  217. [changeSet
  218. addChange:[FSTDocumentViewChange changeWithDocument:newDoc
  219. type:FSTDocumentViewChangeTypeMetadata]];
  220. changeApplied = YES;
  221. }
  222. } else if (!oldDoc && newDoc) {
  223. [changeSet
  224. addChange:[FSTDocumentViewChange changeWithDocument:newDoc
  225. type:FSTDocumentViewChangeTypeAdded]];
  226. changeApplied = YES;
  227. } else if (oldDoc && !newDoc) {
  228. [changeSet
  229. addChange:[FSTDocumentViewChange changeWithDocument:oldDoc
  230. type:FSTDocumentViewChangeTypeRemoved]];
  231. changeApplied = YES;
  232. if (lastDocInLimit) {
  233. // A doc was removed from a full limit query. We'll need to re-query from the local cache
  234. // to see if we know about some other doc that should be in the results.
  235. needsRefill = YES;
  236. }
  237. }
  238. if (changeApplied) {
  239. if (newDoc) {
  240. newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc];
  241. if (newDoc.hasLocalMutations) {
  242. newMutatedKeys = newMutatedKeys.insert(key);
  243. } else {
  244. newMutatedKeys = newMutatedKeys.erase(key);
  245. }
  246. } else {
  247. newDocumentSet = [newDocumentSet documentSetByRemovingKey:key];
  248. newMutatedKeys = newMutatedKeys.erase(key);
  249. }
  250. }
  251. }
  252. if (self.query.limit) {
  253. for (long i = newDocumentSet.count - self.query.limit; i > 0; --i) {
  254. FSTDocument *oldDoc = [newDocumentSet lastDocument];
  255. newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key];
  256. newMutatedKeys = newMutatedKeys.erase(oldDoc.key);
  257. [changeSet
  258. addChange:[FSTDocumentViewChange changeWithDocument:oldDoc
  259. type:FSTDocumentViewChangeTypeRemoved]];
  260. }
  261. }
  262. HARD_ASSERT(!needsRefill || !previousChanges,
  263. "View was refilled using docs that themselves needed refilling.");
  264. return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet
  265. changeSet:changeSet
  266. needsRefill:needsRefill
  267. mutatedKeys:newMutatedKeys];
  268. }
  269. - (BOOL)shouldWaitForSyncedDocument:(FSTDocument *)newDoc oldDocument:(FSTDocument *)oldDoc {
  270. // We suppress the initial change event for documents that were modified as part of a write
  271. // acknowledgment (e.g. when the value of a server transform is applied) as Watch will send us
  272. // the same document again. By suppressing the event, we only raise two user visible events (one
  273. // with `hasPendingWrites` and the final state of the document) instead of three (one with
  274. // `hasPendingWrites`, the modified document with `hasPendingWrites` and the final state of the
  275. // document).
  276. return (oldDoc.hasLocalMutations && newDoc.hasCommittedMutations && !newDoc.hasLocalMutations);
  277. }
  278. - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges {
  279. return [self applyChangesToDocuments:docChanges targetChange:nil];
  280. }
  281. - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges
  282. targetChange:(nullable FSTTargetChange *)targetChange {
  283. HARD_ASSERT(!docChanges.needsRefill, "Cannot apply changes that need a refill");
  284. FSTDocumentSet *oldDocuments = self.documentSet;
  285. self.documentSet = docChanges.documentSet;
  286. _mutatedKeys = docChanges.mutatedKeys;
  287. // Sort changes based on type and query comparator.
  288. NSArray<FSTDocumentViewChange *> *changes = [docChanges.changeSet changes];
  289. changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1,
  290. FSTDocumentViewChange *c2) {
  291. NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type);
  292. if (typeComparison != NSOrderedSame) {
  293. return typeComparison;
  294. }
  295. return self.query.comparator(c1.document, c2.document);
  296. }];
  297. [self applyTargetChange:targetChange];
  298. NSArray<FSTLimboDocumentChange *> *limboChanges = [self updateLimboDocuments];
  299. BOOL synced = _limboDocuments.empty() && self.isCurrent;
  300. FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal;
  301. BOOL syncStateChanged = newSyncState != self.syncState;
  302. self.syncState = newSyncState;
  303. if (changes.count == 0 && !syncStateChanged) {
  304. // No changes.
  305. return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges];
  306. } else {
  307. FSTViewSnapshot *snapshot =
  308. [[FSTViewSnapshot alloc] initWithQuery:self.query
  309. documents:docChanges.documentSet
  310. oldDocuments:oldDocuments
  311. documentChanges:changes
  312. fromCache:newSyncState == FSTSyncStateLocal
  313. mutatedKeys:docChanges.mutatedKeys
  314. syncStateChanged:syncStateChanged
  315. excludesMetadataChanges:NO];
  316. return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges];
  317. }
  318. }
  319. - (FSTViewChange *)applyChangedOnlineState:(OnlineState)onlineState {
  320. if (self.isCurrent && onlineState == OnlineState::Offline) {
  321. // If we're offline, set `current` to NO and then call applyChanges to refresh our syncState
  322. // and generate an FSTViewChange as appropriate. We are guaranteed to get a new FSTTargetChange
  323. // that sets `current` back to YES once the client is back online.
  324. self.current = NO;
  325. return
  326. [self applyChangesToDocuments:[[FSTViewDocumentChanges alloc]
  327. initWithDocumentSet:self.documentSet
  328. changeSet:[FSTDocumentViewChangeSet changeSet]
  329. needsRefill:NO
  330. mutatedKeys:_mutatedKeys]];
  331. } else {
  332. // No effect, just return a no-op FSTViewChange.
  333. return [[FSTViewChange alloc] initWithSnapshot:nil limboChanges:@[]];
  334. }
  335. }
  336. #pragma mark - Private methods
  337. /** Returns whether the doc for the given key should be in limbo. */
  338. - (BOOL)shouldBeLimboDocumentKey:(const DocumentKey &)key {
  339. // If the remote end says it's part of this query, it's not in limbo.
  340. if (_syncedDocuments.contains(key)) {
  341. return NO;
  342. }
  343. // The local store doesn't think it's a result, so it shouldn't be in limbo.
  344. if (![self.documentSet containsKey:key]) {
  345. return NO;
  346. }
  347. // If there are local changes to the doc, they might explain why the server doesn't know that it's
  348. // part of the query. So don't put it in limbo.
  349. // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific
  350. // query.
  351. if ([self.documentSet documentForKey:key].hasLocalMutations) {
  352. return NO;
  353. }
  354. // Everything else is in limbo.
  355. return YES;
  356. }
  357. /**
  358. * Updates syncedDocuments and current based on the given change.
  359. */
  360. - (void)applyTargetChange:(nullable FSTTargetChange *)targetChange {
  361. if (targetChange) {
  362. for (const DocumentKey &key : targetChange.addedDocuments) {
  363. _syncedDocuments = _syncedDocuments.insert(key);
  364. }
  365. for (const DocumentKey &key : targetChange.modifiedDocuments) {
  366. HARD_ASSERT(_syncedDocuments.find(key) != _syncedDocuments.end(),
  367. "Modified document %s not found in view.", key.ToString());
  368. }
  369. for (const DocumentKey &key : targetChange.removedDocuments) {
  370. _syncedDocuments = _syncedDocuments.erase(key);
  371. }
  372. self.current = targetChange.current;
  373. }
  374. }
  375. /** Updates limboDocuments and returns any changes as FSTLimboDocumentChanges. */
  376. - (NSArray<FSTLimboDocumentChange *> *)updateLimboDocuments {
  377. // We can only determine limbo documents when we're in-sync with the server.
  378. if (!self.isCurrent) {
  379. return @[];
  380. }
  381. // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents.
  382. DocumentKeySet oldLimboDocuments = std::move(_limboDocuments);
  383. _limboDocuments = DocumentKeySet{};
  384. for (FSTDocument *doc in self.documentSet.documentEnumerator) {
  385. if ([self shouldBeLimboDocumentKey:doc.key]) {
  386. _limboDocuments = _limboDocuments.insert(doc.key);
  387. }
  388. }
  389. // Diff the new limbo docs with the old limbo docs.
  390. NSMutableArray<FSTLimboDocumentChange *> *changes =
  391. [NSMutableArray arrayWithCapacity:(oldLimboDocuments.size() + _limboDocuments.size())];
  392. for (const DocumentKey &key : oldLimboDocuments) {
  393. if (!_limboDocuments.contains(key)) {
  394. [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved
  395. key:key]];
  396. }
  397. }
  398. for (const DocumentKey &key : _limboDocuments) {
  399. if (!oldLimboDocuments.contains(key)) {
  400. [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded
  401. key:key]];
  402. }
  403. }
  404. return changes;
  405. }
  406. @end
  407. static inline int DocumentViewChangeTypePosition(FSTDocumentViewChangeType changeType) {
  408. switch (changeType) {
  409. case FSTDocumentViewChangeTypeRemoved:
  410. return 0;
  411. case FSTDocumentViewChangeTypeAdded:
  412. return 1;
  413. case FSTDocumentViewChangeTypeModified:
  414. return 2;
  415. case FSTDocumentViewChangeTypeMetadata:
  416. // A metadata change is converted to a modified change at the public API layer. Since we sort
  417. // by document key and then change type, metadata and modified changes must be sorted
  418. // equivalently.
  419. return 2;
  420. default:
  421. HARD_FAIL("Unknown FSTDocumentViewChangeType %s", changeType);
  422. }
  423. }
  424. static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1,
  425. FSTDocumentViewChangeType c2) {
  426. int pos1 = DocumentViewChangeTypePosition(c1);
  427. int pos2 = DocumentViewChangeTypePosition(c2);
  428. if (pos1 == pos2) {
  429. return NSOrderedSame;
  430. } else if (pos1 < pos2) {
  431. return NSOrderedAscending;
  432. } else {
  433. return NSOrderedDescending;
  434. }
  435. }
  436. NS_ASSUME_NONNULL_END