| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451 |
- /*
- * Copyright 2017 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- #import "Firestore/Source/Core/FSTView.h"
- #import "Firestore/Source/Core/FSTQuery.h"
- #import "Firestore/Source/Core/FSTViewSnapshot.h"
- #import "Firestore/Source/Model/FSTDocument.h"
- #import "Firestore/Source/Model/FSTDocumentKey.h"
- #import "Firestore/Source/Model/FSTDocumentSet.h"
- #import "Firestore/Source/Model/FSTFieldValue.h"
- #import "Firestore/Source/Remote/FSTRemoteEvent.h"
- #import "Firestore/Source/Util/FSTAssert.h"
- NS_ASSUME_NONNULL_BEGIN
- #pragma mark - FSTViewDocumentChanges
- /** The result of applying a set of doc changes to a view. */
- @interface FSTViewDocumentChanges ()
- - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet
- changeSet:(FSTDocumentViewChangeSet *)changeSet
- needsRefill:(BOOL)needsRefill
- mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER;
- @end
- @implementation FSTViewDocumentChanges
- - (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet
- changeSet:(FSTDocumentViewChangeSet *)changeSet
- needsRefill:(BOOL)needsRefill
- mutatedKeys:(FSTDocumentKeySet *)mutatedKeys {
- self = [super init];
- if (self) {
- _documentSet = documentSet;
- _changeSet = changeSet;
- _needsRefill = needsRefill;
- _mutatedKeys = mutatedKeys;
- }
- return self;
- }
- @end
- #pragma mark - FSTLimboDocumentChange
- @interface FSTLimboDocumentChange ()
- + (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key;
- - (instancetype)initWithType:(FSTLimboDocumentChangeType)type
- key:(FSTDocumentKey *)key NS_DESIGNATED_INITIALIZER;
- @end
- @implementation FSTLimboDocumentChange
- + (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key {
- return [[FSTLimboDocumentChange alloc] initWithType:type key:key];
- }
- - (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key {
- self = [super init];
- if (self) {
- _type = type;
- _key = key;
- }
- return self;
- }
- - (BOOL)isEqual:(id)other {
- if (self == other) {
- return YES;
- }
- if (![other isKindOfClass:[FSTLimboDocumentChange class]]) {
- return NO;
- }
- FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other;
- return self.type == otherChange.type && [self.key isEqual:otherChange.key];
- }
- @end
- #pragma mark - FSTViewChange
- @interface FSTViewChange ()
- + (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot
- limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges;
- - (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot
- limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges
- NS_DESIGNATED_INITIALIZER;
- @end
- @implementation FSTViewChange
- + (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot
- limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges {
- return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges];
- }
- - (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot
- limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges {
- self = [super init];
- if (self) {
- _snapshot = snapshot;
- _limboChanges = limboChanges;
- }
- return self;
- }
- @end
- #pragma mark - FSTView
- static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1,
- FSTDocumentViewChangeType c2);
- @interface FSTView ()
- @property(nonatomic, strong, readonly) FSTQuery *query;
- @property(nonatomic, assign) FSTSyncState syncState;
- /**
- * A flag whether the view is current with the backend. A view is considered current after it
- * has seen the current flag from the backend and did not lose consistency within the watch stream
- * (e.g. because of an existence filter mismatch).
- */
- @property(nonatomic, assign, getter=isCurrent) BOOL current;
- @property(nonatomic, strong) FSTDocumentSet *documentSet;
- /** Documents included in the remote target. */
- @property(nonatomic, strong) FSTDocumentKeySet *syncedDocuments;
- /** Documents in the view but not in the remote target */
- @property(nonatomic, strong) FSTDocumentKeySet *limboDocuments;
- /** Document Keys that have local changes. */
- @property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys;
- @end
- @implementation FSTView
- - (instancetype)initWithQuery:(FSTQuery *)query
- remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments {
- self = [super init];
- if (self) {
- _query = query;
- _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator];
- _syncedDocuments = remoteDocuments;
- _limboDocuments = [FSTDocumentKeySet keySet];
- _mutatedKeys = [FSTDocumentKeySet keySet];
- }
- return self;
- }
- - (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges {
- return [self computeChangesWithDocuments:docChanges previousChanges:nil];
- }
- - (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges
- previousChanges:
- (nullable FSTViewDocumentChanges *)previousChanges {
- FSTDocumentViewChangeSet *changeSet =
- previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet];
- FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet;
- __block FSTDocumentKeySet *newMutatedKeys =
- previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys;
- __block FSTDocumentSet *newDocumentSet = oldDocumentSet;
- __block BOOL needsRefill = NO;
- // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an
- // update moving a doc past the old limit) might mean there is some other document in the local
- // cache that either should come (1) between the old last limit doc and the new last document,
- // in the case of updates, or (2) after the new last document, in the case of deletes. So we
- // keep this doc at the old limit to compare the updates to.
- //
- // Note that this should never get used in a refill (when previousChanges is set), because there
- // will only be adds -- no deletes or updates.
- FSTDocument *_Nullable lastDocInLimit =
- (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument
- : nil;
- [docChanges enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key,
- FSTMaybeDocument *maybeNewDoc, BOOL *stop) {
- FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key];
- FSTDocument *_Nullable newDoc = nil;
- if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) {
- newDoc = (FSTDocument *)maybeNewDoc;
- }
- if (newDoc) {
- FSTAssert([key isEqual:newDoc.key], @"Mismatching key in document changes: %@ != %@", key,
- newDoc.key);
- if (![self.query matchesDocument:newDoc]) {
- newDoc = nil;
- }
- }
- if (newDoc) {
- newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc];
- if (newDoc.hasLocalMutations) {
- newMutatedKeys = [newMutatedKeys setByAddingObject:key];
- } else {
- newMutatedKeys = [newMutatedKeys setByRemovingObject:key];
- }
- } else {
- newDocumentSet = [newDocumentSet documentSetByRemovingKey:key];
- newMutatedKeys = [newMutatedKeys setByRemovingObject:key];
- }
- // Calculate change
- if (oldDoc && newDoc) {
- BOOL docsEqual = [oldDoc.data isEqual:newDoc.data];
- if (!docsEqual || oldDoc.hasLocalMutations != newDoc.hasLocalMutations) {
- // only report a change if document actually changed.
- if (docsEqual) {
- [changeSet addChange:[FSTDocumentViewChange
- changeWithDocument:newDoc
- type:FSTDocumentViewChangeTypeMetadata]];
- } else {
- [changeSet addChange:[FSTDocumentViewChange
- changeWithDocument:newDoc
- type:FSTDocumentViewChangeTypeModified]];
- }
- if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) {
- // This doc moved from inside the limit to after the limit. That means there may be some
- // doc in the local cache that's actually less than this one.
- needsRefill = YES;
- }
- }
- } else if (!oldDoc && newDoc) {
- [changeSet
- addChange:[FSTDocumentViewChange changeWithDocument:newDoc
- type:FSTDocumentViewChangeTypeAdded]];
- } else if (oldDoc && !newDoc) {
- [changeSet
- addChange:[FSTDocumentViewChange changeWithDocument:oldDoc
- type:FSTDocumentViewChangeTypeRemoved]];
- if (lastDocInLimit) {
- // A doc was removed from a full limit query. We'll need to re-query from the local cache
- // to see if we know about some other doc that should be in the results.
- needsRefill = YES;
- }
- }
- }];
- if (self.query.limit) {
- // TODO(klimt): Make DocumentSet size be constant time.
- while (newDocumentSet.count > self.query.limit) {
- FSTDocument *oldDoc = [newDocumentSet lastDocument];
- newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key];
- [changeSet
- addChange:[FSTDocumentViewChange changeWithDocument:oldDoc
- type:FSTDocumentViewChangeTypeRemoved]];
- }
- }
- FSTAssert(!needsRefill || !previousChanges,
- @"View was refilled using docs that themselves needed refilling.");
- return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet
- changeSet:changeSet
- needsRefill:needsRefill
- mutatedKeys:newMutatedKeys];
- }
- - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges {
- return [self applyChangesToDocuments:docChanges targetChange:nil];
- }
- - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges
- targetChange:(nullable FSTTargetChange *)targetChange {
- FSTAssert(!docChanges.needsRefill, @"Cannot apply changes that need a refill");
- FSTDocumentSet *oldDocuments = self.documentSet;
- self.documentSet = docChanges.documentSet;
- self.mutatedKeys = docChanges.mutatedKeys;
- // Sort changes based on type and query comparator.
- NSArray<FSTDocumentViewChange *> *changes = [docChanges.changeSet changes];
- changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1,
- FSTDocumentViewChange *c2) {
- NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type);
- if (typeComparison != NSOrderedSame) {
- return typeComparison;
- }
- return self.query.comparator(c1.document, c2.document);
- }];
- NSArray<FSTLimboDocumentChange *> *limboChanges = [self applyTargetChange:targetChange];
- BOOL synced = self.limboDocuments.count == 0 && self.isCurrent;
- FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal;
- BOOL syncStateChanged = newSyncState != self.syncState;
- self.syncState = newSyncState;
- if (changes.count == 0 && !syncStateChanged) {
- // No changes.
- return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges];
- } else {
- FSTViewSnapshot *snapshot =
- [[FSTViewSnapshot alloc] initWithQuery:self.query
- documents:docChanges.documentSet
- oldDocuments:oldDocuments
- documentChanges:changes
- fromCache:newSyncState == FSTSyncStateLocal
- hasPendingWrites:!docChanges.mutatedKeys.isEmpty
- syncStateChanged:syncStateChanged];
- return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges];
- }
- }
- #pragma mark - Private methods
- /** Returns whether the doc for the given key should be in limbo. */
- - (BOOL)shouldBeLimboDocumentKey:(FSTDocumentKey *)key {
- // If the remote end says it's part of this query, it's not in limbo.
- if ([self.syncedDocuments containsObject:key]) {
- return NO;
- }
- // The local store doesn't think it's a result, so it shouldn't be in limbo.
- if (![self.documentSet containsKey:key]) {
- return NO;
- }
- // If there are local changes to the doc, they might explain why the server doesn't know that it's
- // part of the query. So don't put it in limbo.
- // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific
- // query.
- if ([self.documentSet documentForKey:key].hasLocalMutations) {
- return NO;
- }
- // Everything else is in limbo.
- return YES;
- }
- /**
- * Updates syncedDocuments, isAcked, and limbo docs based on the given change.
- * @return the list of changes to which docs are in limbo.
- */
- - (NSArray<FSTLimboDocumentChange *> *)applyTargetChange:(nullable FSTTargetChange *)targetChange {
- if (targetChange) {
- FSTTargetMapping *targetMapping = targetChange.mapping;
- if ([targetMapping isKindOfClass:[FSTResetMapping class]]) {
- self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents;
- } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) {
- [((FSTUpdateMapping *)targetMapping).addedDocuments
- enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
- self.syncedDocuments = [self.syncedDocuments setByAddingObject:key];
- }];
- [((FSTUpdateMapping *)targetMapping).removedDocuments
- enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
- self.syncedDocuments = [self.syncedDocuments setByRemovingObject:key];
- }];
- }
- switch (targetChange.currentStatusUpdate) {
- case FSTCurrentStatusUpdateMarkCurrent:
- self.current = YES;
- break;
- case FSTCurrentStatusUpdateMarkNotCurrent:
- self.current = NO;
- break;
- case FSTCurrentStatusUpdateNone:
- break;
- }
- }
- // Recompute the set of limbo docs.
- // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents.
- FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments;
- self.limboDocuments = [FSTDocumentKeySet keySet];
- if (self.isCurrent) {
- for (FSTDocument *doc in self.documentSet.documentEnumerator) {
- if ([self shouldBeLimboDocumentKey:doc.key]) {
- self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key];
- }
- }
- }
- // Diff the new limbo docs with the old limbo docs.
- NSMutableArray<FSTLimboDocumentChange *> *changes =
- [NSMutableArray arrayWithCapacity:(oldLimboDocuments.count + self.limboDocuments.count)];
- [oldLimboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
- if (![self.limboDocuments containsObject:key]) {
- [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved
- key:key]];
- }
- }];
- [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
- if (![oldLimboDocuments containsObject:key]) {
- [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded
- key:key]];
- }
- }];
- return changes;
- }
- @end
- static inline int DocumentViewChangeTypePosition(FSTDocumentViewChangeType changeType) {
- switch (changeType) {
- case FSTDocumentViewChangeTypeRemoved:
- return 0;
- case FSTDocumentViewChangeTypeAdded:
- return 1;
- case FSTDocumentViewChangeTypeModified:
- return 2;
- case FSTDocumentViewChangeTypeMetadata:
- // A metadata change is converted to a modified change at the public API layer. Since we sort
- // by document key and then change type, metadata and modified changes must be sorted
- // equivalently.
- return 2;
- default:
- FSTCFail(@"Unknown FSTDocumentViewChangeType %lu", (unsigned long)changeType);
- }
- }
- static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1,
- FSTDocumentViewChangeType c2) {
- int pos1 = DocumentViewChangeTypePosition(c1);
- int pos2 = DocumentViewChangeTypePosition(c2);
- if (pos1 == pos2) {
- return NSOrderedSame;
- } else if (pos1 < pos2) {
- return NSOrderedAscending;
- } else {
- return NSOrderedDescending;
- }
- }
- NS_ASSUME_NONNULL_END
|