Files
firebase-ios-sdk/Firestore/Source/Core/FSTView.mm
T
Gil 729b8d176c Move all Firestore Objective-C to Objective-C++ (#734)
* Move all Firestore files to Objective-C++
* Update project file references
* Don't use module imports from Objective-C++
* Use extern "C" for C-accessible globals
* Work around more stringent type checking in Objective-C++
  * NSMutableDictionary ivars aren't implicitly casted to NSDictionary
  * FSTMaybeDocument callback can't be passed a function that accepts
    FSTDocument
  * NSComparisonResult can't be multiplied by -1 without casting
* Add a #include <inttypes.h> where needed
* Avoid using C++ keywords as variables
* Remove #if __cplusplus guards
2018-01-31 11:23:55 -08:00

480 lines
18 KiB
Plaintext

/*
* 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];
}
- (NSUInteger)hash {
NSUInteger hash = self.type;
hash = hash * 31u + [self.key hash];
return hash;
}
@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);
}];
[self applyTargetChange:targetChange];
NSArray<FSTLimboDocumentChange *> *limboChanges = [self updateLimboDocuments];
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];
}
}
- (FSTViewChange *)applyChangedOnlineState:(FSTOnlineState)onlineState {
if (self.isCurrent && onlineState == FSTOnlineStateFailed) {
// If we're offline, set `current` to NO and then call applyChanges to refresh our syncState
// and generate an FSTViewChange as appropriate. We are guaranteed to get a new FSTTargetChange
// that sets `current` back to YES once the client is back online.
self.current = NO;
return
[self applyChangesToDocuments:[[FSTViewDocumentChanges alloc]
initWithDocumentSet:self.documentSet
changeSet:[FSTDocumentViewChangeSet changeSet]
needsRefill:NO
mutatedKeys:self.mutatedKeys]];
} else {
// No effect, just return a no-op FSTViewChange.
return [[FSTViewChange alloc] initWithSnapshot:nil 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 and current based on the given change.
*/
- (void)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;
}
}
}
/** Updates limboDocuments and returns any changes as FSTLimboDocumentChanges. */
- (NSArray<FSTLimboDocumentChange *> *)updateLimboDocuments {
// We can only determine limbo documents when we're in-sync with the server.
if (!self.isCurrent) {
return @[];
}
// TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents.
FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments;
self.limboDocuments = [FSTDocumentKeySet keySet];
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