FSTView.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  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. #import "Firestore/Source/Core/FSTQuery.h"
  18. #import "Firestore/Source/Core/FSTViewSnapshot.h"
  19. #import "Firestore/Source/Model/FSTDocument.h"
  20. #import "Firestore/Source/Model/FSTDocumentKey.h"
  21. #import "Firestore/Source/Model/FSTDocumentSet.h"
  22. #import "Firestore/Source/Model/FSTFieldValue.h"
  23. #import "Firestore/Source/Remote/FSTRemoteEvent.h"
  24. #import "Firestore/Source/Util/FSTAssert.h"
  25. NS_ASSUME_NONNULL_BEGIN
  26. #pragma mark - FSTViewDocumentChanges
  27. /** The result of applying a set of doc changes to a view. */
  28. @interface FSTViewDocumentChanges ()
  29. - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet
  30. changeSet:(FSTDocumentViewChangeSet *)changeSet
  31. needsRefill:(BOOL)needsRefill
  32. mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER;
  33. @end
  34. @implementation FSTViewDocumentChanges
  35. - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet
  36. changeSet:(FSTDocumentViewChangeSet *)changeSet
  37. needsRefill:(BOOL)needsRefill
  38. mutatedKeys:(FSTDocumentKeySet *)mutatedKeys {
  39. self = [super init];
  40. if (self) {
  41. _documentSet = documentSet;
  42. _changeSet = changeSet;
  43. _needsRefill = needsRefill;
  44. _mutatedKeys = mutatedKeys;
  45. }
  46. return self;
  47. }
  48. @end
  49. #pragma mark - FSTLimboDocumentChange
  50. @interface FSTLimboDocumentChange ()
  51. + (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key;
  52. - (instancetype)initWithType:(FSTLimboDocumentChangeType)type
  53. key:(FSTDocumentKey *)key NS_DESIGNATED_INITIALIZER;
  54. @end
  55. @implementation FSTLimboDocumentChange
  56. + (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key {
  57. return [[FSTLimboDocumentChange alloc] initWithType:type key:key];
  58. }
  59. - (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key {
  60. self = [super init];
  61. if (self) {
  62. _type = type;
  63. _key = key;
  64. }
  65. return self;
  66. }
  67. - (BOOL)isEqual:(id)other {
  68. if (self == other) {
  69. return YES;
  70. }
  71. if (![other isKindOfClass:[FSTLimboDocumentChange class]]) {
  72. return NO;
  73. }
  74. FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other;
  75. return self.type == otherChange.type && [self.key isEqual:otherChange.key];
  76. }
  77. @end
  78. #pragma mark - FSTViewChange
  79. @interface FSTViewChange ()
  80. + (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot
  81. limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges;
  82. - (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot
  83. limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges
  84. NS_DESIGNATED_INITIALIZER;
  85. @end
  86. @implementation FSTViewChange
  87. + (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot
  88. limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges {
  89. return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges];
  90. }
  91. - (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot
  92. limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges {
  93. self = [super init];
  94. if (self) {
  95. _snapshot = snapshot;
  96. _limboChanges = limboChanges;
  97. }
  98. return self;
  99. }
  100. @end
  101. #pragma mark - FSTView
  102. static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1,
  103. FSTDocumentViewChangeType c2);
  104. @interface FSTView ()
  105. @property(nonatomic, strong, readonly) FSTQuery *query;
  106. @property(nonatomic, assign) FSTSyncState syncState;
  107. /**
  108. * A flag whether the view is current with the backend. A view is considered current after it
  109. * has seen the current flag from the backend and did not lose consistency within the watch stream
  110. * (e.g. because of an existence filter mismatch).
  111. */
  112. @property(nonatomic, assign, getter=isCurrent) BOOL current;
  113. @property(nonatomic, strong) FSTDocumentSet *documentSet;
  114. /** Documents included in the remote target. */
  115. @property(nonatomic, strong) FSTDocumentKeySet *syncedDocuments;
  116. /** Documents in the view but not in the remote target */
  117. @property(nonatomic, strong) FSTDocumentKeySet *limboDocuments;
  118. /** Document Keys that have local changes. */
  119. @property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys;
  120. @end
  121. @implementation FSTView
  122. - (instancetype)initWithQuery:(FSTQuery *)query
  123. remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments {
  124. self = [super init];
  125. if (self) {
  126. _query = query;
  127. _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator];
  128. _syncedDocuments = remoteDocuments;
  129. _limboDocuments = [FSTDocumentKeySet keySet];
  130. _mutatedKeys = [FSTDocumentKeySet keySet];
  131. }
  132. return self;
  133. }
  134. - (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges {
  135. return [self computeChangesWithDocuments:docChanges previousChanges:nil];
  136. }
  137. - (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges
  138. previousChanges:
  139. (nullable FSTViewDocumentChanges *)previousChanges {
  140. FSTDocumentViewChangeSet *changeSet =
  141. previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet];
  142. FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet;
  143. __block FSTDocumentKeySet *newMutatedKeys =
  144. previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys;
  145. __block FSTDocumentSet *newDocumentSet = oldDocumentSet;
  146. __block BOOL needsRefill = NO;
  147. // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an
  148. // update moving a doc past the old limit) might mean there is some other document in the local
  149. // cache that either should come (1) between the old last limit doc and the new last document,
  150. // in the case of updates, or (2) after the new last document, in the case of deletes. So we
  151. // keep this doc at the old limit to compare the updates to.
  152. //
  153. // Note that this should never get used in a refill (when previousChanges is set), because there
  154. // will only be adds -- no deletes or updates.
  155. FSTDocument *_Nullable lastDocInLimit =
  156. (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument
  157. : nil;
  158. [docChanges enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key,
  159. FSTMaybeDocument *maybeNewDoc, BOOL *stop) {
  160. FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key];
  161. FSTDocument *_Nullable newDoc = nil;
  162. if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) {
  163. newDoc = (FSTDocument *)maybeNewDoc;
  164. }
  165. if (newDoc) {
  166. FSTAssert([key isEqual:newDoc.key], @"Mismatching key in document changes: %@ != %@", key,
  167. newDoc.key);
  168. if (![self.query matchesDocument:newDoc]) {
  169. newDoc = nil;
  170. }
  171. }
  172. if (newDoc) {
  173. newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc];
  174. if (newDoc.hasLocalMutations) {
  175. newMutatedKeys = [newMutatedKeys setByAddingObject:key];
  176. } else {
  177. newMutatedKeys = [newMutatedKeys setByRemovingObject:key];
  178. }
  179. } else {
  180. newDocumentSet = [newDocumentSet documentSetByRemovingKey:key];
  181. newMutatedKeys = [newMutatedKeys setByRemovingObject:key];
  182. }
  183. // Calculate change
  184. if (oldDoc && newDoc) {
  185. BOOL docsEqual = [oldDoc.data isEqual:newDoc.data];
  186. if (!docsEqual || oldDoc.hasLocalMutations != newDoc.hasLocalMutations) {
  187. // only report a change if document actually changed.
  188. if (docsEqual) {
  189. [changeSet addChange:[FSTDocumentViewChange
  190. changeWithDocument:newDoc
  191. type:FSTDocumentViewChangeTypeMetadata]];
  192. } else {
  193. [changeSet addChange:[FSTDocumentViewChange
  194. changeWithDocument:newDoc
  195. type:FSTDocumentViewChangeTypeModified]];
  196. }
  197. if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) {
  198. // This doc moved from inside the limit to after the limit. That means there may be some
  199. // doc in the local cache that's actually less than this one.
  200. needsRefill = YES;
  201. }
  202. }
  203. } else if (!oldDoc && newDoc) {
  204. [changeSet
  205. addChange:[FSTDocumentViewChange changeWithDocument:newDoc
  206. type:FSTDocumentViewChangeTypeAdded]];
  207. } else if (oldDoc && !newDoc) {
  208. [changeSet
  209. addChange:[FSTDocumentViewChange changeWithDocument:oldDoc
  210. type:FSTDocumentViewChangeTypeRemoved]];
  211. if (lastDocInLimit) {
  212. // A doc was removed from a full limit query. We'll need to re-query from the local cache
  213. // to see if we know about some other doc that should be in the results.
  214. needsRefill = YES;
  215. }
  216. }
  217. }];
  218. if (self.query.limit) {
  219. // TODO(klimt): Make DocumentSet size be constant time.
  220. while (newDocumentSet.count > self.query.limit) {
  221. FSTDocument *oldDoc = [newDocumentSet lastDocument];
  222. newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key];
  223. [changeSet
  224. addChange:[FSTDocumentViewChange changeWithDocument:oldDoc
  225. type:FSTDocumentViewChangeTypeRemoved]];
  226. }
  227. }
  228. FSTAssert(!needsRefill || !previousChanges,
  229. @"View was refilled using docs that themselves needed refilling.");
  230. return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet
  231. changeSet:changeSet
  232. needsRefill:needsRefill
  233. mutatedKeys:newMutatedKeys];
  234. }
  235. - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges {
  236. return [self applyChangesToDocuments:docChanges targetChange:nil];
  237. }
  238. - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges
  239. targetChange:(nullable FSTTargetChange *)targetChange {
  240. FSTAssert(!docChanges.needsRefill, @"Cannot apply changes that need a refill");
  241. FSTDocumentSet *oldDocuments = self.documentSet;
  242. self.documentSet = docChanges.documentSet;
  243. self.mutatedKeys = docChanges.mutatedKeys;
  244. // Sort changes based on type and query comparator.
  245. NSArray<FSTDocumentViewChange *> *changes = [docChanges.changeSet changes];
  246. changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1,
  247. FSTDocumentViewChange *c2) {
  248. NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type);
  249. if (typeComparison != NSOrderedSame) {
  250. return typeComparison;
  251. }
  252. return self.query.comparator(c1.document, c2.document);
  253. }];
  254. NSArray<FSTLimboDocumentChange *> *limboChanges = [self applyTargetChange:targetChange];
  255. BOOL synced = self.limboDocuments.count == 0 && self.isCurrent;
  256. FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal;
  257. BOOL syncStateChanged = newSyncState != self.syncState;
  258. self.syncState = newSyncState;
  259. if (changes.count == 0 && !syncStateChanged) {
  260. // No changes.
  261. return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges];
  262. } else {
  263. FSTViewSnapshot *snapshot =
  264. [[FSTViewSnapshot alloc] initWithQuery:self.query
  265. documents:docChanges.documentSet
  266. oldDocuments:oldDocuments
  267. documentChanges:changes
  268. fromCache:newSyncState == FSTSyncStateLocal
  269. hasPendingWrites:!docChanges.mutatedKeys.isEmpty
  270. syncStateChanged:syncStateChanged];
  271. return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges];
  272. }
  273. }
  274. #pragma mark - Private methods
  275. /** Returns whether the doc for the given key should be in limbo. */
  276. - (BOOL)shouldBeLimboDocumentKey:(FSTDocumentKey *)key {
  277. // If the remote end says it's part of this query, it's not in limbo.
  278. if ([self.syncedDocuments containsObject:key]) {
  279. return NO;
  280. }
  281. // The local store doesn't think it's a result, so it shouldn't be in limbo.
  282. if (![self.documentSet containsKey:key]) {
  283. return NO;
  284. }
  285. // If there are local changes to the doc, they might explain why the server doesn't know that it's
  286. // part of the query. So don't put it in limbo.
  287. // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific
  288. // query.
  289. if ([self.documentSet documentForKey:key].hasLocalMutations) {
  290. return NO;
  291. }
  292. // Everything else is in limbo.
  293. return YES;
  294. }
  295. /**
  296. * Updates syncedDocuments, isAcked, and limbo docs based on the given change.
  297. * @return the list of changes to which docs are in limbo.
  298. */
  299. - (NSArray<FSTLimboDocumentChange *> *)applyTargetChange:(nullable FSTTargetChange *)targetChange {
  300. if (targetChange) {
  301. FSTTargetMapping *targetMapping = targetChange.mapping;
  302. if ([targetMapping isKindOfClass:[FSTResetMapping class]]) {
  303. self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents;
  304. } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) {
  305. [((FSTUpdateMapping *)targetMapping).addedDocuments
  306. enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
  307. self.syncedDocuments = [self.syncedDocuments setByAddingObject:key];
  308. }];
  309. [((FSTUpdateMapping *)targetMapping).removedDocuments
  310. enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
  311. self.syncedDocuments = [self.syncedDocuments setByRemovingObject:key];
  312. }];
  313. }
  314. switch (targetChange.currentStatusUpdate) {
  315. case FSTCurrentStatusUpdateMarkCurrent:
  316. self.current = YES;
  317. break;
  318. case FSTCurrentStatusUpdateMarkNotCurrent:
  319. self.current = NO;
  320. break;
  321. case FSTCurrentStatusUpdateNone:
  322. break;
  323. }
  324. }
  325. // Recompute the set of limbo docs.
  326. // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents.
  327. FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments;
  328. self.limboDocuments = [FSTDocumentKeySet keySet];
  329. if (self.isCurrent) {
  330. for (FSTDocument *doc in self.documentSet.documentEnumerator) {
  331. if ([self shouldBeLimboDocumentKey:doc.key]) {
  332. self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key];
  333. }
  334. }
  335. }
  336. // Diff the new limbo docs with the old limbo docs.
  337. NSMutableArray<FSTLimboDocumentChange *> *changes =
  338. [NSMutableArray arrayWithCapacity:(oldLimboDocuments.count + self.limboDocuments.count)];
  339. [oldLimboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
  340. if (![self.limboDocuments containsObject:key]) {
  341. [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved
  342. key:key]];
  343. }
  344. }];
  345. [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
  346. if (![oldLimboDocuments containsObject:key]) {
  347. [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded
  348. key:key]];
  349. }
  350. }];
  351. return changes;
  352. }
  353. @end
  354. static inline int DocumentViewChangeTypePosition(FSTDocumentViewChangeType changeType) {
  355. switch (changeType) {
  356. case FSTDocumentViewChangeTypeRemoved:
  357. return 0;
  358. case FSTDocumentViewChangeTypeAdded:
  359. return 1;
  360. case FSTDocumentViewChangeTypeModified:
  361. return 2;
  362. case FSTDocumentViewChangeTypeMetadata:
  363. // A metadata change is converted to a modified change at the public API layer. Since we sort
  364. // by document key and then change type, metadata and modified changes must be sorted
  365. // equivalently.
  366. return 2;
  367. default:
  368. FSTCFail(@"Unknown FSTDocumentViewChangeType %lu", (unsigned long)changeType);
  369. }
  370. }
  371. static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1,
  372. FSTDocumentViewChangeType c2) {
  373. int pos1 = DocumentViewChangeTypePosition(c1);
  374. int pos2 = DocumentViewChangeTypePosition(c2);
  375. if (pos1 == pos2) {
  376. return NSOrderedSame;
  377. } else if (pos1 < pos2) {
  378. return NSOrderedAscending;
  379. } else {
  380. return NSOrderedDescending;
  381. }
  382. }
  383. NS_ASSUME_NONNULL_END