diff options
Diffstat (limited to 'Firestore/Source')
42 files changed, 988 insertions, 253 deletions
diff --git a/Firestore/Source/API/FIRCollectionReference.mm b/Firestore/Source/API/FIRCollectionReference.mm index 92cccc6..70a14c2 100644 --- a/Firestore/Source/API/FIRCollectionReference.mm +++ b/Firestore/Source/API/FIRCollectionReference.mm @@ -15,6 +15,7 @@ */ #import "FIRCollectionReference.h" +#import "FIRFirestore.h" #include "Firestore/core/src/firebase/firestore/util/autoid.h" @@ -65,6 +66,29 @@ NS_ASSUME_NONNULL_BEGIN FSTFail(@"Use FIRCollectionReference initWithPath: initializer."); } +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToReference:other]; +} + +- (BOOL)isEqualToReference:(nullable FIRCollectionReference *)reference { + if (self == reference) return YES; + if (reference == nil) return NO; + if (self.firestore != reference.firestore && ![self.firestore isEqual:reference.firestore]) + return NO; + if (self.query != reference.query && ![self.query isEqual:reference.query]) return NO; + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.query hash]; + return hash; +} + - (NSString *)collectionID { return [self.query.path lastSegment]; } diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m index 970dc90..d1d9999 100644 --- a/Firestore/Source/API/FIRDocumentChange.m +++ b/Firestore/Source/API/FIRDocumentChange.m @@ -57,11 +57,11 @@ NS_ASSUME_NONNULL_BEGIN NSUInteger index = 0; NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array]; for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRDocumentSnapshot *document = - [FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache]; + FIRQueryDocumentSnapshot *document = + [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; FSTAssert(change.type == FSTDocumentViewChangeTypeAdded, @"Invalid event type for first snapshot"); FSTAssert(!lastDocument || @@ -79,11 +79,11 @@ NS_ASSUME_NONNULL_BEGIN FSTDocumentSet *indexTracker = snapshot.oldDocuments; NSMutableArray<FIRDocumentChange *> *changes = [NSMutableArray array]; for (FSTDocumentViewChange *change in snapshot.documentChanges) { - FIRDocumentSnapshot *document = - [FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:change.document.key - document:change.document - fromCache:snapshot.isFromCache]; + FIRQueryDocumentSnapshot *document = + [FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:change.document.key + document:change.document + fromCache:snapshot.isFromCache]; NSUInteger oldIndex = NSNotFound; NSUInteger newIndex = NSNotFound; @@ -112,7 +112,7 @@ NS_ASSUME_NONNULL_BEGIN @implementation FIRDocumentChange - (instancetype)initWithType:(FIRDocumentChangeType)type - document:(FIRDocumentSnapshot *)document + document:(FIRQueryDocumentSnapshot *)document oldIndex:(NSUInteger)oldIndex newIndex:(NSUInteger)newIndex { if (self = [super init]) { diff --git a/Firestore/Source/API/FIRDocumentReference.m b/Firestore/Source/API/FIRDocumentReference.m index 1c80ea9..87e6631 100644 --- a/Firestore/Source/API/FIRDocumentReference.m +++ b/Firestore/Source/API/FIRDocumentReference.m @@ -48,6 +48,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges NS_DESIGNATED_INITIALIZER; +@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; + @end @implementation FIRDocumentListenOptions @@ -114,7 +116,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isEqual:(nullable id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self isEqualToReference:other]; } diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m index b78472e..358ddac 100644 --- a/Firestore/Source/API/FIRDocumentSnapshot.m +++ b/Firestore/Source/API/FIRDocumentSnapshot.m @@ -20,11 +20,13 @@ #import "Firestore/Source/API/FIRFieldPath+Internal.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRSnapshotMetadata+Internal.h" +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" #import "Firestore/Source/Model/FSTDatabaseID.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTDocumentKey.h" #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/Model/FSTPath.h" +#import "Firestore/Source/Util/FSTAssert.h" #import "Firestore/Source/Util/FSTUsageValidation.h" NS_ASSUME_NONNULL_BEGIN @@ -76,6 +78,37 @@ NS_ASSUME_NONNULL_BEGIN return self; } +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + // self class could be FIRDocumentSnapshot or subtype. So we compare with base type explicitly. + if (![other isKindOfClass:[FIRDocumentSnapshot class]]) return NO; + + return [self isEqualToSnapshot:other]; +} + +- (BOOL)isEqualToSnapshot:(nullable FIRDocumentSnapshot *)snapshot { + if (self == snapshot) return YES; + if (snapshot == nil) return NO; + if (self.firestore != snapshot.firestore && ![self.firestore isEqual:snapshot.firestore]) + return NO; + if (self.internalKey != snapshot.internalKey && ![self.internalKey isEqual:snapshot.internalKey]) + return NO; + if (self.internalDocument != snapshot.internalDocument && + ![self.internalDocument isEqual:snapshot.internalDocument]) + return NO; + if (self.fromCache != snapshot.fromCache) return NO; + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.internalKey hash]; + hash = hash * 31u + [self.internalDocument hash]; + hash = hash * 31u + (self.fromCache ? 1 : 0); + return hash; +} + @dynamic exists; - (BOOL)exists { @@ -99,40 +132,48 @@ NS_ASSUME_NONNULL_BEGIN return _cachedMetadata; } -- (NSDictionary<NSString *, id> *)data { - FSTDocument *document = self.internalDocument; - - if (!document) { - FSTThrowInvalidUsage( - @"NonExistentDocumentException", - @"Document '%@' doesn't exist. " - @"Check document.exists to make sure the document exists before calling document.data.", - self.internalKey); - } +- (nullable NSDictionary<NSString *, id> *)data { + return [self dataWithOptions:[FIRSnapshotOptions defaultOptions]]; +} - return [self convertedObject:[self.internalDocument data]]; +- (nullable NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options { + return self.internalDocument == nil + ? nil + : [self convertedObject:[self.internalDocument data] + options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; } -- (nullable id)objectForKeyedSubscript:(id)key { +- (nullable id)valueForField:(id)field { + return [self valueForField:field options:[FIRSnapshotOptions defaultOptions]]; +} + +- (nullable id)valueForField:(id)field options:(FIRSnapshotOptions *)options { FIRFieldPath *fieldPath; - if ([key isKindOfClass:[NSString class]]) { - fieldPath = [FIRFieldPath pathWithDotSeparatedString:key]; - } else if ([key isKindOfClass:[FIRFieldPath class]]) { - fieldPath = key; + if ([field isKindOfClass:[NSString class]]) { + fieldPath = [FIRFieldPath pathWithDotSeparatedString:field]; + } else if ([field isKindOfClass:[FIRFieldPath class]]) { + fieldPath = field; } else { FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); } FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; - return [self convertedValue:fieldValue]; + return fieldValue == nil + ? nil + : [self convertedValue:fieldValue + options:[FSTFieldValueOptions optionsForSnapshotOptions:options]]; } -- (id)convertedValue:(FSTFieldValue *)value { +- (nullable id)objectForKeyedSubscript:(id)key { + return [self valueForField:key]; +} + +- (id)convertedValue:(FSTFieldValue *)value options:(FSTFieldValueOptions *)options { if ([value isKindOfClass:[FSTObjectValue class]]) { - return [self convertedObject:(FSTObjectValue *)value]; + return [self convertedObject:(FSTObjectValue *)value options:options]; } else if ([value isKindOfClass:[FSTArrayValue class]]) { - return [self convertedArray:(FSTArrayValue *)value]; + return [self convertedArray:(FSTArrayValue *)value options:options]; } else if ([value isKindOfClass:[FSTReferenceValue class]]) { FSTReferenceValue *ref = (FSTReferenceValue *)value; FSTDatabaseID *refDatabase = ref.databaseID; @@ -146,30 +187,69 @@ NS_ASSUME_NONNULL_BEGIN self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID, database.databaseID); } - return [FIRDocumentReference referenceWithKey:ref.value firestore:self.firestore]; + return [FIRDocumentReference referenceWithKey:[ref valueWithOptions:options] + firestore:self.firestore]; } else { - return value.value; + return [value valueWithOptions:options]; } } -- (NSDictionary<NSString *, id> *)convertedObject:(FSTObjectValue *)objectValue { +- (NSDictionary<NSString *, id> *)convertedObject:(FSTObjectValue *)objectValue + options:(FSTFieldValueOptions *)options { NSMutableDictionary *result = [NSMutableDictionary dictionary]; [objectValue.internalValue enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { - result[key] = [self convertedValue:value]; + result[key] = [self convertedValue:value options:options]; }]; return result; } -- (NSArray<id> *)convertedArray:(FSTArrayValue *)arrayValue { +- (NSArray<id> *)convertedArray:(FSTArrayValue *)arrayValue + options:(FSTFieldValueOptions *)options { NSArray<FSTFieldValue *> *internalValue = arrayValue.internalValue; NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count]; [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) { - [result addObject:[self convertedValue:value]]; + [result addObject:[self convertedValue:value options:options]]; }]; return result; } @end +@interface FIRQueryDocumentSnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(FSTDocument *)document + fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRQueryDocumentSnapshot + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(FSTDocument *)document + fromCache:(BOOL)fromCache { + self = [super initWithFirestore:firestore + documentKey:documentKey + document:document + fromCache:fromCache]; + return self; +} + +- (NSDictionary<NSString *, id> *)data { + NSDictionary<NSString *, id> *data = [super data]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +- (NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options { + NSDictionary<NSString *, id> *data = [super dataWithOptions:options]; + FSTAssert(data, @"Document in a QueryDocumentSnapshot should exist"); + return data; +} + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath.m b/Firestore/Source/API/FIRFieldPath.m index d0a70c0..f4e532f 100644 --- a/Firestore/Source/API/FIRFieldPath.m +++ b/Firestore/Source/API/FIRFieldPath.m @@ -80,7 +80,7 @@ NS_ASSUME_NONNULL_BEGIN return [[[self class] alloc] initPrivate:self.internalValue]; } -- (BOOL)isEqual:(id)object { +- (BOOL)isEqual:(nullable id)object { if (self == object) { return YES; } diff --git a/Firestore/Source/API/FIRFirestore.m b/Firestore/Source/API/FIRFirestore.m index 7814ce1..9df3711 100644 --- a/Firestore/Source/API/FIRFirestore.m +++ b/Firestore/Source/API/FIRFirestore.m @@ -50,13 +50,17 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; @property(nonatomic, strong) id<FSTCredentialsProvider> credentialsProvider; @property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; -@property(nonatomic, strong) FSTFirestoreClient *client; +// Note that `client` is updated after initialization, but marking this readwrite would generate an +// incorrect setter (since we make the assignment to `client` inside an `@synchronized` block. +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; @property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; @end @implementation FIRFirestore { + // All guarded by @synchronized(self) FIRFirestoreSettings *_settings; + FSTFirestoreClient *_client; } + (NSMutableDictionary<NSString *, FIRFirestore *> *)instances { @@ -154,64 +158,74 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; } - (FIRFirestoreSettings *)settings { - // Disallow mutation of our internal settings - return [_settings copy]; + @synchronized(self) { + // Disallow mutation of our internal settings + return [_settings copy]; + } } - (void)setSettings:(FIRFirestoreSettings *)settings { - // As a special exception, don't throw if the same settings are passed repeatedly. This should - // make it more friendly to create a Firestore instance. - if (_client && ![_settings isEqual:settings]) { - FSTThrowInvalidUsage(@"FIRIllegalStateException", - @"Firestore instance has already been started and its settings can no " - "longer be changed. You can only set settings before calling any " - "other methods on a Firestore instance."); + @synchronized(self) { + // As a special exception, don't throw if the same settings are passed repeatedly. This should + // make it more friendly to create a Firestore instance. + if (_client && ![_settings isEqual:settings]) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"Firestore instance has already been started and its settings can no " + "longer be changed. You can only set settings before calling any " + "other methods on a Firestore instance."); + } + _settings = [settings copy]; } - _settings = [settings copy]; } /** - * Ensures that the FirestoreClient is configured. - * @return self + * Ensures that the FirestoreClient is configured and returns it. */ -- (instancetype)firestoreWithConfiguredClient { - if (!_client) { - // These values are validated elsewhere; this is just double-checking: - FSTAssert(_settings.host, @"FirestoreSettings.host cannot be nil."); - FSTAssert(_settings.dispatchQueue, @"FirestoreSettings.dispatchQueue cannot be nil."); - - FSTDatabaseInfo *databaseInfo = - [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID - persistenceKey:_persistenceKey - host:_settings.host - sslEnabled:_settings.sslEnabled]; - - FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; - - _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo - usePersistence:_settings.persistenceEnabled - credentialsProvider:_credentialsProvider - userDispatchQueue:userDispatchQueue - workerDispatchQueue:_workerDispatchQueue]; +- (FSTFirestoreClient *)client { + [self ensureClientConfigured]; + return _client; +} + +- (void)ensureClientConfigured { + @synchronized(self) { + if (!_client) { + // These values are validated elsewhere; this is just double-checking: + FSTAssert(_settings.host, @"FirestoreSettings.host cannot be nil."); + FSTAssert(_settings.dispatchQueue, @"FirestoreSettings.dispatchQueue cannot be nil."); + + FSTDatabaseInfo *databaseInfo = + [FSTDatabaseInfo databaseInfoWithDatabaseID:_databaseID + persistenceKey:_persistenceKey + host:_settings.host + sslEnabled:_settings.sslEnabled]; + + FSTDispatchQueue *userDispatchQueue = [FSTDispatchQueue queueWith:_settings.dispatchQueue]; + + _client = [FSTFirestoreClient clientWithDatabaseInfo:databaseInfo + usePersistence:_settings.persistenceEnabled + credentialsProvider:_credentialsProvider + userDispatchQueue:userDispatchQueue + workerDispatchQueue:_workerDispatchQueue]; + } } - return self; } - (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { if (!collectionPath) { FSTThrowInvalidArgument(@"Collection path cannot be nil."); } + [self ensureClientConfigured]; FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath]; - return - [FIRCollectionReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient]; + return [FIRCollectionReference referenceWithPath:path firestore:self]; } - (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { if (!documentPath) { FSTThrowInvalidArgument(@"Document path cannot be nil."); } + [self ensureClientConfigured]; FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath]; - return [FIRDocumentReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient]; + return [FIRDocumentReference referenceWithPath:path firestore:self]; } - (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock @@ -241,12 +255,13 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; internalCompletion(result, error); }); }; - [self firestoreWithConfiguredClient]; [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; } - (FIRWriteBatch *)batch { - return [FIRWriteBatch writeBatchWithFirestore:[self firestoreWithConfiguredClient]]; + [self ensureClientConfigured]; + + return [FIRWriteBatch writeBatchWithFirestore:self]; } - (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock @@ -264,11 +279,19 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; } - (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { - if (!self.client) { + FSTFirestoreClient *client; + @synchronized(self) { + client = _client; + _client = nil; + } + + if (!client) { + // We should be dispatching the callback on the user dispatch queue but if the client is nil + // here that queue was never created. completion(nil); - return; + } else { + [client shutdownWithCompletion:completion]; } - return [self.client shutdownWithCompletion:completion]; } + (BOOL)isLoggingEnabled { @@ -279,6 +302,16 @@ NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); } +- (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + [self ensureClientConfigured]; + [self.client enableNetworkWithCompletion:completion]; +} + +- (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable))completion { + [self ensureClientConfigured]; + [self.client disableNetworkWithCompletion:completion]; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.m b/Firestore/Source/API/FIRQuery.m index 12e79c5..2feca39 100644 --- a/Firestore/Source/API/FIRQuery.m +++ b/Firestore/Source/API/FIRQuery.m @@ -107,7 +107,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isEqual:(nullable id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self isEqualToQuery:other]; } @@ -256,6 +256,95 @@ addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions value:value]; } +- (FIRQuery *)queryFilteredUsingComparisonPredicate:(NSPredicate *)predicate { + NSComparisonPredicate *comparison = (NSComparisonPredicate *)predicate; + if (comparison.comparisonPredicateModifier != NSDirectPredicateModifier) { + FSTThrowInvalidArgument(@"Invalid query. Predicate cannot have an aggregate modifier."); + } + NSString *path; + id value = nil; + if ([comparison.leftExpression expressionType] == NSKeyPathExpressionType && + [comparison.rightExpression expressionType] == NSConstantValueExpressionType) { + path = comparison.leftExpression.keyPath; + value = comparison.rightExpression.constantValue; + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: + return [self queryWhereField:path isEqualTo:value]; + case NSLessThanPredicateOperatorType: + return [self queryWhereField:path isLessThan:value]; + case NSLessThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isLessThanOrEqualTo:value]; + case NSGreaterThanPredicateOperatorType: + return [self queryWhereField:path isGreaterThan:value]; + case NSGreaterThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isGreaterThanOrEqualTo:value]; + default:; // Fallback below to throw assertion. + } + } else if ([comparison.leftExpression expressionType] == NSConstantValueExpressionType && + [comparison.rightExpression expressionType] == NSKeyPathExpressionType) { + path = comparison.rightExpression.keyPath; + value = comparison.leftExpression.constantValue; + switch (comparison.predicateOperatorType) { + case NSEqualToPredicateOperatorType: + return [self queryWhereField:path isEqualTo:value]; + case NSLessThanPredicateOperatorType: + return [self queryWhereField:path isGreaterThan:value]; + case NSLessThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isGreaterThanOrEqualTo:value]; + case NSGreaterThanPredicateOperatorType: + return [self queryWhereField:path isLessThan:value]; + case NSGreaterThanOrEqualToPredicateOperatorType: + return [self queryWhereField:path isLessThanOrEqualTo:value]; + default:; // Fallback below to throw assertion. + } + } else { + FSTThrowInvalidArgument( + @"Invalid query. Predicate comparisons must include a key path and a constant."); + } + // Fallback cases of unsupported comparison operator. + switch (comparison.predicateOperatorType) { + case NSCustomSelectorPredicateOperatorType: + FSTThrowInvalidArgument(@"Invalid query. Custom predicate filters are not supported."); + break; + default: + FSTThrowInvalidArgument(@"Invalid query. Operator type %lu is not supported.", + (unsigned long)comparison.predicateOperatorType); + } +} + +- (FIRQuery *)queryFilteredUsingCompoundPredicate:(NSPredicate *)predicate { + NSCompoundPredicate *compound = (NSCompoundPredicate *)predicate; + if (compound.compoundPredicateType != NSAndPredicateType || compound.subpredicates.count == 0) { + FSTThrowInvalidArgument(@"Invalid query. Only compound queries using AND are supported."); + } + FIRQuery *query = self; + for (NSPredicate *pred in compound.subpredicates) { + query = [query queryFilteredUsingPredicate:pred]; + } + return query; +} + +- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate { + if ([predicate isKindOfClass:[NSComparisonPredicate class]]) { + return [self queryFilteredUsingComparisonPredicate:predicate]; + } else if ([predicate isKindOfClass:[NSCompoundPredicate class]]) { + return [self queryFilteredUsingCompoundPredicate:predicate]; + } else if ([predicate isKindOfClass:[[NSPredicate + predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) { + return true; + }] class]]) { + FSTThrowInvalidArgument( + @"Invalid query. Block-based predicates are not " + "supported. Please use predicateWithFormat to " + "create predicates instead."); + } else { + FSTThrowInvalidArgument( + @"Invalid query. Expect comparison or compound of " + "comparison predicate. Please use " + "predicateWithFormat to create predicates."); + } +} + - (FIRQuery *)queryOrderedByField:(NSString *)field { return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m index 6bc6761..177e461 100644 --- a/Firestore/Source/API/FIRQuerySnapshot.m +++ b/Firestore/Source/API/FIRQuerySnapshot.m @@ -16,6 +16,7 @@ #import "Firestore/Source/API/FIRQuerySnapshot+Internal.h" +#import "FIRFirestore.h" #import "FIRSnapshotMetadata.h" #import "Firestore/Source/API/FIRDocumentChange+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" @@ -57,7 +58,7 @@ NS_ASSUME_NONNULL_BEGIN @implementation FIRQuerySnapshot { // Cached value of the documents property. - NSArray<FIRDocumentSnapshot *> *_documents; + NSArray<FIRQueryDocumentSnapshot *> *_documents; // Cached value of the documentChanges property. NSArray<FIRDocumentChange *> *_documentChanges; @@ -76,6 +77,35 @@ NS_ASSUME_NONNULL_BEGIN return self; } +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToSnapshot:other]; +} + +- (BOOL)isEqualToSnapshot:(nullable FIRQuerySnapshot *)snapshot { + if (self == snapshot) return YES; + if (snapshot == nil) return NO; + if (self.firestore != snapshot.firestore && ![self.firestore isEqual:snapshot.firestore]) + return NO; + if (self.originalQuery != snapshot.originalQuery && + ![self.originalQuery isEqual:snapshot.originalQuery]) + return NO; + if (self.snapshot != snapshot.snapshot && ![self.snapshot isEqual:snapshot.snapshot]) return NO; + if (self.metadata != snapshot.metadata && ![self.metadata isEqual:snapshot.metadata]) return NO; + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.firestore hash]; + hash = hash * 31u + [self.originalQuery hash]; + hash = hash * 31u + [self.snapshot hash]; + hash = hash * 31u + [self.metadata hash]; + return hash; +} + @dynamic empty; - (FIRQuery *)query { @@ -93,18 +123,18 @@ NS_ASSUME_NONNULL_BEGIN return self.snapshot.documents.count; } -- (NSArray<FIRDocumentSnapshot *> *)documents { +- (NSArray<FIRQueryDocumentSnapshot *> *)documents { if (!_documents) { FSTDocumentSet *documentSet = self.snapshot.documents; FIRFirestore *firestore = self.firestore; BOOL fromCache = self.metadata.fromCache; - NSMutableArray<FIRDocumentSnapshot *> *result = [NSMutableArray array]; + NSMutableArray<FIRQueryDocumentSnapshot *> *result = [NSMutableArray array]; for (FSTDocument *document in documentSet.documentEnumerator) { - [result addObject:[FIRDocumentSnapshot snapshotWithFirestore:firestore - documentKey:document.key - document:document - fromCache:fromCache]]; + [result addObject:[FIRQueryDocumentSnapshot snapshotWithFirestore:firestore + documentKey:document.key + document:document + fromCache:fromCache]]; } _documents = result; diff --git a/Firestore/Source/API/FIRSetOptions.m b/Firestore/Source/API/FIRSetOptions.m index 623deaa..743bcc7 100644 --- a/Firestore/Source/API/FIRSetOptions.m +++ b/Firestore/Source/API/FIRSetOptions.m @@ -15,7 +15,6 @@ */ #import "Firestore/Source/API/FIRSetOptions+Internal.h" -#import "Firestore/Source/Model/FSTMutation.h" NS_ASSUME_NONNULL_BEGIN diff --git a/Firestore/Source/API/FIRSnapshotMetadata.m b/Firestore/Source/API/FIRSnapshotMetadata.m index 224015f..d957a8d 100644 --- a/Firestore/Source/API/FIRSnapshotMetadata.m +++ b/Firestore/Source/API/FIRSnapshotMetadata.m @@ -44,6 +44,28 @@ NS_ASSUME_NONNULL_BEGIN return self; } +// NSObject Methods +- (BOOL)isEqual:(nullable id)other { + if (other == self) return YES; + if (![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToMetadata:other]; +} + +- (BOOL)isEqualToMetadata:(nullable FIRSnapshotMetadata *)metadata { + if (self == metadata) return YES; + if (metadata == nil) return NO; + if (self.pendingWrites != metadata.pendingWrites) return NO; + if (self.fromCache != metadata.fromCache) return NO; + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = self.pendingWrites ? 1 : 0; + hash = hash * 31u + (self.fromCache ? 1 : 0); + return hash; +} + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotOptions+Internal.h b/Firestore/Source/API/FIRSnapshotOptions+Internal.h new file mode 100644 index 0000000..64e7dbc --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotOptions+Internal.h @@ -0,0 +1,38 @@ +/* + * 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 "FIRDocumentSnapshot.h" + +#import <Foundation/Foundation.h> + +#import "Firestore/Source/Model/FSTFieldValue.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotOptions (Internal) + +/** Returns a default instance of FIRSnapshotOptions that specifies no options. */ ++ (instancetype)defaultOptions; + +/* Initializes a new instance with the specified server timestamp behavior. */ +- (instancetype)initWithServerTimestampBehavior:(FSTServerTimestampBehavior)serverTimestampBehavior; + +/* Returns the server timestamp behavior. Returns -1 if no behavior is specified. */ +- (FSTServerTimestampBehavior)serverTimestampBehavior; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotOptions.m b/Firestore/Source/API/FIRSnapshotOptions.m new file mode 100644 index 0000000..72ea3cc --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotOptions.m @@ -0,0 +1,72 @@ +/* + * 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 "FIRDocumentSnapshot.h" + +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" +#import "Firestore/Source/Util/FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotOptions () + +@property(nonatomic) FSTServerTimestampBehavior serverTimestampBehavior; + +@end + +@implementation FIRSnapshotOptions + +- (instancetype)initWithServerTimestampBehavior: + (FSTServerTimestampBehavior)serverTimestampBehavior { + self = [super init]; + + if (self) { + _serverTimestampBehavior = serverTimestampBehavior; + } + + return self; +} + ++ (instancetype)defaultOptions { + static FIRSnapshotOptions *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = + [[FIRSnapshotOptions alloc] initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; + }); + + return sharedInstance; +} + ++ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior { + switch (serverTimestampBehavior) { + case FIRServerTimestampBehaviorEstimate: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorEstimate]; + case FIRServerTimestampBehaviorPrevious: + return [[FIRSnapshotOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorPrevious]; + case FIRServerTimestampBehaviorNone: + return [FIRSnapshotOptions defaultOptions]; + default: + FSTFail(@"Encountered unknown server timestamp behavior: %d", (int)serverTimestampBehavior); + } +} + +@end + +NS_ASSUME_NONNULL_END
\ No newline at end of file diff --git a/Firestore/Source/API/FIRWriteBatch.m b/Firestore/Source/API/FIRWriteBatch.m index b918a9a..b1cfa09 100644 --- a/Firestore/Source/API/FIRWriteBatch.m +++ b/Firestore/Source/API/FIRWriteBatch.m @@ -93,7 +93,11 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion { +- (void)commit { + [self commitWithCompletion:nil]; +} + +- (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { [self verifyNotCommitted]; self.committed = TRUE; [self.firestore.client writeMutations:self.mutations completion:completion]; diff --git a/Firestore/Source/Core/FSTEventManager.h b/Firestore/Source/Core/FSTEventManager.h index edd2a96..8eafd4b 100644 --- a/Firestore/Source/Core/FSTEventManager.h +++ b/Firestore/Source/Core/FSTEventManager.h @@ -62,7 +62,7 @@ NS_ASSUME_NONNULL_BEGIN - (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot; - (void)queryDidError:(NSError *)error; -- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState; +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState; @property(nonatomic, strong, readonly) FSTQuery *query; diff --git a/Firestore/Source/Core/FSTEventManager.m b/Firestore/Source/Core/FSTEventManager.m index 3e1b99b..bc204a0 100644 --- a/Firestore/Source/Core/FSTEventManager.m +++ b/Firestore/Source/Core/FSTEventManager.m @@ -151,7 +151,7 @@ NS_ASSUME_NONNULL_BEGIN self.viewSnapshotHandler(nil, error); } -- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState { +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { self.onlineState = onlineState; if (self.snapshot && !self.raisedInitialEvent && [self shouldRaiseInitialEventForSnapshot:self.snapshot onlineState:onlineState]) { @@ -268,7 +268,7 @@ NS_ASSUME_NONNULL_BEGIN } [queryInfo.listeners addObject:listener]; - [listener clientDidChangeOnlineState:self.onlineState]; + [listener applyChangedOnlineState:self.onlineState]; if (queryInfo.viewSnapshot) { [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot]; @@ -321,11 +321,11 @@ NS_ASSUME_NONNULL_BEGIN [self.queries removeObjectForKey:query]; } -- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState { +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { self.onlineState = onlineState; for (FSTQueryListenersInfo *info in self.queries.objectEnumerator) { for (FSTQueryListener *listener in info.listeners) { - [listener clientDidChangeOnlineState:onlineState]; + [listener applyChangedOnlineState:onlineState]; } } } diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h index 6a1e11b..0ecf2f6 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.h +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN * SDK architecture. It is responsible for creating the worker queue that is shared by all of the * other components in the system. */ -@interface FSTFirestoreClient : NSObject +@interface FSTFirestoreClient : NSObject <FSTOnlineStateDelegate> /** * Creates and returns a FSTFirestoreClient with the given parameters. diff --git a/Firestore/Source/Core/FSTFirestoreClient.m b/Firestore/Source/Core/FSTFirestoreClient.m index 2e0e407..fff644d 100644 --- a/Firestore/Source/Core/FSTFirestoreClient.m +++ b/Firestore/Source/Core/FSTFirestoreClient.m @@ -172,7 +172,7 @@ NS_ASSUME_NONNULL_BEGIN // Setup wiring for remote store. _remoteStore.syncEngine = _syncEngine; - _remoteStore.onlineStateDelegate = _eventManager; + _remoteStore.onlineStateDelegate = self; // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation // queue, etc.) so must be started after LocalStore. @@ -187,6 +187,11 @@ NS_ASSUME_NONNULL_BEGIN [self.syncEngine userDidChange:user]; } +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + [self.syncEngine applyChangedOnlineState:onlineState]; + [self.eventManager applyChangedOnlineState:onlineState]; +} + - (void)disableNetworkWithCompletion:(nullable FSTVoidErrorBlock)completion { [self.workerDispatchQueue dispatchAsync:^{ [self.remoteStore disableNetwork]; diff --git a/Firestore/Source/Core/FSTQuery.m b/Firestore/Source/Core/FSTQuery.m index 0bfd917..13657f7 100644 --- a/Firestore/Source/Core/FSTQuery.m +++ b/Firestore/Source/Core/FSTQuery.m @@ -205,7 +205,7 @@ NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOpe - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self.field isEqual:((FSTNullFilter *)other).field]; } @@ -246,7 +246,7 @@ NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOpe - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self.field isEqual:((FSTNanFilter *)other).field]; } diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h index bb45196..7060155 100644 --- a/Firestore/Source/Core/FSTSyncEngine.h +++ b/Firestore/Source/Core/FSTSyncEngine.h @@ -100,6 +100,9 @@ NS_ASSUME_NONNULL_BEGIN - (void)userDidChange:(FSTUser *)user; +/** Applies an FSTOnlineState change to the sync engine and notifies any views of the change. */ +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.m b/Firestore/Source/Core/FSTSyncEngine.m index 98658e4..27ab73e 100644 --- a/Firestore/Source/Core/FSTSyncEngine.m +++ b/Firestore/Source/Core/FSTSyncEngine.m @@ -318,6 +318,21 @@ NS_ASSUME_NONNULL_BEGIN [self emitNewSnapshotsWithChanges:changes remoteEvent:remoteEvent]; } +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState { + NSMutableArray<FSTViewSnapshot *> *newViewSnapshots = [NSMutableArray array]; + [self.queryViewsByQuery + enumerateKeysAndObjectsUsingBlock:^(FSTQuery *query, FSTQueryView *queryView, BOOL *stop) { + FSTViewChange *viewChange = [queryView.view applyChangedOnlineState:onlineState]; + FSTAssert(viewChange.limboChanges.count == 0, + @"OnlineState should not affect limbo documents."); + if (viewChange.snapshot) { + [newViewSnapshots addObject:viewChange.snapshot]; + } + }]; + + [self.delegate handleViewSnapshots:newViewSnapshots]; +} + - (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error { [self assertDelegateExistsForSelector:_cmd]; diff --git a/Firestore/Source/Core/FSTTypes.h b/Firestore/Source/Core/FSTTypes.h index c10f1bf..b47bd0b 100644 --- a/Firestore/Source/Core/FSTTypes.h +++ b/Firestore/Source/Core/FSTTypes.h @@ -67,8 +67,8 @@ typedef void (^FSTTransactionBlock)(FSTTransaction *transaction, typedef NS_ENUM(NSUInteger, FSTOnlineState) { /** * The Firestore client is in an unknown online state. This means the client is either not - * actively trying to establish a connection or it was previously in an unknown state and is - * trying to establish a connection. + * actively trying to establish a connection or it is currently trying to establish a connection, + * but it has not succeeded or failed yet. */ FSTOnlineStateUnknown, @@ -80,9 +80,8 @@ typedef NS_ENUM(NSUInteger, FSTOnlineState) { FSTOnlineStateHealthy, /** - * The client has tried to establish a connection but has failed. - * This state is reached after either a connection attempt failed or a healthy stream was closed - * for unexpected reasons. + * The client considers itself offline. It is either trying to establish a connection but + * failing, or it has been explicitly marked offline via a call to `disableNetwork`. */ FSTOnlineStateFailed }; diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h index ed230a3..6ff77cd 100644 --- a/Firestore/Source/Core/FSTView.h +++ b/Firestore/Source/Core/FSTView.h @@ -16,6 +16,7 @@ #import <Foundation/Foundation.h> +#import "Firestore/Source/Core/FSTTypes.h" #import "Firestore/Source/Model/FSTDocumentDictionary.h" #import "Firestore/Source/Model/FSTDocumentKeySet.h" @@ -138,6 +139,12 @@ typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { - (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges targetChange:(nullable FSTTargetChange *)targetChange; +/** + * Applies an FSTOnlineState change to the view, potentially generating an FSTViewChange if the + * view's syncState changes as a result. + */ +- (FSTViewChange *)applyChangedOnlineState:(FSTOnlineState)onlineState; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.m b/Firestore/Source/Core/FSTView.m index 9b44bf4..78019c6 100644 --- a/Firestore/Source/Core/FSTView.m +++ b/Firestore/Source/Core/FSTView.m @@ -94,6 +94,12 @@ NS_ASSUME_NONNULL_BEGIN 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 @@ -330,6 +336,24 @@ static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChang } } +- (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. */ diff --git a/Firestore/Source/Local/FSTDocumentReference.m b/Firestore/Source/Local/FSTDocumentReference.m index 1631789..25a5935 100644 --- a/Firestore/Source/Local/FSTDocumentReference.m +++ b/Firestore/Source/Local/FSTDocumentReference.m @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; FSTDocumentReference *reference = (FSTDocumentReference *)other; diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm index fb1c81a..83b932c 100644 --- a/Firestore/Source/Local/FSTLevelDB.mm +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -72,8 +72,8 @@ using leveldb::WriteOptions; #else #error "local storage on tvOS" -// TODO(mcg): Writing to NSDocumentsDirectory on tvOS will fail; we need to write to Caches -// https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/ + // TODO(mcg): Writing to NSDocumentsDirectory on tvOS will fail; we need to write to Caches + // https://developer.apple.com/library/content/documentation/General/Conceptual/AppleTV_PG/ #endif } diff --git a/Firestore/Source/Model/FSTDatabaseID.m b/Firestore/Source/Model/FSTDatabaseID.m index 4d0448a..bff5855 100644 --- a/Firestore/Source/Model/FSTDatabaseID.m +++ b/Firestore/Source/Model/FSTDatabaseID.m @@ -48,7 +48,7 @@ NSString *const kDefaultDatabaseID = @"(default)"; - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; return [self isEqualToDatabaseId:other]; } diff --git a/Firestore/Source/Model/FSTFieldValue.h b/Firestore/Source/Model/FSTFieldValue.h index 6de9793..93fd5c4 100644 --- a/Firestore/Source/Model/FSTFieldValue.h +++ b/Firestore/Source/Model/FSTFieldValue.h @@ -22,7 +22,9 @@ @class FSTDocumentKey; @class FSTFieldPath; @class FSTTimestamp; +@class FSTFieldValueOptions; @class FIRGeoPoint; +@class FIRSnapshotOptions; NS_ASSUME_NONNULL_BEGIN @@ -40,6 +42,32 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { FSTTypeOrderObject, }; +/** Defines the return value for pending server timestamps. */ +typedef NS_ENUM(NSInteger, FSTServerTimestampBehavior) { + FSTServerTimestampBehaviorNone, + FSTServerTimestampBehaviorEstimate, + FSTServerTimestampBehaviorPrevious +}; + +/** Holds properties that define field value deserialization options. */ +@interface FSTFieldValueOptions : NSObject + +@property(nonatomic, readonly, assign) FSTServerTimestampBehavior serverTimestampBehavior; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates an FSTFieldValueOptions instance that specifies deserialization behavior for pending + * server timestamps. + */ +- (instancetype)initWithServerTimestampBehavior:(FSTServerTimestampBehavior)serverTimestampBehavior + NS_DESIGNATED_INITIALIZER; + +/** Creates an FSTFieldValueOption instance from FIRSnapshotOptions. */ ++ (instancetype)optionsForSnapshotOptions:(FIRSnapshotOptions *)value; + +@end + /** * Abstract base class representing an immutable data value as stored in Firestore. FSTFieldValue * represents all the different kinds of values that can be stored in fields in a document. @@ -58,7 +86,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { * - Array * - Object */ -@interface FSTFieldValue : NSObject +@interface FSTFieldValue <__covariant T> : NSObject /** Returns the FSTTypeOrder for this value. */ - (FSTTypeOrder)typeOrder; @@ -69,7 +97,15 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { * TODO(mikelehen): This conversion should probably happen at the API level and right now `value` is * used inappropriately in the serializer implementation, etc. We need to do some reworking. */ -- (id)value; +- (T)value; + +/** + * Converts an FSTFieldValue into the value that users will see in document snapshots. + * + * Options can be provided to configure the deserialization of some field values (such as server + * timestamps). + */ +- (T)valueWithOptions:(FSTFieldValueOptions *)options; /** Compares against another FSTFieldValue. */ - (NSComparisonResult)compare:(FSTFieldValue *)other; @@ -79,26 +115,24 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { /** * A null value stored in Firestore. The |value| of a FSTNullValue is [NSNull null]. */ -@interface FSTNullValue : FSTFieldValue +@interface FSTNullValue : FSTFieldValue <NSNull *> + (instancetype)nullValue; -- (id)value; @end /** * A boolean value stored in Firestore. */ -@interface FSTBooleanValue : FSTFieldValue +@interface FSTBooleanValue : FSTFieldValue <NSNumber *> + (instancetype)trueValue; + (instancetype)falseValue; + (instancetype)booleanValue:(BOOL)value; -- (NSNumber *)value; @end /** * Base class inherited from by FSTIntegerValue and FSTDoubleValue. It implements proper number * comparisons between the two types. */ -@interface FSTNumberValue : FSTFieldValue +@interface FSTNumberValue : FSTFieldValue <NSNumber *> @end /** @@ -106,7 +140,6 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { */ @interface FSTIntegerValue : FSTNumberValue + (instancetype)integerValue:(int64_t)value; -- (NSNumber *)value; - (int64_t)internalValue; @end @@ -116,24 +149,21 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { @interface FSTDoubleValue : FSTNumberValue + (instancetype)doubleValue:(double)value; + (instancetype)nanValue; -- (NSNumber *)value; - (double)internalValue; @end /** * A string stored in Firestore. */ -@interface FSTStringValue : FSTFieldValue +@interface FSTStringValue : FSTFieldValue <NSString *> + (instancetype)stringValue:(NSString *)value; -- (NSString *)value; @end /** * A timestamp value stored in Firestore. */ -@interface FSTTimestampValue : FSTFieldValue +@interface FSTTimestampValue : FSTFieldValue <NSDate *> + (instancetype)timestampValue:(FSTTimestamp *)value; -- (NSDate *)value; - (FSTTimestamp *)internalValue; @end @@ -144,46 +174,54 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { * - FSTServerTimestampValue instances are created as the result of applying an FSTTransformMutation * (see [FSTTransformMutation applyTo]). They can only exist in the local view of a document. * Therefore they do not need to be parsed or serialized. - * - When evaluated locally (e.g. via FSTDocumentSnapshot data), they evaluate to NSNull (at least - * for now, see b/62064202). + * - When evaluated locally (e.g. via FSTDocumentSnapshot data), they by default evaluate to NSNull. + * This behavior can be configured by passing custom FSTFieldValueOptions to `valueWithOptions:`. * - They sort after all FSTTimestampValues. With respect to other FSTServerTimestampValues, they * sort by their localWriteTime. */ -@interface FSTServerTimestampValue : FSTFieldValue -+ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime; -- (NSNull *)value; +@interface FSTServerTimestampValue : FSTFieldValue <id> ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue; + @property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime; +@property(nonatomic, strong, readonly, nullable) FSTFieldValue *previousValue; + @end /** * A geo point value stored in Firestore. */ -@interface FSTGeoPointValue : FSTFieldValue +@interface FSTGeoPointValue : FSTFieldValue <FIRGeoPoint *> + (instancetype)geoPointValue:(FIRGeoPoint *)value; -- (FIRGeoPoint *)value; +- (FIRGeoPoint *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** * A blob value stored in Firestore. */ -@interface FSTBlobValue : FSTFieldValue +@interface FSTBlobValue : FSTFieldValue <NSData *> + (instancetype)blobValue:(NSData *)value; -- (NSData *)value; +- (NSData *)valueWithOptions:(FSTFieldValueOptions *)options; @end /** * A reference value stored in Firestore. */ -@interface FSTReferenceValue : FSTFieldValue +@interface FSTReferenceValue : FSTFieldValue <FSTDocumentKey *> + (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID; -- (FSTDocumentKey *)value; +- (FSTDocumentKey *)valueWithOptions:(FSTFieldValueOptions *)options; @property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; @end /** * A structured object value stored in Firestore. */ -@interface FSTObjectValue : FSTFieldValue +// clang-format off +@interface FSTObjectValue : FSTFieldValue < NSDictionary<NSString *, id> * > + +- (instancetype)init NS_UNAVAILABLE; +// clang-format on + /** Returns an empty FSTObjectValue. */ + (instancetype)objectValue; @@ -198,9 +236,7 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { - (instancetype)initWithImmutableDictionary: (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)value NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; - -- (NSDictionary<NSString *, id> *)value; +- (NSDictionary<NSString *, id> *)valueWithOptions:(FSTFieldValueOptions *)options; - (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)internalValue; /** Returns the value at the given path if it exists. Returns nil otherwise. */ @@ -222,19 +258,20 @@ typedef NS_ENUM(NSInteger, FSTTypeOrder) { /** * An array value stored in Firestore. */ -@interface FSTArrayValue : FSTFieldValue +// clang-format off +@interface FSTArrayValue : FSTFieldValue < NSArray <id> * > + +- (instancetype)init NS_UNAVAILABLE; +// clang-format on /** * Initializes this instance with the given array of wrapped values. * * @param value An immutable array of FSTFieldValue objects. Caller is responsible for copying the - * value or releasing all references. + * value or releasing all references. */ - (instancetype)initWithValueNoCopy:(NSArray<FSTFieldValue *> *)value NS_DESIGNATED_INITIALIZER; -- (instancetype)init NS_UNAVAILABLE; - -- (NSArray<id> *)value; - (NSArray<FSTFieldValue *> *)internalValue; @end diff --git a/Firestore/Source/Model/FSTFieldValue.m b/Firestore/Source/Model/FSTFieldValue.m index 95ad306..a6326a7 100644 --- a/Firestore/Source/Model/FSTFieldValue.m +++ b/Firestore/Source/Model/FSTFieldValue.m @@ -17,6 +17,7 @@ #import "Firestore/Source/Model/FSTFieldValue.h" #import "Firestore/Source/API/FIRGeoPoint+Internal.h" +#import "Firestore/Source/API/FIRSnapshotOptions+Internal.h" #import "Firestore/Source/Core/FSTTimestamp.h" #import "Firestore/Source/Model/FSTDatabaseID.h" #import "Firestore/Source/Model/FSTDocumentKey.h" @@ -27,6 +28,38 @@ NS_ASSUME_NONNULL_BEGIN +#pragma mark - FSTFieldValueOptions + +@implementation FSTFieldValueOptions + ++ (instancetype)optionsForSnapshotOptions:(FIRSnapshotOptions *)options { + if (options.serverTimestampBehavior == FSTServerTimestampBehaviorNone) { + static FSTFieldValueOptions *defaultInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + defaultInstance = [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:FSTServerTimestampBehaviorNone]; + }); + return defaultInstance; + } else { + return [[FSTFieldValueOptions alloc] + initWithServerTimestampBehavior:options.serverTimestampBehavior]; + } +} + +- (instancetype)initWithServerTimestampBehavior: + (FSTServerTimestampBehavior)serverTimestampBehavior { + self = [super init]; + + if (self) { + _serverTimestampBehavior = serverTimestampBehavior; + } + return self; +} + +@end + #pragma mark - FSTFieldValue @interface FSTFieldValue () @@ -40,6 +73,11 @@ NS_ASSUME_NONNULL_BEGIN } - (id)value { + return [self valueWithOptions:[FSTFieldValueOptions + optionsForSnapshotOptions:[FIRSnapshotOptions defaultOptions]]]; +} + +- (id)valueWithOptions:(FSTFieldValueOptions *)options { @throw FSTAbstractMethodException(); // NOLINT } @@ -89,7 +127,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderNull; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return [NSNull null]; } @@ -155,7 +193,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderBoolean; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue ? @YES : @NO; } @@ -233,7 +271,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return @(self.internalValue); } @@ -285,7 +323,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return @(self.internalValue); } @@ -332,7 +370,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderString; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -379,7 +417,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderTimestamp; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { // For developers, we expose Timestamps as Dates. return self.internalValue.approximateDateValue; } @@ -410,14 +448,18 @@ NS_ASSUME_NONNULL_BEGIN @implementation FSTServerTimestampValue -+ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime { - return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime]; ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue { + return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime + previousValue:previousValue]; } -- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime { +- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime + previousValue:(nullable FSTFieldValue *)previousValue { self = [super init]; if (self) { _localWriteTime = localWriteTime; + _previousValue = previousValue; } return self; } @@ -426,9 +468,17 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderTimestamp; } -- (NSNull *)value { - // For developers, server timestamps always evaluate to NSNull (for now, at least; b/62064202). - return [NSNull null]; +- (id)valueWithOptions:(FSTFieldValueOptions *)options { + switch (options.serverTimestampBehavior) { + case FSTServerTimestampBehaviorNone: + return [NSNull null]; + case FSTServerTimestampBehaviorEstimate: + return [self.localWriteTime approximateDateValue]; + case FSTServerTimestampBehaviorPrevious: + return self.previousValue ? [self.previousValue valueWithOptions:options] : [NSNull null]; + default: + FSTFail(@"Unexpected server timestamp option: %d", (int)options.serverTimestampBehavior); + } } - (BOOL)isEqual:(id)other { @@ -481,7 +531,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderGeoPoint; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -529,7 +579,7 @@ NS_ASSUME_NONNULL_BEGIN return FSTTypeOrderBlob; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.internalValue; } @@ -573,7 +623,7 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { return self.key; } @@ -648,11 +698,11 @@ NS_ASSUME_NONNULL_BEGIN return [self initWithImmutableDictionary:dictionary]; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { NSMutableDictionary *result = [NSMutableDictionary dictionary]; [self.internalValue enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { - result[key] = [obj value]; + result[key] = [obj valueWithOptions:options]; }]; return result; } @@ -803,7 +853,7 @@ NS_ASSUME_NONNULL_BEGIN return [self.internalValue hash]; } -- (id)value { +- (id)valueWithOptions:(FSTFieldValueOptions *)options { NSMutableArray *result = [NSMutableArray arrayWithCapacity:_internalValue.count]; [self.internalValue enumerateObjectsUsingBlock:^(FSTFieldValue *obj, NSUInteger idx, BOOL *stop) { [result addObject:[obj value]]; diff --git a/Firestore/Source/Model/FSTMutation.h b/Firestore/Source/Model/FSTMutation.h index ef7f1c8..7c5f6de 100644 --- a/Firestore/Source/Model/FSTMutation.h +++ b/Firestore/Source/Model/FSTMutation.h @@ -158,8 +158,10 @@ typedef NS_ENUM(NSUInteger, FSTPreconditionExists) { * Applies this mutation to the given FSTDocument, FSTDeletedDocument or nil, if we don't have * information about this document. Both the input and returned documents can be nil. * - * @param maybeDoc The document to mutate. The input document should nil if it does not currently - * exist. + * @param maybeDoc The current state of the document to mutate. The input document should be nil if + * it does not currently exist. + * @param baseDoc The state of the document prior to this mutation batch. The input document should + * be nil if it the document did not exist. * @param localWriteTime A timestamp indicating the local write time of the batch this mutation is * a part of. * @param mutationResult Optional result info from the backend. If omitted, it's assumed that @@ -196,16 +198,18 @@ typedef NS_ENUM(NSUInteger, FSTPreconditionExists) { * apply the transform if the prior mutation resulted in an FSTDocument (always true for an * FSTSetMutation, but not necessarily for an FSTPatchMutation). */ -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult; + mutationResult:(nullable FSTMutationResult *)mutationResult; /** * A helper version of applyTo for applying mutations locally (without a mutation result from the * backend). */ -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc - localWriteTime:(FSTTimestamp *)localWriteTime; +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(nullable FSTTimestamp *)localWriteTime; @property(nonatomic, strong, readonly) FSTDocumentKey *key; diff --git a/Firestore/Source/Model/FSTMutation.m b/Firestore/Source/Model/FSTMutation.m index 5b47280..c249138 100644 --- a/Firestore/Source/Model/FSTMutation.m +++ b/Firestore/Source/Model/FSTMutation.m @@ -97,7 +97,7 @@ NS_ASSUME_NONNULL_BEGIN - (BOOL)isEqual:(id)other { if (other == self) return YES; - if (!other || ![[other class] isEqual:[self class]]) return NO; + if (![[other class] isEqual:[self class]]) return NO; FSTFieldTransform *otherFieldTransform = other; return [self.path isEqual:otherFieldTransform.path] && [self.transform isEqual:otherFieldTransform.transform]; @@ -236,15 +236,18 @@ NS_ASSUME_NONNULL_BEGIN return self; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { @throw FSTAbstractMethodException(); // NOLINT } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc - localWriteTime:(FSTTimestamp *)localWriteTime { - return [self applyTo:maybeDoc localWriteTime:localWriteTime mutationResult:nil]; +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc + localWriteTime:(nullable FSTTimestamp *)localWriteTime { + return + [self applyTo:maybeDoc baseDocument:baseDoc localWriteTime:localWriteTime mutationResult:nil]; } @end @@ -287,9 +290,10 @@ NS_ASSUME_NONNULL_BEGIN return result; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { if (mutationResult) { FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTSetMutation."); } @@ -362,9 +366,10 @@ NS_ASSUME_NONNULL_BEGIN self.key, self.fieldMask, self.value, self.precondition]; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { if (mutationResult) { FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTPatchMutation."); } @@ -451,9 +456,10 @@ NS_ASSUME_NONNULL_BEGIN self.key, self.fieldTransforms, self.precondition]; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { if (mutationResult) { FSTAssert(mutationResult.transformResults, @"Transform results missing for FSTTransformMutation."); @@ -473,8 +479,9 @@ NS_ASSUME_NONNULL_BEGIN BOOL hasLocalMutations = (mutationResult == nil); NSArray<FSTFieldValue *> *transformResults = - mutationResult ? mutationResult.transformResults - : [self localTransformResultsWithWriteTime:localWriteTime]; + mutationResult + ? mutationResult.transformResults + : [self localTransformResultsWithBaseDocument:baseDoc writeTime:localWriteTime]; FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; return [FSTDocument documentWithData:newData key:doc.key @@ -486,16 +493,26 @@ NS_ASSUME_NONNULL_BEGIN * Creates an array of "transform results" (a transform result is a field value representing the * result of applying a transform) for use when applying an FSTTransformMutation locally. * + * @param baseDocument The document prior to applying this mutation batch. * @param localWriteTime The local time of the transform mutation (used to generate * FSTServerTimestampValues). * @return The transform results array. */ -- (NSArray<FSTFieldValue *> *)localTransformResultsWithWriteTime:(FSTTimestamp *)localWriteTime { +- (NSArray<FSTFieldValue *> *)localTransformResultsWithBaseDocument: + (FSTMaybeDocument *_Nullable)baseDocument + writeTime:(FSTTimestamp *)localWriteTime { NSMutableArray<FSTFieldValue *> *transformResults = [NSMutableArray array]; for (FSTFieldTransform *fieldTransform in self.fieldTransforms) { if ([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]]) { - [transformResults addObject:[FSTServerTimestampValue - serverTimestampValueWithLocalWriteTime:localWriteTime]]; + FSTFieldValue *previousValue = nil; + + if ([baseDocument isMemberOfClass:[FSTDocument class]]) { + previousValue = [((FSTDocument *)baseDocument) fieldForPath:fieldTransform.path]; + } + + [transformResults + addObject:[FSTServerTimestampValue serverTimestampValueWithLocalWriteTime:localWriteTime + previousValue:previousValue]]; } else { FSTFail(@"Encountered unknown transform: %@", fieldTransform); } @@ -551,9 +568,10 @@ NS_ASSUME_NONNULL_BEGIN stringWithFormat:@"<FSTDeleteMutation key=%@ precondition=%@>", self.key, self.precondition]; } -- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc +- (nullable FSTMaybeDocument *)applyTo:(nullable FSTMaybeDocument *)maybeDoc + baseDocument:(nullable FSTMaybeDocument *)baseDoc localWriteTime:(FSTTimestamp *)localWriteTime - mutationResult:(FSTMutationResult *_Nullable)mutationResult { + mutationResult:(nullable FSTMutationResult *)mutationResult { if (mutationResult) { FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTDeleteMutation."); diff --git a/Firestore/Source/Model/FSTMutationBatch.m b/Firestore/Source/Model/FSTMutationBatch.m index 3677908..01adca7 100644 --- a/Firestore/Source/Model/FSTMutationBatch.m +++ b/Firestore/Source/Model/FSTMutationBatch.m @@ -71,6 +71,7 @@ const FSTBatchID kFSTBatchIDUnknown = -1; mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult { FSTAssert(!maybeDoc || [maybeDoc.key isEqualToKey:documentKey], @"applyTo: key %@ doesn't match maybeDoc key %@", documentKey, maybeDoc.key); + FSTMaybeDocument *baseDoc = maybeDoc; if (mutationBatchResult) { FSTAssert(mutationBatchResult.mutationResults.count == self.mutations.count, @"Mismatch between mutations length (%lu) and results length (%lu)", @@ -83,6 +84,7 @@ const FSTBatchID kFSTBatchIDUnknown = -1; FSTMutationResult *_Nullable mutationResult = mutationBatchResult.mutationResults[i]; if ([mutation.key isEqualToKey:documentKey]) { maybeDoc = [mutation applyTo:maybeDoc + baseDocument:baseDoc localWriteTime:self.localWriteTime mutationResult:mutationResult]; } diff --git a/Firestore/Source/Model/FSTPath.m b/Firestore/Source/Model/FSTPath.m index b236107..636c322 100644 --- a/Firestore/Source/Model/FSTPath.m +++ b/Firestore/Source/Model/FSTPath.m @@ -240,7 +240,7 @@ NS_ASSUME_NONNULL_BEGIN c = *source++; // TODO(b/37244157): Make this a user-facing exception once we finalize field escaping. FSTAssert(c != '\0', @"Trailing escape characters not allowed in %@", fieldPath); - // Fall through + // Fall through default: // copy into the current segment diff --git a/Firestore/Source/Public/FIRDocumentChange.h b/Firestore/Source/Public/FIRDocumentChange.h index 022c81b..4717067 100644 --- a/Firestore/Source/Public/FIRDocumentChange.h +++ b/Firestore/Source/Public/FIRDocumentChange.h @@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN -@class FIRDocumentSnapshot; +@class FIRQueryDocumentSnapshot; /** An enumeration of document change types. */ typedef NS_ENUM(NSInteger, FIRDocumentChangeType) { @@ -47,7 +47,7 @@ NS_SWIFT_NAME(DocumentChange) @property(nonatomic, readonly) FIRDocumentChangeType type; /** The document affected by this change. */ -@property(nonatomic, strong, readonly) FIRDocumentSnapshot *document; +@property(nonatomic, strong, readonly) FIRQueryDocumentSnapshot *document; /** * The index of the changed document in the result set immediately prior to this FIRDocumentChange diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h index 439e727..7fcc7a8 100644 --- a/Firestore/Source/Public/FIRDocumentReference.h +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -36,8 +36,6 @@ NS_SWIFT_NAME(DocumentListenOptions) - (instancetype)init; -@property(nonatomic, assign, readonly) BOOL includeMetadataChanges; - /** * Sets the includeMetadataChanges option which controls whether metadata-only changes (i.e. only * `FIRDocumentSnapshot.metadata` changed) should trigger snapshot events. Default is NO. diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h index 3e67c25..6e79a7f 100644 --- a/Firestore/Source/Public/FIRDocumentSnapshot.h +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -22,9 +22,61 @@ NS_ASSUME_NONNULL_BEGIN /** + * Controls the return value for server timestamps that have not yet been set to + * their final value. + */ +typedef NS_ENUM(NSInteger, FIRServerTimestampBehavior) { + /** + * Return `NSNull` for `FieldValue.serverTimestamp()` fields that have not yet + * been set to their final value. + */ + FIRServerTimestampBehaviorNone, + + /** + * Return a local estimates for `FieldValue.serverTimestamp()` + * fields that have not yet been set to their final value. This estimate will + * likely differ from the final value and may cause these pending values to + * change once the server result becomes available. + */ + FIRServerTimestampBehaviorEstimate, + + /** + * Return the previous value for `FieldValue.serverTimestamp()` fields that + * have not yet been set to their final value. + */ + FIRServerTimestampBehaviorPrevious +} NS_SWIFT_NAME(ServerTimestampBehavior); + +/** + * Options that configure how data is retrieved from a `DocumentSnapshot` + * (e.g. the desired behavior for server timestamps that have not yet been set + * to their final value). + */ +NS_SWIFT_NAME(SnapshotOptions) +@interface FIRSnapshotOptions : NSObject + +/** */ +- (instancetype)init __attribute__((unavailable("FIRSnapshotOptions cannot be created directly."))); + +/** + * If set, controls the return value for `FieldValue.serverTimestamp()` + * fields that have not yet been set to their final value. + * + * If omitted, `NSNull` will be returned by default. + * + * @return The created `FIRSnapshotOptions` object. + */ ++ (instancetype)serverTimestampBehavior:(FIRServerTimestampBehavior)serverTimestampBehavior; + +@end + +/** * A `FIRDocumentSnapshot` contains data read from a document in your Firestore database. The data * can be extracted with the `data` property or by using subscript syntax to access a specific * field. + * + * For a `FIRDocumentSnapshot` that points to a non-existing document, any data access will return + * `nil`. You can use the `exists` property to explicitly verify a documents existence. */ NS_SWIFT_NAME(DocumentSnapshot) @interface FIRDocumentSnapshot : NSObject @@ -46,21 +98,104 @@ NS_SWIFT_NAME(DocumentSnapshot) @property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata; /** - * Retrieves all fields in the document as an `NSDictionary`. + * Retrieves all fields in the document as an `NSDictionary`. Returns `nil` if the document doesn't + * exist. * - * @return An `NSDictionary` containing all fields in the document. + * Server-provided timestamps that have not yet been set to their final value will be returned as + * `NSNull`. You can use `dataWithOptions()` to configure this behavior. + * + * @return An `NSDictionary` containing all fields in the document or `nil` if the document doesn't + * exist. */ -- (NSDictionary<NSString *, id> *)data; +- (nullable NSDictionary<NSString *, id> *)data; + +/** + * Retrieves all fields in the document as a `Dictionary`. Returns `nil` if the document doesn't + * exist. + * + * @param options `SnapshotOptions` to configure how data is returned from the snapshot (e.g. the + * desired behavior for server timestamps that have not yet been set to their final value). + * @return A `Dictionary` containing all fields in the document or `nil` if the document doesn't + * exist. + */ +- (nullable NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options; + +/** + * Retrieves a specific field from the document. Returns `nil` if the document or the field doesn't + * exist. + * + * The timestamps that have not yet been set to their final value will be returned as `NSNull`. The + * can use `get(_:options:)` to configure this behavior. + * + * @param field The field to retrieve. + * @return The value contained in the field or `nil` if the document or field doesn't exist. + */ +- (nullable id)valueForField:(id)field NS_SWIFT_NAME(get(_:)); + +/** + * Retrieves a specific field from the document. Returns `nil` if the document or the field doesn't + * exist. + * + * The timestamps that have not yet been set to their final value will be returned as `NSNull`. The + * can use `get(_:options:)` to configure this behavior. + * + * @param field The field to retrieve. + * @param options `SnapshotOptions` to configure how data is returned from the snapshot (e.g. the + * desired behavior for server timestamps that have not yet been set to their final value). + * @return The value contained in the field or `nil` if the document or field doesn't exist. + */ +// clang-format off +- (nullable id)valueForField:(id)field + options:(FIRSnapshotOptions *)options + NS_SWIFT_NAME(get(_:options:)); +// clang-format on /** * Retrieves a specific field from the document. * * @param key The field to retrieve. * - * @return The value contained in the field or `nil` if the field doesn't exist. + * @return The value contained in the field or `nil` if the document or field doesn't exist. */ - (nullable id)objectForKeyedSubscript:(id)key; @end +/** + * A `FIRQueryDocumentSnapshot` contains data read from a document in your Firestore database as + * part of a query. The document is guaranteed to exist and its data can be extracted with the + * `data` property or by using subscript syntax to access a specific field. + * + * A `FIRQueryDocumentSnapshot` offers the same API surface as a `FIRDocumentSnapshot`. As + * deleted documents are not returned from queries, its `exists` property will always be true and + * `data:` will never return `nil`. + */ +NS_SWIFT_NAME(QueryDocumentSnapshot) +@interface FIRQueryDocumentSnapshot : FIRDocumentSnapshot + +/** */ +- (instancetype)init + __attribute__((unavailable("FIRQueryDocumentSnapshot cannot be created directly."))); + +/** + * Retrieves all fields in the document as an `NSDictionary`. + * + * Server-provided timestamps that have not yet been set to their final value will be returned as + * `NSNull`. You can use `dataWithOptions()` to configure this behavior. + * + * @return An `NSDictionary` containing all fields in the document. + */ +- (NSDictionary<NSString *, id> *)data; + +/** + * Retrieves all fields in the document as a `Dictionary`. + * + * @param options `SnapshotOptions` to configure how data is returned from the snapshot (e.g. the + * desired behavior for server timestamps that have not yet been set to their final value). + * @return A `Dictionary` containing all fields in the document. + */ +- (NSDictionary<NSString *, id> *)dataWithOptions:(FIRSnapshotOptions *)options; + +@end + NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestore.h b/Firestore/Source/Public/FIRFirestore.h index 91a96a5..4c85aba 100644 --- a/Firestore/Source/Public/FIRFirestore.h +++ b/Firestore/Source/Public/FIRFirestore.h @@ -139,6 +139,23 @@ NS_SWIFT_NAME(Firestore) + (void)enableLogging:(BOOL)logging DEPRECATED_MSG_ATTRIBUTE("Use FIRSetLoggerLevel(FIRLoggerLevelDebug) to enable logging"); +#pragma mark - Network + +/** + * Re-enables usage of the network by this Firestore instance after a prior call to + * `disableNetworkWithCompletion`. Completion block, if provided, will be called once network uasge + * has been enabled. + */ +- (void)enableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; + +/** + * Disables usage of the network by this Firestore instance. It can be re-enabled by via + * `enableNetworkWithCompletion`. While the network is disabled, any snapshot listeners or get calls + * will return results from cache and any write operations will be queued until the network is + * restored. The completion block, if provided, will be called once network usage has been disabled. + */ +- (void)disableNetworkWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; + @end NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h index 0f3aeed..ff15ac6 100644 --- a/Firestore/Source/Public/FIRQuery.h +++ b/Firestore/Source/Public/FIRQuery.h @@ -256,6 +256,19 @@ NS_SWIFT_NAME(Query) isGreaterThanOrEqualTo:(id)value NS_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:)); // clang-format on +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * satisfy the specified predicate. + * + * @param predicate The predicate the document must satisfy. Can be either comparison + * or compound of comparison. In particular, block-based predicate is not supported. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryFilteredUsingPredicate:(NSPredicate *)predicate NS_SWIFT_NAME(filter(using:)); +// clang-format on + #pragma mark - Sorting Data /** * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field. diff --git a/Firestore/Source/Public/FIRQuerySnapshot.h b/Firestore/Source/Public/FIRQuerySnapshot.h index c49a07a..1266832 100644 --- a/Firestore/Source/Public/FIRQuerySnapshot.h +++ b/Firestore/Source/Public/FIRQuerySnapshot.h @@ -19,8 +19,8 @@ NS_ASSUME_NONNULL_BEGIN @class FIRDocumentChange; -@class FIRDocumentSnapshot; @class FIRQuery; +@class FIRQueryDocumentSnapshot; @class FIRSnapshotMetadata; /** @@ -50,7 +50,7 @@ NS_SWIFT_NAME(QuerySnapshot) @property(nonatomic, readonly) NSInteger count; /** An Array of the `FIRDocumentSnapshots` that make up this document set. */ -@property(nonatomic, strong, readonly) NSArray<FIRDocumentSnapshot *> *documents; +@property(nonatomic, strong, readonly) NSArray<FIRQueryDocumentSnapshot *> *documents; /** * An array of the documents that changed since the last snapshot. If this is the first snapshot, diff --git a/Firestore/Source/Public/FIRWriteBatch.h b/Firestore/Source/Public/FIRWriteBatch.h index 5f0034c..8ff1bec 100644 --- a/Firestore/Source/Public/FIRWriteBatch.h +++ b/Firestore/Source/Public/FIRWriteBatch.h @@ -94,6 +94,11 @@ NS_SWIFT_NAME(WriteBatch) /** * Commits all of the writes in this write batch as a single atomic unit. + */ +- (void)commit; + +/** + * Commits all of the writes in this write batch as a single atomic unit. * * @param completion A block to be called once all of the writes in the batch have been * successfully written to the backend as an atomic unit. This block will only execute @@ -101,7 +106,7 @@ NS_SWIFT_NAME(WriteBatch) * completion handler will not be called when the device is offline, though local * changes will be visible immediately. */ -- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion; +- (void)commitWithCompletion:(nullable void (^)(NSError *_Nullable error))completion; @end diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h index 313ddb7..18331ff 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.h +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -83,7 +83,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol FSTOnlineStateDelegate <NSObject> /** Called whenever the online state of the watch stream changes */ -- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState; +- (void)applyChangedOnlineState:(FSTOnlineState)onlineState; @end diff --git a/Firestore/Source/Remote/FSTRemoteStore.m b/Firestore/Source/Remote/FSTRemoteStore.m index 063e487..a0c5059 100644 --- a/Firestore/Source/Remote/FSTRemoteStore.m +++ b/Firestore/Source/Remote/FSTRemoteStore.m @@ -160,27 +160,38 @@ static const int kOnlineAttemptsBeforeFailure = 2; [self enableNetwork]; } -- (void)setOnlineStateToHealthy { - self.shouldWarnOffline = NO; - [self updateAndNotifyAboutOnlineState:FSTOnlineStateHealthy]; -} - -- (void)setOnlineStateToUnknown { - // The state is set to unknown when a healthy stream is closed (e.g. due to a token timeout) or - // when we have no active listens and therefore there's no need to start the stream. Assuming - // there is (possibly in the future) an active listen, then we will eventually move to state - // Online or Failed, but we always want to make at least kOnlineAttemptsBeforeFailure attempts - // before failing, so we reset the count here. - self.watchStreamFailures = 0; - [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown]; +/** + * Updates our OnlineState to the new state, updating local state and notifying the + * onlineStateHandler as appropriate. + */ +- (void)updateOnlineState:(FSTOnlineState)newState { + // Update and broadcast the new state. + if (newState != self.watchStreamOnlineState) { + if (newState == FSTOnlineStateHealthy) { + // We've connected to watch at least once. Don't warn the developer about being offline going + // forward. + self.shouldWarnOffline = NO; + } else if (newState == FSTOnlineStateUnknown) { + // The state is set to unknown when a healthy stream is closed (e.g. due to a token timeout) + // or when we have no active listens and therefore there's no need to start the stream. + // Assuming there is (possibly in the future) an active listen, then we will eventually move + // to state Online or Failed, but we always want to make at least kOnlineAttemptsBeforeFailure + // attempts before failing, so we reset the count here. + self.watchStreamFailures = 0; + } + self.watchStreamOnlineState = newState; + [self.onlineStateDelegate applyChangedOnlineState:newState]; + } } +/** + * Updates our FSTOnlineState as appropriate after the watch stream reports a failure. The first + * failure moves us to the 'Unknown' state. We then may allow multiple failures (based on + * kOnlineAttemptsBeforeFailure) before we actually transition to FSTOnlineStateFailed. + */ - (void)updateOnlineStateAfterFailure { - // The first failure after we are successfully connected moves us to the 'Unknown' state. We - // then may make multiple attempts (based on kOnlineAttemptsBeforeFailure) before we actually - // report failure. if (self.watchStreamOnlineState == FSTOnlineStateHealthy) { - [self setOnlineStateToUnknown]; + [self updateOnlineState:FSTOnlineStateUnknown]; } else { self.watchStreamFailures++; if (self.watchStreamFailures >= kOnlineAttemptsBeforeFailure) { @@ -188,19 +199,11 @@ static const int kOnlineAttemptsBeforeFailure = 2; FSTWarn(@"Could not reach Firestore backend."); self.shouldWarnOffline = NO; } - [self updateAndNotifyAboutOnlineState:FSTOnlineStateFailed]; + [self updateOnlineState:FSTOnlineStateFailed]; } } } -- (void)updateAndNotifyAboutOnlineState:(FSTOnlineState)watchStreamOnlineState { - BOOL didChange = (watchStreamOnlineState != self.watchStreamOnlineState); - self.watchStreamOnlineState = watchStreamOnlineState; - if (didChange) { - [self.onlineStateDelegate watchStreamDidChangeOnlineState:watchStreamOnlineState]; - } -} - #pragma mark Online/Offline state - (BOOL)isNetworkEnabled { @@ -210,8 +213,9 @@ static const int kOnlineAttemptsBeforeFailure = 2; } - (void)enableNetwork { - FSTAssert(self.watchStream == nil, @"enableNetwork: called with non-null watchStream."); - FSTAssert(self.writeStream == nil, @"enableNetwork: called with non-null writeStream."); + if ([self isNetworkEnabled]) { + return; + } // Create new streams (but note they're not started yet). self.watchStream = [self.datastore createWatchStream]; @@ -227,47 +231,51 @@ static const int kOnlineAttemptsBeforeFailure = 2; [self fillWritePipeline]; // This may start the writeStream. // We move back to the unknown state because we might not want to re-open the stream - [self setOnlineStateToUnknown]; + [self updateOnlineState:FSTOnlineStateUnknown]; } - (void)disableNetwork { - [self updateAndNotifyAboutOnlineState:FSTOnlineStateFailed]; + [self disableNetworkInternal]; + // Set the FSTOnlineState to failed so get()'s return from cache, etc. + [self updateOnlineState:FSTOnlineStateFailed]; +} - // NOTE: We're guaranteed not to get any further events from these streams (not even a close - // event). - [self.watchStream stop]; - [self.writeStream stop]; +/** Disables the network, setting the FSTOnlineState to the specified targetOnlineState. */ +- (void)disableNetworkInternal { + if ([self isNetworkEnabled]) { + // NOTE: We're guaranteed not to get any further events from these streams (not even a close + // event). + [self.watchStream stop]; + [self.writeStream stop]; - [self cleanUpWatchStreamState]; - [self cleanUpWriteStreamState]; + [self cleanUpWatchStreamState]; + [self cleanUpWriteStreamState]; - self.writeStream = nil; - self.watchStream = nil; + self.writeStream = nil; + self.watchStream = nil; + } } #pragma mark Shutdown - (void)shutdown { FSTLog(@"FSTRemoteStore %p shutting down", (__bridge void *)self); - - // Don't fire initial listener callbacks on shutdown. - self.onlineStateDelegate = nil; - - // For now, all shutdown logic is handled by disableNetwork(). We might expand on this in the - // future. - if ([self isNetworkEnabled]) { - [self disableNetwork]; - } + [self disableNetworkInternal]; + // Set the FSTOnlineState to Unknown (rather than Failed) to avoid potentially triggering + // spurious listener events with cached data, etc. + [self updateOnlineState:FSTOnlineStateUnknown]; } - (void)userDidChange:(FSTUser *)user { FSTLog(@"FSTRemoteStore %p changing users: %@", (__bridge void *)self, user); - - // Tear down and re-create our network streams. This will ensure we get a fresh auth token - // for the new user and re-fill the write pipeline with new mutations from the LocalStore - // (since mutations are per-user). - [self disableNetwork]; - [self enableNetwork]; + if ([self isNetworkEnabled]) { + // Tear down and re-create our network streams. This will ensure we get a fresh auth token + // for the new user and re-fill the write pipeline with new mutations from the LocalStore + // (since mutations are per-user). + [self disableNetworkInternal]; + [self updateOnlineState:FSTOnlineStateUnknown]; + [self enableNetwork]; + } } #pragma mark Watch Stream @@ -348,7 +356,7 @@ static const int kOnlineAttemptsBeforeFailure = 2; - (void)watchStreamDidChange:(FSTWatchChange *)change snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { // Mark the connection as healthy because we got a message from the server. - [self setOnlineStateToHealthy]; + [self updateOnlineState:FSTOnlineStateHealthy]; FSTWatchTargetChange *watchTargetChange = [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil; @@ -391,7 +399,7 @@ static const int kOnlineAttemptsBeforeFailure = 2; } else { // We don't need to restart the watch stream because there are no active targets. The online // state is set to unknown because there is no active attempt at establishing a connection. - [self setOnlineStateToUnknown]; + [self updateOnlineState:FSTOnlineStateUnknown]; } } @@ -532,6 +540,8 @@ static const int kOnlineAttemptsBeforeFailure = 2; - (void)cleanUpWriteStreamState { self.lastBatchSeen = kFSTBatchIDUnknown; + FSTLog(@"Stopping write stream with %lu pending writes", + (unsigned long)[self.pendingWrites count]); [self.pendingWrites removeAllObjects]; } diff --git a/Firestore/Source/Remote/FSTStream.m b/Firestore/Source/Remote/FSTStream.m index 2c039be..5719ec8 100644 --- a/Firestore/Source/Remote/FSTStream.m +++ b/Firestore/Source/Remote/FSTStream.m @@ -542,7 +542,7 @@ static const NSTimeInterval kIdleTimeout = 60.0; FSTWeakify(self); [self.workerDispatchQueue dispatchAsync:^{ FSTStrongify(self); - if (!self || ![self isStarted]) { + if (![self isStarted]) { FSTLog(@"%@ Ignoring stream message from inactive stream.", NSStringFromClass([self class])); } |