diff options
author | Gil <mcg@google.com> | 2017-10-03 08:55:22 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-03 08:55:22 -0700 |
commit | bde743ed25166a0b320ae157bfb1d68064f531c9 (patch) | |
tree | 4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Source | |
parent | bf550507ffa8beee149383a5bf1e2363bccefbb4 (diff) |
Release 4.3.0 (#327)
Initial release of Firestore at 0.8.0
Bump FirebaseCommunity to 0.1.3
Diffstat (limited to 'Firestore/Source')
185 files changed, 26654 insertions, 0 deletions
diff --git a/Firestore/Source/API/FIRCollectionReference+Internal.h b/Firestore/Source/API/FIRCollectionReference+Internal.h new file mode 100644 index 0000000..1d00cbb --- /dev/null +++ b/Firestore/Source/API/FIRCollectionReference+Internal.h @@ -0,0 +1,28 @@ +/* + * 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 "FIRCollectionReference.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTResourcePath; + +/** Internal FIRCollectionReference API we don't want exposed in our public header files. */ +@interface FIRCollectionReference (Internal) ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRCollectionReference.m b/Firestore/Source/API/FIRCollectionReference.m new file mode 100644 index 0000000..1ded4d2 --- /dev/null +++ b/Firestore/Source/API/FIRCollectionReference.m @@ -0,0 +1,113 @@ +/* + * 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 "FIRCollectionReference.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRQuery+Internal.h" +#import "FIRQuery_Init.h" +#import "FSTAssert.h" +#import "FSTDocumentKey.h" +#import "FSTPath.h" +#import "FSTQuery.h" +#import "FSTUsageValidation.h" +#import "FSTUtil.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRCollectionReference () +- (instancetype)initWithPath:(FSTResourcePath *)path + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +// Mark the super class designated initializer unavailable. +- (instancetype)initWithQuery:(FSTQuery *)query + firestore:(FIRFirestore *)firestore + __attribute__((unavailable("Use the initWithPath constructor of FIRCollectionReference."))); +@end + +@implementation FIRCollectionReference (Internal) ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { + return [[FIRCollectionReference alloc] initWithPath:path firestore:firestore]; +} +@end + +@implementation FIRCollectionReference + +- (instancetype)initWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { + if (path.length % 2 != 1) { + FSTThrowInvalidArgument( + @"Invalid collection reference. Collection references must have an odd " + "number of segments, but %@ has %d", + path.canonicalString, path.length); + } + self = [super initWithQuery:[FSTQuery queryWithPath:path] firestore:firestore]; + return self; +} + +// Override the designated initializer from the super class. +- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + FSTFail(@"Use FIRCollectionReference initWithPath: initializer."); +} + +- (NSString *)collectionID { + return [self.query.path lastSegment]; +} + +- (FIRDocumentReference *_Nullable)parent { + FSTResourcePath *parentPath = [self.query.path pathByRemovingLastSegment]; + if (parentPath.isEmpty) { + return nil; + } else { + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:parentPath]; + return [FIRDocumentReference referenceWithKey:key firestore:self.firestore]; + } +} + +- (NSString *)path { + return [self.query.path canonicalString]; +} + +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { + if (!documentPath) { + FSTThrowInvalidArgument(@"Document path cannot be nil."); + } + FSTResourcePath *subPath = [FSTResourcePath pathWithString:documentPath]; + FSTResourcePath *path = [self.query.path pathByAppendingPath:subPath]; + return [FIRDocumentReference referenceWithPath:path firestore:self.firestore]; +} + +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data { + return [self addDocumentWithData:data completion:nil]; +} + +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data + completion: + (nullable void (^)(NSError *_Nullable error))completion { + FIRDocumentReference *docRef = [self documentWithAutoID]; + [docRef setData:data completion:completion]; + return docRef; +} + +- (FIRDocumentReference *)documentWithAutoID { + NSString *autoID = [FSTUtil autoID]; + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:autoID]]; + return [FIRDocumentReference referenceWithKey:key firestore:self.firestore]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentChange+Internal.h b/Firestore/Source/API/FIRDocumentChange+Internal.h new file mode 100644 index 0000000..7e2e5c6 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentChange+Internal.h @@ -0,0 +1,32 @@ +/* + * 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 "FIRDocumentChange.h" + +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRDocumentChange API we don't want exposed in our public header files. */ +@interface FIRDocumentChange (Internal) + +/** Calculates the array of FIRDocumentChange's based on the given FSTViewSnapshot. */ ++ (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + firestore:(FIRFirestore *)firestore; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentChange.m b/Firestore/Source/API/FIRDocumentChange.m new file mode 100644 index 0000000..f284bfe --- /dev/null +++ b/Firestore/Source/API/FIRDocumentChange.m @@ -0,0 +1,129 @@ +/* + * 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 "FIRDocumentChange.h" + +#import "FIRDocumentSnapshot+Internal.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentSet.h" +#import "FSTQuery.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentChange () + +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRDocumentChange (Internal) + ++ (FIRDocumentChangeType)documentChangeTypeForChange:(FSTDocumentViewChange *)change { + if (change.type == FSTDocumentViewChangeTypeAdded) { + return FIRDocumentChangeTypeAdded; + } else if (change.type == FSTDocumentViewChangeTypeModified || + change.type == FSTDocumentViewChangeTypeMetadata) { + return FIRDocumentChangeTypeModified; + } else if (change.type == FSTDocumentViewChangeTypeRemoved) { + return FIRDocumentChangeTypeRemoved; + } else { + FSTFail(@"Unknown FSTDocumentViewChange: %ld", (long)change.type); + } +} + ++ (NSArray<FIRDocumentChange *> *)documentChangesForSnapshot:(FSTViewSnapshot *)snapshot + firestore:(FIRFirestore *)firestore { + if (snapshot.oldDocuments.isEmpty) { + // Special case the first snapshot because index calculation is easy and fast + FSTDocument *_Nullable lastDocument = nil; + 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]; + FSTAssert(change.type == FSTDocumentViewChangeTypeAdded, + @"Invalid event type for first snapshot"); + FSTAssert(!lastDocument || + snapshot.query.comparator(lastDocument, change.document) == NSOrderedAscending, + @"Got added events in wrong order"); + [changes addObject:[[FIRDocumentChange alloc] initWithType:FIRDocumentChangeTypeAdded + document:document + oldIndex:NSNotFound + newIndex:index++]]; + } + return changes; + } else { + // A DocumentSet that is updated incrementally as changes are applied to use to lookup the index + // of a document. + 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]; + + NSUInteger oldIndex = NSNotFound; + NSUInteger newIndex = NSNotFound; + if (change.type != FSTDocumentViewChangeTypeAdded) { + oldIndex = [indexTracker indexOfKey:change.document.key]; + FSTAssert(oldIndex != NSNotFound, @"Index for document not found"); + indexTracker = [indexTracker documentSetByRemovingKey:change.document.key]; + } + if (change.type != FSTDocumentViewChangeTypeRemoved) { + indexTracker = [indexTracker documentSetByAddingDocument:change.document]; + newIndex = [indexTracker indexOfKey:change.document.key]; + } + [FIRDocumentChange documentChangeTypeForChange:change]; + FIRDocumentChangeType type = [FIRDocumentChange documentChangeTypeForChange:change]; + [changes addObject:[[FIRDocumentChange alloc] initWithType:type + document:document + oldIndex:oldIndex + newIndex:newIndex]]; + } + return changes; + } +} + +@end + +@implementation FIRDocumentChange + +- (instancetype)initWithType:(FIRDocumentChangeType)type + document:(FIRDocumentSnapshot *)document + oldIndex:(NSUInteger)oldIndex + newIndex:(NSUInteger)newIndex { + if (self = [super init]) { + _type = type; + _document = document; + _oldIndex = oldIndex; + _newIndex = newIndex; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference+Internal.h b/Firestore/Source/API/FIRDocumentReference+Internal.h new file mode 100644 index 0000000..5e12ddc --- /dev/null +++ b/Firestore/Source/API/FIRDocumentReference+Internal.h @@ -0,0 +1,34 @@ +/* + * 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 "FIRDocumentReference.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTDocumentKey; +@class FSTResourcePath; + +/** Internal FIRDocumentReference API we don't want exposed in our public header files. */ +@interface FIRDocumentReference (Internal) + ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore; ++ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore; + +@property(nonatomic, strong, readonly) FSTDocumentKey *key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentReference.m b/Firestore/Source/API/FIRDocumentReference.m new file mode 100644 index 0000000..5515bd6 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentReference.m @@ -0,0 +1,285 @@ +/* + * 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 "FIRDocumentReference.h" + +#import "FIRCollectionReference+Internal.h" +#import "FIRDocumentReference+Internal.h" +#import "FIRDocumentSnapshot+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRFirestoreErrors.h" +#import "FIRListenerRegistration+Internal.h" +#import "FIRSetOptions+Internal.h" +#import "FIRSnapshotMetadata.h" +#import "FSTAssert.h" +#import "FSTAsyncQueryListener.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTEventManager.h" +#import "FSTFieldValue.h" +#import "FSTFirestoreClient.h" +#import "FSTMutation.h" +#import "FSTPath.h" +#import "FSTQuery.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +#import <GRPCClient/GRPCCall.h> + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRDocumentListenOptions + +@interface FIRDocumentListenOptions () + +- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRDocumentListenOptions + ++ (instancetype)options { + return [[FIRDocumentListenOptions alloc] init]; +} + +- (instancetype)initWithIncludeMetadataChanges:(BOOL)includeMetadataChanges { + if (self = [super init]) { + _includeMetadataChanges = includeMetadataChanges; + } + return self; +} + +- (instancetype)init { + return [self initWithIncludeMetadataChanges:NO]; +} + +- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges { + return [[FIRDocumentListenOptions alloc] initWithIncludeMetadataChanges:includeMetadataChanges]; +} + +@end + +#pragma mark - FIRDocumentReference + +@interface FIRDocumentReference () +- (instancetype)initWithKey:(FSTDocumentKey *)key + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@end + +@implementation FIRDocumentReference (Internal) + ++ (instancetype)referenceWithPath:(FSTResourcePath *)path firestore:(FIRFirestore *)firestore { + if (path.length % 2 != 0) { + FSTThrowInvalidArgument( + @"Invalid document reference. Document references must have an even " + "number of segments, but %@ has %d", + path.canonicalString, path.length); + } + return + [FIRDocumentReference referenceWithKey:[FSTDocumentKey keyWithPath:path] firestore:firestore]; +} + ++ (instancetype)referenceWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { + return [[FIRDocumentReference alloc] initWithKey:key firestore:firestore]; +} + +@end + +@implementation FIRDocumentReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key firestore:(FIRFirestore *)firestore { + if (self = [super init]) { + _key = key; + _firestore = firestore; + } + return self; +} + +- (NSString *)documentID { + return [self.key.path lastSegment]; +} + +- (FIRCollectionReference *)parent { + FSTResourcePath *parentPath = [self.key.path pathByRemovingLastSegment]; + return [FIRCollectionReference referenceWithPath:parentPath firestore:self.firestore]; +} + +- (NSString *)path { + return [self.key.path canonicalString]; +} + +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath { + if (!collectionPath) { + FSTThrowInvalidArgument(@"Collection path cannot be nil."); + } + FSTResourcePath *subPath = [FSTResourcePath pathWithString:collectionPath]; + FSTResourcePath *path = [self.key.path pathByAppendingPath:subPath]; + return [FIRCollectionReference referenceWithPath:path firestore:self.firestore]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:nil]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData options:(FIRSetOptions *)options { + return [self setData:documentData options:options completion:nil]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData + completion:(nullable void (^)(NSError *_Nullable error))completion { + return [self setData:documentData options:[FIRSetOptions overwrite] completion:completion]; +} + +- (void)setData:(NSDictionary<NSString *, id> *)documentData + options:(FIRSetOptions *)options + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedSetData *parsed = + [self.firestore.dataConverter parsedSetData:documentData options:options]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key precondition:[FSTPrecondition none]] + completion:completion]; +} + +- (void)updateData:(NSDictionary<id, id> *)fields { + return [self updateData:fields completion:nil]; +} + +- (void)updateData:(NSDictionary<id, id> *)fields + completion:(nullable void (^)(NSError *_Nullable error))completion { + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + return [self.firestore.client + writeMutations:[parsed mutationsWithKey:self.key + precondition:[FSTPrecondition preconditionWithExists:YES]] + completion:completion]; +} + +- (void)deleteDocument { + return [self deleteDocumentWithCompletion:nil]; +} + +- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + FSTDeleteMutation *mutation = + [[FSTDeleteMutation alloc] initWithKey:self.key precondition:[FSTPrecondition none]]; + return [self.firestore.client writeMutations:@[ mutation ] completion:completion]; +} + +- (void)getDocumentWithCompletion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + FSTListenOptions *listenOptions = + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; + + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + __block id<FIRListenerRegistration> listenerRegistration; + FIRDocumentSnapshotBlock listener = ^(FIRDocumentSnapshot *snapshot, NSError *error) { + if (error) { + completion(nil, error); + return; + } + + // Remove query first before passing event to user to avoid user actions affecting the + // now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [listenerRegistration remove]; + + if (!snapshot.exists && snapshot.metadata.fromCache) { + // TODO(dimond): Reconsider how to raise missing documents when offline. + // If we're online and the document doesn't exist then we call the completion with + // a document with document.exists set to false. If we're offline however, we call the + // completion handler with an error. Two options: + // 1) Cache the negative response from the server so we can deliver that even when you're + // offline. + // 2) Actually call the completion handler with an error if the document doesn't exist when + // you are offline. + // TODO(dimond): Use proper error domain + completion(nil, + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnavailable + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed to get document because the client is offline.", + }]); + } else { + completion(snapshot, nil); + } + }; + + listenerRegistration = + [self addSnapshotListenerInternalWithOptions:listenOptions listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id<FIRListenerRegistration>)addSnapshotListener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions: + (nullable FIRDocumentListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id<FIRListenerRegistration>) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRDocumentSnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = [FSTQuery queryWithPath:self.key.path]; + FSTDocumentKey *key = self.key; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FSTAssert(snapshot.documents.count <= 1, @"Too many document returned on a document query"); + FSTDocument *document = [snapshot.documents documentForKey:key]; + + FIRDocumentSnapshot *result = [FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:key + document:document + fromCache:snapshot.fromCache]; + listener(result, nil); + }; + + FSTAsyncQueryListener *asyncListener = + [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue + snapshotHandler:snapshotHandler]; + + FSTQueryListener *internalListener = + [firestore.client listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client + asyncListener:asyncListener + internalListener:internalListener]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRDocumentListenOptions *)options { + return + [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:options.includeMetadataChanges + includeDocumentMetadataChanges:options.includeMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot+Internal.h b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h new file mode 100644 index 0000000..f2776f0 --- /dev/null +++ b/Firestore/Source/API/FIRDocumentSnapshot+Internal.h @@ -0,0 +1,37 @@ +/* + * 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" + +@class FIRFirestore; +@class FSTDocument; +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRDocumentSnapshot API we don't want exposed in our public header files. */ +@interface FIRDocumentSnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache; + +@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRDocumentSnapshot.m b/Firestore/Source/API/FIRDocumentSnapshot.m new file mode 100644 index 0000000..b5f61ba --- /dev/null +++ b/Firestore/Source/API/FIRDocumentSnapshot.m @@ -0,0 +1,175 @@ +/* + * 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 "FIRDocumentReference+Internal.h" +#import "FIRFieldPath+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRSnapshotMetadata+Internal.h" +#import "FSTDatabaseID.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTPath.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRDocumentSnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) FSTDocumentKey *internalKey; +@property(nonatomic, strong, readonly, nullable) FSTDocument *internalDocument; +@property(nonatomic, assign, readonly) BOOL fromCache; + +@end + +@implementation FIRDocumentSnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache { + return [[FIRDocumentSnapshot alloc] initWithFirestore:firestore + documentKey:documentKey + document:document + fromCache:fromCache]; +} + +@end + +@implementation FIRDocumentSnapshot { + FIRSnapshotMetadata *_cachedMetadata; +} + +@dynamic metadata; + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTDocument *)document + fromCache:(BOOL)fromCache { + if (self = [super init]) { + _firestore = firestore; + _internalKey = documentKey; + _internalDocument = document; + _fromCache = fromCache; + } + return self; +} + +@dynamic exists; + +- (BOOL)exists { + return _internalDocument != nil; +} + +- (FIRDocumentReference *)reference { + return [FIRDocumentReference referenceWithKey:self.internalKey firestore:self.firestore]; +} + +- (NSString *)documentID { + return [self.internalKey.path lastSegment]; +} + +- (FIRSnapshotMetadata *)metadata { + if (!_cachedMetadata) { + _cachedMetadata = [FIRSnapshotMetadata + snapshotMetadataWithPendingWrites:self.internalDocument.hasLocalMutations + fromCache:self.fromCache]; + } + 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); + } + + return [self convertedObject:[self.internalDocument data]]; +} + +- (nullable id)objectForKeyedSubscript:(id)key { + FIRFieldPath *fieldPath; + + if ([key isKindOfClass:[NSString class]]) { + fieldPath = [FIRFieldPath pathWithDotSeparatedString:key]; + } else if ([key isKindOfClass:[FIRFieldPath class]]) { + fieldPath = key; + } else { + FSTThrowInvalidArgument(@"Subscript key must be an NSString or FIRFieldPath."); + } + + FSTFieldValue *fieldValue = [[self.internalDocument data] valueForPath:fieldPath.internalValue]; + return [self convertedValue:fieldValue]; +} + +- (id)convertedValue:(FSTFieldValue *)value { + if ([value isKindOfClass:[FSTObjectValue class]]) { + return [self convertedObject:(FSTObjectValue *)value]; + } else if ([value isKindOfClass:[FSTArrayValue class]]) { + return [self convertedArray:(FSTArrayValue *)value]; + } else if ([value isKindOfClass:[FSTReferenceValue class]]) { + FSTReferenceValue *ref = (FSTReferenceValue *)value; + FSTDatabaseID *refDatabase = ref.databaseID; + FSTDatabaseID *database = self.firestore.databaseID; + if (![refDatabase isEqualToDatabaseId:database]) { + // TODO(b/32073923): Log this as a proper warning. + NSLog( + @"WARNING: Document %@ contains a document reference within a different database " + "(%@/%@) which is not supported. It will be treated as a reference within the " + "current database (%@/%@) instead.", + self.reference.path, refDatabase.projectID, refDatabase.databaseID, database.projectID, + database.databaseID); + } + return [FIRDocumentReference referenceWithKey:ref.value firestore:self.firestore]; + } else { + return value.value; + } +} + +- (NSDictionary<NSString *, id> *)convertedObject:(FSTObjectValue *)objectValue { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [objectValue.internalValue + enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *value, BOOL *stop) { + result[key] = [self convertedValue:value]; + }]; + return result; +} + +- (NSArray<id> *)convertedArray:(FSTArrayValue *)arrayValue { + NSArray<FSTFieldValue *> *internalValue = arrayValue.internalValue; + NSMutableArray *result = [NSMutableArray arrayWithCapacity:internalValue.count]; + [internalValue enumerateObjectsUsingBlock:^(id value, NSUInteger idx, BOOL *stop) { + [result addObject:[self convertedValue:value]]; + }]; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath+Internal.h b/Firestore/Source/API/FIRFieldPath+Internal.h new file mode 100644 index 0000000..227cdad --- /dev/null +++ b/Firestore/Source/API/FIRFieldPath+Internal.h @@ -0,0 +1,39 @@ +/* + * 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 "FIRFieldPath.h" + +@class FSTFieldPath; + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldPath () + +- (instancetype)initPrivate:(FSTFieldPath *)path NS_DESIGNATED_INITIALIZER; + +/** Internal field path representation */ +@property(nonatomic, strong, readonly) FSTFieldPath *internalValue; + +@end + +/** Internal FIRFieldPath API we don't want exposed in our public header files. */ +@interface FIRFieldPath (Internal) + ++ (instancetype)pathWithDotSeparatedString:(NSString *)path; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldPath.m b/Firestore/Source/API/FIRFieldPath.m new file mode 100644 index 0000000..b3c919c --- /dev/null +++ b/Firestore/Source/API/FIRFieldPath.m @@ -0,0 +1,101 @@ +/* + * 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 "FIRFieldPath+Internal.h" + +#import "FSTPath.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRFieldPath + +- (instancetype)initWithFields:(NSArray<NSString *> *)fieldNames { + if (fieldNames.count == 0) { + FSTThrowInvalidArgument(@"Invalid field path. Provided names must not be empty."); + } + + for (int i = 0; i < fieldNames.count; ++i) { + if (fieldNames[i].length == 0) { + FSTThrowInvalidArgument(@"Invalid field name at index %d. Field names must not be empty.", i); + } + } + + return [self initPrivate:[FSTFieldPath pathWithSegments:fieldNames]]; +} + ++ (instancetype)documentID { + return [[FIRFieldPath alloc] initPrivate:FSTFieldPath.keyFieldPath]; +} + +- (instancetype)initPrivate:(FSTFieldPath *)fieldPath { + if (self = [super init]) { + _internalValue = fieldPath; + } + return self; +} + ++ (instancetype)pathWithDotSeparatedString:(NSString *)path { + if ([[FIRFieldPath reservedCharactersRegex] + numberOfMatchesInString:path + options:0 + range:NSMakeRange(0, path.length)] > 0) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not contain '~', '*', '/', '[', or ']'", path); + } + @try { + return [[FIRFieldPath alloc] initWithFields:[path componentsSeparatedByString:@"."]]; + } @catch (NSException *exception) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not be empty, begin with '.', end with '.', or " + @"contain '..'", + path); + } +} + +/** Matches any characters in a field path string that are reserved. */ ++ (NSRegularExpression *)reservedCharactersRegex { + static NSRegularExpression *regex = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + regex = [NSRegularExpression regularExpressionWithPattern:@"[~*/\\[\\]]" options:0 error:nil]; + }); + return regex; +} + +- (id)copyWithZone:(NSZone *__nullable)zone { + return [[[self class] alloc] initPrivate:self.internalValue]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FIRFieldPath class]]) { + return NO; + } + + return [self.internalValue isEqual:((FIRFieldPath *)object).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue+Internal.h b/Firestore/Source/API/FIRFieldValue+Internal.h new file mode 100644 index 0000000..1b4a99c --- /dev/null +++ b/Firestore/Source/API/FIRFieldValue+Internal.h @@ -0,0 +1,37 @@ +/* + * 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 "FIRFieldValue.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FIRFieldValue class for field deletes. Exposed internally so code can do isKindOfClass checks on + * it. + */ +@interface FSTDeleteFieldValue : FIRFieldValue +- (instancetype)init NS_UNAVAILABLE; +@end + +/** + * FIRFieldValue class for server timestamps. Exposed internally so code can do isKindOfClass checks + * on it. + */ +@interface FSTServerTimestampFieldValue : FIRFieldValue +- (instancetype)init NS_UNAVAILABLE; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFieldValue.m b/Firestore/Source/API/FIRFieldValue.m new file mode 100644 index 0000000..a44d8fa --- /dev/null +++ b/Firestore/Source/API/FIRFieldValue.m @@ -0,0 +1,96 @@ +/* + * 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 "FIRFieldValue+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRFieldValue () +- (instancetype)initPrivate NS_DESIGNATED_INITIALIZER; +@end + +#pragma mark - FSTDeleteFieldValue + +@interface FSTDeleteFieldValue () +/** Returns a single shared instance of the class. */ ++ (instancetype)deleteFieldValue; +@end + +@implementation FSTDeleteFieldValue + +- (instancetype)initPrivate { + self = [super initPrivate]; + return self; +} + ++ (instancetype)deleteFieldValue { + static FSTDeleteFieldValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTDeleteFieldValue alloc] initPrivate]; + }); + return sharedInstance; +} + +@end + +#pragma mark - FSTServerTimestampFieldValue + +@interface FSTServerTimestampFieldValue () +/** Returns a single shared instance of the class. */ ++ (instancetype)serverTimestampFieldValue; +@end + +@implementation FSTServerTimestampFieldValue + +- (instancetype)initPrivate { + self = [super initPrivate]; + return self; +} + ++ (instancetype)serverTimestampFieldValue { + static FSTServerTimestampFieldValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTServerTimestampFieldValue alloc] initPrivate]; + }); + return sharedInstance; +} + +@end + +#pragma mark - FIRFieldValue + +@implementation FIRFieldValue + +- (instancetype)initPrivate { + self = [super init]; + return self; +} + ++ (instancetype)fieldValueForDelete { + return [FSTDeleteFieldValue deleteFieldValue]; +} + ++ (instancetype)fieldValueForServerTimestamp { + return [FSTServerTimestampFieldValue serverTimestampFieldValue]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore+Internal.h b/Firestore/Source/API/FIRFirestore+Internal.h new file mode 100644 index 0000000..08f5266 --- /dev/null +++ b/Firestore/Source/API/FIRFirestore+Internal.h @@ -0,0 +1,64 @@ +/* + * 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 "FIRFirestore.h" +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTDatabaseID; +@class FSTDispatchQueue; +@class FSTFirestoreClient; +@class FSTUserDataConverter; +@protocol FSTCredentialsProvider; + +@interface FIRFirestore (/* Init */) + +/** + * Initializes a Firestore object with all the required parameters directly. This exists so that + * tests can create FIRFirestore objects without needing FIRApp. + */ +- (instancetype)initWithProjectID:(NSString *)projectID + database:(NSString *)database + persistenceKey:(NSString *)persistenceKey + credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + firebaseApp:(FIRApp *)app; + +@end + +/** Internal FIRFirestore API we don't want exposed in our public header files. */ +@interface FIRFirestore (Internal) + +/** Checks to see if logging is is globally enabled for the Firestore client. */ ++ (BOOL)isLoggingEnabled; + +/** + * Shutdown this `FIRFirestore`, releasing all resources (abandoning any outstanding writes, + * removing all listens, closing all network connections, etc.). + * + * @param completion A block to execute once everything has shut down. + */ +- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion + FIR_SWIFT_NAME(shutdown(completion:)); + +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; +@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestore.m b/Firestore/Source/API/FIRFirestore.m new file mode 100644 index 0000000..e8c4fa6 --- /dev/null +++ b/Firestore/Source/API/FIRFirestore.m @@ -0,0 +1,284 @@ +/* + * 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 "FIRFirestore.h" + +#import <FirebaseCommunity/FIRApp.h> +#import <FirebaseCommunity/FIRLogger.h> +#import <FirebaseCommunity/FIROptions.h> + +#import "FIRCollectionReference+Internal.h" +#import "FIRDocumentReference+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRFirestoreSettings.h" +#import "FIRTransaction+Internal.h" +#import "FIRWriteBatch+Internal.h" +#import "FSTUserDataConverter.h" + +#import "FSTAssert.h" +#import "FSTCredentialsProvider.h" +#import "FSTDatabaseID.h" +#import "FSTDatabaseInfo.h" +#import "FSTDispatchQueue.h" +#import "FSTDocumentKey.h" +#import "FSTFirestoreClient.h" +#import "FSTLogger.h" +#import "FSTPath.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +NSString *const FIRFirestoreErrorDomain = @"FIRFirestoreErrorDomain"; + +@interface FIRFirestore () + +@property(nonatomic, strong) FSTDatabaseID *databaseID; +@property(nonatomic, strong) NSString *persistenceKey; +@property(nonatomic, strong) id<FSTCredentialsProvider> credentialsProvider; +@property(nonatomic, strong) FSTDispatchQueue *workerDispatchQueue; + +@property(nonatomic, strong) FSTFirestoreClient *client; +@property(nonatomic, strong, readonly) FSTUserDataConverter *dataConverter; + +@end + +@implementation FIRFirestore { + FIRFirestoreSettings *_settings; +} + ++ (NSMutableDictionary<NSString *, FIRFirestore *> *)instances { + static dispatch_once_t token = 0; + static NSMutableDictionary<NSString *, FIRFirestore *> *instances; + dispatch_once(&token, ^{ + instances = [NSMutableDictionary dictionary]; + }); + return instances; +} + ++ (instancetype)firestore { + FIRApp *app = [FIRApp defaultApp]; + if (!app) { + FSTThrowInvalidUsage( + @"FIRAppNotConfiguredException", + @"Failed to get FIRApp instance. Please call FIRApp.configure() before using FIRFirestore"); + } + return [self firestoreForApp:app database:kDefaultDatabaseID]; +} + ++ (instancetype)firestoreForApp:(FIRApp *)app { + return [self firestoreForApp:app database:kDefaultDatabaseID]; +} + +// TODO(b/62410906): make this public ++ (instancetype)firestoreForApp:(FIRApp *)app database:(NSString *)database { + if (!app) { + FSTThrowInvalidArgument( + @"FirebaseApp instance may not be nil. Use FirebaseApp.app() if you'd " + "like to use the default FirebaseApp instance."); + } + if (!database) { + FSTThrowInvalidArgument( + @"database identifier may not be nil. Use '%@' if you want the default " + "database", + kDefaultDatabaseID); + } + NSString *key = [NSString stringWithFormat:@"%@|%@", app.name, database]; + + NSMutableDictionary<NSString *, FIRFirestore *> *instances = self.instances; + @synchronized(instances) { + FIRFirestore *firestore = instances[key]; + if (!firestore) { + NSString *projectID = app.options.projectID; + FSTAssert(projectID, @"FIROptions.projectID cannot be nil."); + + FSTDispatchQueue *workerDispatchQueue = [FSTDispatchQueue + queueWith:dispatch_queue_create("com.google.firebase.firestore", DISPATCH_QUEUE_SERIAL)]; + + id<FSTCredentialsProvider> credentialsProvider; + credentialsProvider = [[FSTFirebaseCredentialsProvider alloc] initWithApp:app]; + + NSString *persistenceKey = app.name; + + firestore = [[FIRFirestore alloc] initWithProjectID:projectID + database:database + persistenceKey:persistenceKey + credentialsProvider:credentialsProvider + workerDispatchQueue:workerDispatchQueue + firebaseApp:app]; + instances[key] = firestore; + } + + return firestore; + } +} + +- (instancetype)initWithProjectID:(NSString *)projectID + database:(NSString *)database + persistenceKey:(NSString *)persistenceKey + credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + firebaseApp:(FIRApp *)app { + if (self = [super init]) { + _databaseID = [FSTDatabaseID databaseIDWithProject:projectID database:database]; + FSTPreConverterBlock block = ^id _Nullable(id _Nullable input) { + if ([input isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *documentReference = (FIRDocumentReference *)input; + return [[FSTDocumentKeyReference alloc] initWithKey:documentReference.key + databaseID:documentReference.firestore.databaseID]; + } else { + return input; + } + }; + _dataConverter = + [[FSTUserDataConverter alloc] initWithDatabaseID:_databaseID preConverter:block]; + _persistenceKey = persistenceKey; + _credentialsProvider = credentialsProvider; + _workerDispatchQueue = workerDispatchQueue; + _app = app; + _settings = [[FIRFirestoreSettings alloc] init]; + } + return self; +} + +- (FIRFirestoreSettings *)settings { + // 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."); + } + _settings = [settings copy]; +} + +/** + * Ensures that the FirestoreClient is configured. + * @return self + */ +- (instancetype)firestoreWithConfiguredClient { + if (!_client) { + // These values are validated elsewhere; this is just double-checking: + FSTAssert(_settings.host, @"FIRFirestoreSettings.host cannot be nil."); + FSTAssert(_settings.dispatchQueue, @"FIRFirestoreSettings.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."); + } + FSTResourcePath *path = [FSTResourcePath pathWithString:collectionPath]; + return + [FIRCollectionReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient]; +} + +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath { + if (!documentPath) { + FSTThrowInvalidArgument(@"Document path cannot be nil."); + } + FSTResourcePath *path = [FSTResourcePath pathWithString:documentPath]; + return [FIRDocumentReference referenceWithPath:path firestore:self.firestoreWithConfiguredClient]; +} + +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock + dispatchQueue:(dispatch_queue_t)queue + completion: + (void (^)(id _Nullable result, NSError *_Nullable error))completion { + // We wrap the function they provide in order to use internal implementation classes for + // FSTTransaction, and to run the user callback block on the proper queue. + if (!updateBlock) { + FSTThrowInvalidArgument(@"Transaction block cannot be nil."); + } else if (!completion) { + FSTThrowInvalidArgument(@"Transaction completion block cannot be nil."); + } + + FSTTransactionBlock wrappedUpdate = + ^(FSTTransaction *internalTransaction, + void (^internalCompletion)(id _Nullable, NSError *_Nullable)) { + FIRTransaction *transaction = + [FIRTransaction transactionWithFSTTransaction:internalTransaction firestore:self]; + dispatch_async(queue, ^{ + NSError *_Nullable error = nil; + id _Nullable result = updateBlock(transaction, &error); + if (error) { + // Force the result to be nil in the case of an error, in case the user set both. + result = nil; + } + internalCompletion(result, error); + }); + }; + [self firestoreWithConfiguredClient]; + [self.client transactionWithRetries:5 updateBlock:wrappedUpdate completion:completion]; +} + +- (FIRWriteBatch *)batch { + return [FIRWriteBatch writeBatchWithFirestore:[self firestoreWithConfiguredClient]]; +} + +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **error))updateBlock + completion: + (void (^)(id _Nullable result, NSError *_Nullable error))completion { + static dispatch_queue_t transactionDispatchQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + transactionDispatchQueue = dispatch_queue_create("com.google.firebase.firestore.transaction", + DISPATCH_QUEUE_CONCURRENT); + }); + [self runTransactionWithBlock:updateBlock + dispatchQueue:transactionDispatchQueue + completion:completion]; +} + +- (void)shutdownWithCompletion:(nullable void (^)(NSError *_Nullable error))completion { + if (!self.client) { + completion(nil); + return; + } + return [self.client shutdownWithCompletion:completion]; +} + ++ (BOOL)isLoggingEnabled { + return FIRIsLoggableLevel(FIRLoggerLevelDebug, NO); +} + ++ (void)enableLogging:(BOOL)logging { + FIRSetLoggerLevel(logging ? FIRLoggerLevelDebug : FIRLoggerLevelNotice); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreSettings.m b/Firestore/Source/API/FIRFirestoreSettings.m new file mode 100644 index 0000000..106a0b5 --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreSettings.m @@ -0,0 +1,92 @@ +/* + * 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 "FIRFirestoreSettings.h" + +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kDefaultHost = @"firestore.googleapis.com"; +static const BOOL kDefaultSSLEnabled = YES; +static const BOOL kDefaultPersistenceEnabled = YES; + +@implementation FIRFirestoreSettings + +- (instancetype)init { + if (self = [super init]) { + _host = kDefaultHost; + _sslEnabled = kDefaultSSLEnabled; + _dispatchQueue = dispatch_get_main_queue(); + _persistenceEnabled = kDefaultPersistenceEnabled; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FIRFirestoreSettings class]]) { + return NO; + } + + FIRFirestoreSettings *otherSettings = (FIRFirestoreSettings *)other; + return [self.host isEqual:otherSettings.host] && + self.isSSLEnabled == otherSettings.isSSLEnabled && + self.dispatchQueue == otherSettings.dispatchQueue && + self.isPersistenceEnabled == otherSettings.isPersistenceEnabled; +} + +- (NSUInteger)hash { + NSUInteger result = [self.host hash]; + result = 31 * result + (self.isSSLEnabled ? 1231 : 1237); + // Ignore the dispatchQueue to avoid having to deal with sizeof(dispatch_queue_t). + result = 31 * result + (self.isPersistenceEnabled ? 1231 : 1237); + return result; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + FIRFirestoreSettings *copy = [[FIRFirestoreSettings alloc] init]; + copy.host = _host; + copy.sslEnabled = _sslEnabled; + copy.dispatchQueue = _dispatchQueue; + copy.persistenceEnabled = _persistenceEnabled; + return copy; +} + +- (void)setHost:(NSString *)host { + if (!host) { + FSTThrowInvalidArgument( + @"host setting may not be nil. You should generally just use the default value " + "(which is %@)", + kDefaultHost); + } + _host = [host mutableCopy]; +} + +- (void)setDispatchQueue:(dispatch_queue_t)dispatchQueue { + if (!dispatchQueue) { + FSTThrowInvalidArgument( + @"dispatch queue setting may not be nil. Create a new dispatch queue with " + "dispatch_queue_create(\"com.example.MyQueue\", NULL) or just use the default " + "(which is the main queue, returned from dispatch_get_main_queue())"); + } + _dispatchQueue = dispatchQueue; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRFirestoreVersion.h b/Firestore/Source/API/FIRFirestoreVersion.h new file mode 100644 index 0000000..6fb21eb --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreVersion.h @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** Version for Firestore. */ + +#import <Foundation/Foundation.h> + +/** Version string for the Firebase Firestore SDK. */ +FOUNDATION_EXPORT const unsigned char *const FirebaseFirestoreVersionString; diff --git a/Firestore/Source/API/FIRFirestoreVersion.m b/Firestore/Source/API/FIRFirestoreVersion.m new file mode 100644 index 0000000..4f8bb28 --- /dev/null +++ b/Firestore/Source/API/FIRFirestoreVersion.m @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#ifndef FIRFirestore_VERSION +#error "FIRFirestore_VERSION is not defined: add -DFIRFirestore_VERSION=... to the build invocation" +#endif + +// The following two macros supply the incantation so that the C +// preprocessor does not try to parse the version as a floating +// point number. See +// https://www.guyrutenberg.com/2008/12/20/expanding-macros-into-string-constants-in-c/ +#define STR(x) STR_EXPAND(x) +#define STR_EXPAND(x) #x + +const unsigned char *const FirebaseFirestoreVersionString = + (const unsigned char *const)STR(FIRFirestore_VERSION); diff --git a/Firestore/Source/API/FIRGeoPoint+Internal.h b/Firestore/Source/API/FIRGeoPoint+Internal.h new file mode 100644 index 0000000..6eb8548 --- /dev/null +++ b/Firestore/Source/API/FIRGeoPoint+Internal.h @@ -0,0 +1,26 @@ +/* + * 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 "FIRGeoPoint.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRGeoPoint API we don't want exposed in our public header files. */ +@interface FIRGeoPoint (Internal) +- (NSComparisonResult)compare:(FIRGeoPoint *)other; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRGeoPoint.m b/Firestore/Source/API/FIRGeoPoint.m new file mode 100644 index 0000000..a50cf37 --- /dev/null +++ b/Firestore/Source/API/FIRGeoPoint.m @@ -0,0 +1,85 @@ +/* + * 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 "FIRGeoPoint+Internal.h" + +#import "FSTComparison.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRGeoPoint + +- (instancetype)initWithLatitude:(double)latitude longitude:(double)longitude { + if (self = [super init]) { + if (latitude < -90 || latitude > 90 || !isfinite(latitude)) { + FSTThrowInvalidArgument( + @"GeoPoint requires a latitude value in the range of [-90, 90], " + "but was %f", + latitude); + } + if (longitude < -180 || longitude > 180 || !isfinite(longitude)) { + FSTThrowInvalidArgument( + @"GeoPoint requires a longitude value in the range of [-180, 180], " + "but was %f", + longitude); + } + + _latitude = latitude; + _longitude = longitude; + } + return self; +} + +- (NSComparisonResult)compare:(FIRGeoPoint *)other { + NSComparisonResult result = FSTCompareDoubles(self.latitude, other.latitude); + if (result != NSOrderedSame) { + return result; + } else { + return FSTCompareDoubles(self.longitude, other.longitude); + } +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"<FIRGeoPoint: (%f, %f)>", self.latitude, self.longitude]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FIRGeoPoint class]]) { + return NO; + } + FIRGeoPoint *otherGeoPoint = (FIRGeoPoint *)other; + return FSTDoubleBitwiseEquals(self.latitude, otherGeoPoint.latitude) && + FSTDoubleBitwiseEquals(self.longitude, otherGeoPoint.longitude); +} + +- (NSUInteger)hash { + return 31 * FSTDoubleBitwiseHash(self.latitude) + FSTDoubleBitwiseHash(self.longitude); +} + +/** Implements NSCopying without actually copying because geopoints are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRListenerRegistration+Internal.h b/Firestore/Source/API/FIRListenerRegistration+Internal.h new file mode 100644 index 0000000..4cd2d57 --- /dev/null +++ b/Firestore/Source/API/FIRListenerRegistration+Internal.h @@ -0,0 +1,34 @@ +/* + * 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 "FIRListenerRegistration.h" + +@class FSTAsyncQueryListener; +@class FSTFirestoreClient; +@class FSTQueryListener; + +NS_ASSUME_NONNULL_BEGIN + +/** Private implementation of the FIRListenerRegistration protocol. */ +@interface FSTListenerRegistration : NSObject <FIRListenerRegistration> + +- (instancetype)initWithClient:(FSTFirestoreClient *)client + asyncListener:(FSTAsyncQueryListener *)asyncListener + internalListener:(FSTQueryListener *)internalListener; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRListenerRegistration.m b/Firestore/Source/API/FIRListenerRegistration.m new file mode 100644 index 0000000..9ce0127 --- /dev/null +++ b/Firestore/Source/API/FIRListenerRegistration.m @@ -0,0 +1,57 @@ +/* + * 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 "FIRListenerRegistration+Internal.h" + +#import "FSTAsyncQueryListener.h" +#import "FSTFirestoreClient.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTListenerRegistration () + +/** The client that was used to register this listen. */ +@property(nonatomic, strong, readonly) FSTFirestoreClient *client; + +/** The async listener that is used to mute events synchronously. */ +@property(nonatomic, strong, readonly) FSTAsyncQueryListener *asyncListener; + +/** The internal FSTQueryListener that can be used to unlisten the query. */ +@property(nonatomic, strong, readwrite) FSTQueryListener *internalListener; + +@end + +@implementation FSTListenerRegistration + +- (instancetype)initWithClient:(FSTFirestoreClient *)client + asyncListener:(FSTAsyncQueryListener *)asyncListener + internalListener:(FSTQueryListener *)internalListener { + if (self = [super init]) { + _client = client; + _asyncListener = asyncListener; + _internalListener = internalListener; + } + return self; +} + +- (void)remove { + [self.asyncListener mute]; + [self.client removeListener:self.internalListener]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery+Internal.h b/Firestore/Source/API/FIRQuery+Internal.h new file mode 100644 index 0000000..3c2b2a7 --- /dev/null +++ b/Firestore/Source/API/FIRQuery+Internal.h @@ -0,0 +1,29 @@ +/* + * 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 "FIRQuery.h" + +@class FSTQuery; + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRQuery API we don't want exposed in our public header files. */ +@interface FIRQuery (Internal) ++ (FIRQuery *)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore; +@property(nonatomic, strong, readonly) FSTQuery *query; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery.m b/Firestore/Source/API/FIRQuery.m new file mode 100644 index 0000000..63244fd --- /dev/null +++ b/Firestore/Source/API/FIRQuery.m @@ -0,0 +1,520 @@ +/* + * 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 "FIRQuery.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRDocumentReference.h" +#import "FIRDocumentSnapshot+Internal.h" +#import "FIRFieldPath+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRListenerRegistration+Internal.h" +#import "FIRQuery+Internal.h" +#import "FIRQuerySnapshot+Internal.h" +#import "FIRQuery_Init.h" +#import "FIRSnapshotMetadata+Internal.h" +#import "FSTAssert.h" +#import "FSTAsyncQueryListener.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTEventManager.h" +#import "FSTFieldValue.h" +#import "FSTFirestoreClient.h" +#import "FSTPath.h" +#import "FSTQuery.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQueryListenOptions () + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FIRQueryListenOptions + ++ (instancetype)options { + return [[FIRQueryListenOptions alloc] init]; +} + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { + if (self = [super init]) { + _includeQueryMetadataChanges = includeQueryMetadataChanges; + _includeDocumentMetadataChanges = includeDocumentMetadataChanges; + } + return self; +} + +- (instancetype)init { + return [self initWithIncludeQueryMetadataChanges:NO includeDocumentMetadataChanges:NO]; +} + +- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges { + return [[FIRQueryListenOptions alloc] + initWithIncludeQueryMetadataChanges:includeQueryMetadataChanges + includeDocumentMetadataChanges:_includeDocumentMetadataChanges]; +} + +- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges { + return [[FIRQueryListenOptions alloc] + initWithIncludeQueryMetadataChanges:_includeQueryMetadataChanges + includeDocumentMetadataChanges:includeDocumentMetadataChanges]; +} + +@end + +@interface FIRQuery () +@property(nonatomic, strong, readonly) FSTQuery *query; +@end + +@implementation FIRQuery (Internal) ++ (instancetype)referenceWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + return [[FIRQuery alloc] initWithQuery:query firestore:firestore]; +} +@end + +@implementation FIRQuery + +#pragma mark - Public Methods + +- (instancetype)initWithQuery:(FSTQuery *)query firestore:(FIRFirestore *)firestore { + if (self = [super init]) { + _query = query; + _firestore = firestore; + } + return self; +} + +- (void)getDocumentsWithCompletion:(void (^)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error))completion { + FSTListenOptions *options = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:YES + includeDocumentMetadataChanges:YES + waitForSyncWhenOnline:YES]; + + dispatch_semaphore_t registered = dispatch_semaphore_create(0); + __block id<FIRListenerRegistration> listenerRegistration; + FIRQuerySnapshotBlock listener = ^(FIRQuerySnapshot *snapshot, NSError *error) { + if (error) { + completion(nil, error); + return; + } + + // Remove query first before passing event to user to avoid user actions affecting the + // now stale query. + dispatch_semaphore_wait(registered, DISPATCH_TIME_FOREVER); + [listenerRegistration remove]; + + completion(snapshot, nil); + }; + + listenerRegistration = [self addSnapshotListenerInternalWithOptions:options listener:listener]; + dispatch_semaphore_signal(registered); +} + +- (id<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerWithOptions:nil listener:listener]; +} + +- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions: + (nullable FIRQueryListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener { + return [self addSnapshotListenerInternalWithOptions:[self internalOptions:options] + listener:listener]; +} + +- (id<FIRListenerRegistration>) +addSnapshotListenerInternalWithOptions:(FSTListenOptions *)internalOptions + listener:(FIRQuerySnapshotBlock)listener { + FIRFirestore *firestore = self.firestore; + FSTQuery *query = self.query; + + FSTViewSnapshotHandler snapshotHandler = ^(FSTViewSnapshot *snapshot, NSError *error) { + if (error) { + listener(nil, error); + return; + } + + FIRSnapshotMetadata *metadata = + [FIRSnapshotMetadata snapshotMetadataWithPendingWrites:snapshot.hasPendingWrites + fromCache:snapshot.fromCache]; + + listener([FIRQuerySnapshot snapshotWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata], + nil); + }; + + FSTAsyncQueryListener *asyncListener = + [[FSTAsyncQueryListener alloc] initWithDispatchQueue:self.firestore.client.userDispatchQueue + snapshotHandler:snapshotHandler]; + + FSTQueryListener *internalListener = + [firestore.client listenToQuery:query + options:internalOptions + viewSnapshotHandler:[asyncListener asyncSnapshotHandler]]; + return [[FSTListenerRegistration alloc] initWithClient:self.firestore.client + asyncListener:asyncListener + internalListener:internalListener]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isLessThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorLessThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThan:(id)value { + return + [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan field:field value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThan:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThan + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryWhereField:(NSString *)field isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + field:field + value:value]; +} + +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path isGreaterThanOrEqualTo:(id)value { + return [self queryWithFilterOperator:FSTRelationFilterOperatorGreaterThanOrEqual + path:path.internalValue + value:value]; +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field { + return + [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] descending:NO]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath { + return [self queryOrderedByFieldPath:fieldPath descending:NO]; +} + +- (FIRQuery *)queryOrderedByField:(NSString *)field descending:(BOOL)descending { + return [self queryOrderedByFieldPath:[FIRFieldPath pathWithDotSeparatedString:field] + descending:descending]; +} + +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)fieldPath descending:(BOOL)descending { + [self validateNewOrderByPath:fieldPath.internalValue]; + if (self.query.startAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify a starting point before specifying the order by."); + } + if (self.query.endAt) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You must not specify an ending point before specifying the order by."); + } + FSTSortOrder *sortOrder = + [FSTSortOrder sortOrderWithFieldPath:fieldPath.internalValue ascending:!descending]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingSortOrder:sortOrder] + firestore:self.firestore]; +} + +- (FIRQuery *)queryLimitedTo:(NSInteger)limit { + if (limit <= 0) { + FSTThrowInvalidArgument(@"Invalid Query. Query limit (%ld) is invalid. Limit must be positive.", + (long)limit); + } + return [FIRQuery referenceWithQuery:[self.query queryBySettingLimit:limit] firestore:_firestore]; +} + +- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return [FIRQuery referenceWithQuery:[self.query queryByAddingStartAt:bound] + firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:YES]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)snapshot { + FSTBound *bound = [self boundFromSnapshot:snapshot isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues { + FSTBound *bound = [self boundFromFieldValues:fieldValues isBefore:NO]; + return + [FIRQuery referenceWithQuery:[self.query queryByAddingEndAt:bound] firestore:self.firestore]; +} + +#pragma mark - Private Methods + +/** Private helper for all of the queryWhereField: methods. */ +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + field:(NSString *)field + value:(id)value { + return [self queryWithFilterOperator:filterOperator + path:[FIRFieldPath pathWithDotSeparatedString:field].internalValue + value:value]; +} + +- (FIRQuery *)queryWithFilterOperator:(FSTRelationFilterOperator)filterOperator + path:(FSTFieldPath *)fieldPath + value:(id)value { + FSTFieldValue *fieldValue; + if ([fieldPath isKeyFieldPath]) { + if ([value isKindOfClass:[NSString class]]) { + NSString *documentKey = (NSString *)value; + if ([documentKey containsString:@"/"]) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but '%@' contains a '/' character.", + documentKey); + } else if (documentKey.length == 0) { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide " + "a valid document ID, but it was an empty string."); + } + FSTResourcePath *path = [self.query.path pathByAppendingSegment:documentKey]; + fieldValue = [FSTReferenceValue referenceValue:[FSTDocumentKey keyWithPath:path] + databaseID:self.firestore.databaseID]; + } else if ([value isKindOfClass:[FIRDocumentReference class]]) { + FIRDocumentReference *ref = (FIRDocumentReference *)value; + fieldValue = [FSTReferenceValue referenceValue:ref.key databaseID:self.firestore.databaseID]; + } else { + FSTThrowInvalidArgument( + @"Invalid query. When querying by document ID you must provide a " + "valid string or DocumentReference, but it was of type: %@", + NSStringFromClass([value class])); + } + } else { + fieldValue = [self.firestore.dataConverter parsedQueryValue:value]; + } + + id<FSTFilter> filter; + if ([fieldValue isEqual:[FSTNullValue nullValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on nil / " + "NSNull."); + } + filter = [[FSTNullFilter alloc] initWithField:fieldPath]; + } else if ([fieldValue isEqual:[FSTDoubleValue nanValue]]) { + if (filterOperator != FSTRelationFilterOperatorEqual) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid Query. You can only perform equality comparisons on NaN."); + } + filter = [[FSTNanFilter alloc] initWithField:fieldPath]; + } else { + filter = [FSTRelationFilter filterWithField:fieldPath + filterOperator:filterOperator + value:fieldValue]; + [self validateNewRelationFilter:filter]; + } + return [FIRQuery referenceWithQuery:[self.query queryByAddingFilter:filter] + firestore:self.firestore]; +} + +- (void)validateNewRelationFilter:(FSTRelationFilter *)filter { + if ([filter isInequality]) { + FSTFieldPath *existingField = [self.query inequalityFilterField]; + if (existingField && ![existingField isEqual:filter.field]) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid Query. All where filters with an inequality " + "(lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) must be on the same " + "field. But you have inequality filters on '%@' and '%@'", + existingField, filter.field); + } + + FSTFieldPath *firstOrderByField = [self.query firstSortOrderField]; + if (firstOrderByField) { + [self validateOrderByField:firstOrderByField matchesInequalityField:filter.field]; + } + } +} + +- (void)validateNewOrderByPath:(FSTFieldPath *)fieldPath { + if (![self.query firstSortOrderField]) { + // This is the first order by. It must match any inequality. + FSTFieldPath *inequalityField = [self.query inequalityFilterField]; + if (inequalityField) { + [self validateOrderByField:fieldPath matchesInequalityField:inequalityField]; + } + } +} + +- (void)validateOrderByField:(FSTFieldPath *)orderByField + matchesInequalityField:(FSTFieldPath *)inequalityField { + if (!([orderByField isEqual:inequalityField])) { + FSTThrowInvalidUsage( + @"InvalidQueryException", + @"Invalid query. You have a where filter with an " + "inequality (lessThan, lessThanOrEqual, greaterThan, or greaterThanOrEqual) on field '%@' " + "and so you must also use '%@' as your first queryOrderedBy field, but your first " + "queryOrderedBy is currently on field '%@' instead.", + inequalityField, inequalityField, orderByField); + } +} + +/** + * Create a FSTBound from a query given the document. + * + * Note that the FSTBound will always include the key of the document and the position will be + * unambiguous. + * + * Will throw if the document does not contain all fields of the order by of the query. + */ +- (FSTBound *)boundFromSnapshot:(FIRDocumentSnapshot *)snapshot isBefore:(BOOL)isBefore { + if (![snapshot exists]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a document " + @"that doesn't exist."); + } + FSTDocument *document = snapshot.internalDocument; + NSMutableArray<FSTFieldValue *> *components = [NSMutableArray array]; + + // Because people expect to continue/end a query at the exact document provided, we need to + // use the implicit sort order rather than the explicit sort order, because it's guaranteed to + // contain the document key. That way the position becomes unambiguous and the query + // continues/ends exactly at the provided document. Without the key (by using the explicit sort + // orders), multiple documents could match the position, yielding duplicate results. + for (FSTSortOrder *sortOrder in self.query.sortOrders) { + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + [components addObject:[FSTReferenceValue referenceValue:document.key + databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *value = [document fieldForPath:sortOrder.field]; + if (value != nil) { + [components addObject:value]; + } else { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using a " + "document for which the field '%@' (used as the order by) " + "does not exist.", + sortOrder.field.canonicalString); + } + } + } + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts a list of field values to an FSTBound. */ +- (FSTBound *)boundFromFieldValues:(NSArray<id> *)fieldValues isBefore:(BOOL)isBefore { + // Use explicit sort order because it has to match the query the user made + NSArray<FSTSortOrder *> *explicitSortOrders = self.query.explicitSortOrders; + if (fieldValues.count > explicitSortOrders.count) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. You are trying to start or end a query using more values " + @"than were specified in the order by."); + } + + NSMutableArray<FSTFieldValue *> *components = [NSMutableArray array]; + [fieldValues enumerateObjectsUsingBlock:^(id rawValue, NSUInteger idx, BOOL *stop) { + FSTSortOrder *sortOrder = explicitSortOrders[idx]; + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + if (![rawValue isKindOfClass:[NSString class]]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Expected a string for the document ID."); + } + NSString *documentID = (NSString *)rawValue; + if ([documentID containsString:@"/"]) { + FSTThrowInvalidUsage(@"InvalidQueryException", + @"Invalid query. Document ID '%@' contains a slash.", documentID); + } + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self.query.path pathByAppendingSegment:documentID]]; + [components + addObject:[FSTReferenceValue referenceValue:key databaseID:self.firestore.databaseID]]; + } else { + FSTFieldValue *fieldValue = [self.firestore.dataConverter parsedQueryValue:rawValue]; + [components addObject:fieldValue]; + } + }]; + + return [FSTBound boundWithPosition:components isBefore:isBefore]; +} + +/** Converts the public API options object to the internal options object. */ +- (FSTListenOptions *)internalOptions:(nullable FIRQueryListenOptions *)options { + return [[FSTListenOptions alloc] + initWithIncludeQueryMetadataChanges:options.includeQueryMetadataChanges + includeDocumentMetadataChanges:options.includeDocumentMetadataChanges + waitForSyncWhenOnline:NO]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot+Internal.h b/Firestore/Source/API/FIRQuerySnapshot+Internal.h new file mode 100644 index 0000000..3a1e9db --- /dev/null +++ b/Firestore/Source/API/FIRQuerySnapshot+Internal.h @@ -0,0 +1,37 @@ +/* + * 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 "FIRQuerySnapshot.h" + +@class FIRFirestore; +@class FIRSnapshotMetadata; +@class FSTDocumentSet; +@class FSTQuery; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +/** Internal FIRQuerySnapshot API we don't want exposed in our public header files. */ +@interface FIRQuerySnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuerySnapshot.m b/Firestore/Source/API/FIRQuerySnapshot.m new file mode 100644 index 0000000..4bf4edf --- /dev/null +++ b/Firestore/Source/API/FIRQuerySnapshot.m @@ -0,0 +1,125 @@ +/* + * 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 "FIRQuerySnapshot+Internal.h" + +#import "FIRDocumentChange+Internal.h" +#import "FIRDocumentSnapshot+Internal.h" +#import "FIRQuery+Internal.h" +#import "FIRSnapshotMetadata.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentSet.h" +#import "FSTQuery.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRQuerySnapshot () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) FSTQuery *originalQuery; +@property(nonatomic, strong, readonly) FSTViewSnapshot *snapshot; + +@end + +@implementation FIRQuerySnapshot (Internal) + ++ (instancetype)snapshotWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + return [[FIRQuerySnapshot alloc] initWithFirestore:firestore + originalQuery:query + snapshot:snapshot + metadata:metadata]; +} + +@end + +@implementation FIRQuerySnapshot { + // Cached value of the documents property. + NSArray<FIRDocumentSnapshot *> *_documents; + + // Cached value of the documentChanges property. + NSArray<FIRDocumentChange *> *_documentChanges; +} + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore + originalQuery:(FSTQuery *)query + snapshot:(FSTViewSnapshot *)snapshot + metadata:(FIRSnapshotMetadata *)metadata { + if (self = [super init]) { + _firestore = firestore; + _originalQuery = query; + _snapshot = snapshot; + _metadata = metadata; + } + return self; +} + +@dynamic empty; + +- (FIRQuery *)query { + return [FIRQuery referenceWithQuery:self.originalQuery firestore:self.firestore]; +} + +- (BOOL)isEmpty { + return self.snapshot.documents.isEmpty; +} + +// This property is exposed as an NSInteger instead of an NSUInteger since (as of Xcode 8.1) +// Swift bridges NSUInteger as UInt, and we want to avoid forcing Swift users to cast their ints +// where we can. See cr/146959032 for additional context. +- (NSInteger)count { + return self.snapshot.documents.count; +} + +- (NSArray<FIRDocumentSnapshot *> *)documents { + if (!_documents) { + FSTDocumentSet *documentSet = self.snapshot.documents; + FIRFirestore *firestore = self.firestore; + BOOL fromCache = self.metadata.fromCache; + + NSMutableArray<FIRDocumentSnapshot *> *result = [NSMutableArray array]; + for (FSTDocument *document in documentSet.documentEnumerator) { + [result addObject:[FIRDocumentSnapshot snapshotWithFirestore:firestore + documentKey:document.key + document:document + fromCache:fromCache]]; + } + + _documents = result; + } + return _documents; +} + +- (NSArray<FIRDocumentChange *> *)documentChanges { + if (!_documentChanges) { + _documentChanges = + [FIRDocumentChange documentChangesForSnapshot:self.snapshot firestore:self.firestore]; + } + return _documentChanges; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRQuery_Init.h b/Firestore/Source/API/FIRQuery_Init.h new file mode 100644 index 0000000..d6b0f37 --- /dev/null +++ b/Firestore/Source/API/FIRQuery_Init.h @@ -0,0 +1,32 @@ +/* + * 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 "FIRQuery.h" + +@class FSTQuery; + +NS_ASSUME_NONNULL_BEGIN + +/** + * An Internal class extension for `FIRQuery` that exposes the init method to classes + * that need to derive from it. + */ +@interface FIRQuery (/*Init*/) +- (instancetype)initWithQuery:(FSTQuery *)query + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions+Internal.h b/Firestore/Source/API/FIRSetOptions+Internal.h new file mode 100644 index 0000000..9118096 --- /dev/null +++ b/Firestore/Source/API/FIRSetOptions+Internal.h @@ -0,0 +1,33 @@ +/* + * 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 "FIRSetOptions.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSetOptions () + +- (instancetype)initWithMerge:(BOOL)merge NS_DESIGNATED_INITIALIZER; + +@end + +@interface FIRSetOptions (Internal) + ++ (instancetype)overwrite; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSetOptions.m b/Firestore/Source/API/FIRSetOptions.m new file mode 100644 index 0000000..ea68c63 --- /dev/null +++ b/Firestore/Source/API/FIRSetOptions.m @@ -0,0 +1,65 @@ +/* + * 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 "FIRSetOptions+Internal.h" +#import "FSTMutation.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FIRSetOptions + +- (instancetype)initWithMerge:(BOOL)merge { + if (self = [super init]) { + _merge = merge; + } + return self; +} + ++ (instancetype)merge { + return [[FIRSetOptions alloc] initWithMerge:YES]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FIRSetOptions class]]) { + return NO; + } + + FIRSetOptions *otherOptions = (FIRSetOptions *)other; + + return otherOptions.merge != self.merge; +} + +- (NSUInteger)hash { + return self.merge ? 1231 : 1237; +} +@end + +@implementation FIRSetOptions (Internal) + ++ (instancetype)overwrite { + static FIRSetOptions *overwriteInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + overwriteInstance = [[FIRSetOptions alloc] initWithMerge:NO]; + }); + return overwriteInstance; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotMetadata+Internal.h b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h new file mode 100644 index 0000000..d3265cd --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotMetadata+Internal.h @@ -0,0 +1,29 @@ +/* + * 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 "FIRSnapshotMetadata.h" + +#import <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotMetadata (Internal) + ++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRSnapshotMetadata.m b/Firestore/Source/API/FIRSnapshotMetadata.m new file mode 100644 index 0000000..ff49d8f --- /dev/null +++ b/Firestore/Source/API/FIRSnapshotMetadata.m @@ -0,0 +1,49 @@ +/* + * 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 "FIRSnapshotMetadata.h" + +#import "FIRSnapshotMetadata+Internal.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FIRSnapshotMetadata () + +- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache; + +@end + +@implementation FIRSnapshotMetadata (Internal) + ++ (instancetype)snapshotMetadataWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { + return [[FIRSnapshotMetadata alloc] initWithPendingWrites:pendingWrites fromCache:fromCache]; +} + +@end + +@implementation FIRSnapshotMetadata + +- (instancetype)initWithPendingWrites:(BOOL)pendingWrites fromCache:(BOOL)fromCache { + if (self = [super init]) { + _pendingWrites = pendingWrites; + _fromCache = fromCache; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRTransaction+Internal.h b/Firestore/Source/API/FIRTransaction+Internal.h new file mode 100644 index 0000000..8fd3f65 --- /dev/null +++ b/Firestore/Source/API/FIRTransaction+Internal.h @@ -0,0 +1,27 @@ +/* + * 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 "FIRTransaction.h" + +@class FIRFirestore; +@class FSTTransaction; + +@interface FIRTransaction (Internal) + ++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore; + +@end diff --git a/Firestore/Source/API/FIRTransaction.m b/Firestore/Source/API/FIRTransaction.m new file mode 100644 index 0000000..02006bb --- /dev/null +++ b/Firestore/Source/API/FIRTransaction.m @@ -0,0 +1,147 @@ +/* + * 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 "FIRTransaction+Internal.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRDocumentSnapshot+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRSetOptions+Internal.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTTransaction.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRTransaction + +@interface FIRTransaction () + +- (instancetype)initWithTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTTransaction *internalTransaction; +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@end + +@implementation FIRTransaction (Internal) + ++ (instancetype)transactionWithFSTTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore { + return [[FIRTransaction alloc] initWithTransaction:transaction firestore:firestore]; +} + +@end + +@implementation FIRTransaction + +- (instancetype)initWithTransaction:(FSTTransaction *)transaction + firestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _internalTransaction = transaction; + _firestore = firestore; + } + return self; +} + +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self validateReference:document]; + FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options]; + [self.internalTransaction setData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)updateData:(NSDictionary<id, id> *)fields + forDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.internalTransaction updateData:parsed forDocument:document.key]; + return self; +} + +- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document { + [self validateReference:document]; + [self.internalTransaction deleteDocument:document.key]; + return self; +} + +- (void)getDocument:(FIRDocumentReference *)document + completion:(void (^)(FIRDocumentSnapshot *_Nullable document, + NSError *_Nullable error))completion { + [self validateReference:document]; + [self.internalTransaction + lookupDocumentsForKeys:@[ document.key ] + completion:^(NSArray<FSTMaybeDocument *> *_Nullable documents, + NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + FSTAssert(documents.count == 1, + @"Mismatch in docs returned from document lookup."); + FSTMaybeDocument *internalDoc = documents.firstObject; + if ([internalDoc isKindOfClass:[FSTDeletedDocument class]]) { + completion(nil, nil); + return; + } + FIRDocumentSnapshot *doc = + [FIRDocumentSnapshot snapshotWithFirestore:self.firestore + documentKey:internalDoc.key + document:(FSTDocument *)internalDoc + fromCache:NO]; + completion(doc, nil); + }]; +} + +- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document + error:(NSError *__autoreleasing *)error { + [self validateReference:document]; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + __block FIRDocumentSnapshot *result; + // We have to explicitly assign the innerError into a local to cause it to retain correctly. + __block NSError *outerError = nil; + [self getDocument:document + completion:^(FIRDocumentSnapshot *_Nullable snapshot, NSError *_Nullable innerError) { + result = snapshot; + outerError = innerError; + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); + if (error) { + *error = outerError; + } + return result; +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FIRWriteBatch+Internal.h b/Firestore/Source/API/FIRWriteBatch+Internal.h new file mode 100644 index 0000000..a434e02 --- /dev/null +++ b/Firestore/Source/API/FIRWriteBatch+Internal.h @@ -0,0 +1,25 @@ +/* + * 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 "FIRWriteBatch.h" + +@class FIRFirestore; + +@interface FIRWriteBatch (Internal) + ++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore; + +@end diff --git a/Firestore/Source/API/FIRWriteBatch.m b/Firestore/Source/API/FIRWriteBatch.m new file mode 100644 index 0000000..32b6ce8 --- /dev/null +++ b/Firestore/Source/API/FIRWriteBatch.m @@ -0,0 +1,116 @@ +/* + * 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 "FIRWriteBatch+Internal.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRSetOptions+Internal.h" +#import "FSTFirestoreClient.h" +#import "FSTMutation.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FIRWriteBatch + +@interface FIRWriteBatch () + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FIRFirestore *firestore; +@property(nonatomic, strong, readonly) NSMutableArray<FSTMutation *> *mutations; +@property(nonatomic, assign) BOOL committed; + +@end + +@implementation FIRWriteBatch (Internal) + ++ (instancetype)writeBatchWithFirestore:(FIRFirestore *)firestore { + return [[FIRWriteBatch alloc] initWithFirestore:firestore]; +} + +@end + +@implementation FIRWriteBatch + +- (instancetype)initWithFirestore:(FIRFirestore *)firestore { + self = [super init]; + if (self) { + _firestore = firestore; + _mutations = [NSMutableArray array]; + } + return self; +} + +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document { + return [self setData:data forDocument:document options:[FIRSetOptions overwrite]]; +} + +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedSetData *parsed = [self.firestore.dataConverter parsedSetData:data options:options]; + [self.mutations addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (FIRWriteBatch *)updateData:(NSDictionary<id, id> *)fields + forDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + FSTParsedUpdateData *parsed = [self.firestore.dataConverter parsedUpdateData:fields]; + [self.mutations + addObjectsFromArray:[parsed mutationsWithKey:document.key + precondition:[FSTPrecondition preconditionWithExists:YES]]]; + return self; +} + +- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document { + [self verifyNotCommitted]; + [self validateReference:document]; + [self.mutations addObject:[[FSTDeleteMutation alloc] initWithKey:document.key + precondition:[FSTPrecondition none]]]; + return self; +} + +- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion { + [self verifyNotCommitted]; + self.committed = TRUE; + [self.firestore.client writeMutations:self.mutations completion:completion]; +} + +- (void)verifyNotCommitted { + if (self.committed) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"A write batch can no longer be used after commit has been called."); + } +} + +- (void)validateReference:(FIRDocumentReference *)reference { + if (reference.firestore != self.firestore) { + FSTThrowInvalidArgument(@"Provided document reference is from a different Firestore instance."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.h b/Firestore/Source/API/FSTUserDataConverter.h new file mode 100644 index 0000000..69d1fa9 --- /dev/null +++ b/Firestore/Source/API/FSTUserDataConverter.h @@ -0,0 +1,124 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FIRSetOptions; +@class FSTDatabaseID; +@class FSTDocumentKey; +@class FSTObjectValue; +@class FSTFieldMask; +@class FSTFieldValue; +@class FSTFieldTransform; +@class FSTMutation; +@class FSTPrecondition; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** The result of parsing document data (e.g. for a setData call). */ +@interface FSTParsedSetData : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(nullable FSTFieldMask *)fieldMask + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTObjectValue *data; +@property(nonatomic, strong, readonly, nullable) FSTFieldMask *fieldMask; +@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms; + +/** + * Converts the parsed document data into 1 or 2 mutations (depending on whether there are any + * field transforms) using the specified document key and precondition. + */ +- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition; + +@end + +/** The result of parsing "update" data (i.e. for an updateData call). */ +@interface FSTParsedUpdateData : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(FSTFieldMask *)fieldMask + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms + NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTObjectValue *data; +@property(nonatomic, strong, readonly) FSTFieldMask *fieldMask; +@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms; + +/** + * Converts the parsed update data into 1 or 2 mutations (depending on whether there are any + * field transforms) using the specified document key and precondition. + */ +- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition; + +@end + +/** + * An internal representation of FIRDocumentReference, representing a key in a specific database. + * This is necessary because keys assume a database from context (usually the current one). + * FSTDocumentKeyReference binds a key to a specific databaseID. + * + * TODO(b/64160088): Make FSTDocumentKey aware of the specific databaseID it is tied to. + */ +@interface FSTDocumentKeyReference : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithKey:(FSTDocumentKey *)key + databaseID:(FSTDatabaseID *)databaseID NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; + +@end + +/** + * An interface that allows arbitrary pre-converting of user data. + * + * Returns the converted value (can return back the input to act as a no-op). + */ +typedef id _Nullable (^FSTPreConverterBlock)(id _Nullable); + +/** + * Helper for parsing raw user input (provided via the API) into internal model classes. + */ +@interface FSTUserDataConverter : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + preConverter:(FSTPreConverterBlock)preConverter NS_DESIGNATED_INITIALIZER; + +/** Parse document data (e.g. from a setData call). */ +- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options; + +/** Parse "update" data (i.e. from an updateData call). */ +- (FSTParsedUpdateData *)parsedUpdateData:(id)input; + +/** Parse a "query value" (e.g. value in a where filter or a value in a cursor bound). */ +- (FSTFieldValue *)parsedQueryValue:(id)input; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/API/FSTUserDataConverter.m b/Firestore/Source/API/FSTUserDataConverter.m new file mode 100644 index 0000000..7a6c950 --- /dev/null +++ b/Firestore/Source/API/FSTUserDataConverter.m @@ -0,0 +1,568 @@ +/* + * 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 "FSTUserDataConverter.h" + +#import "FIRDocumentReference+Internal.h" +#import "FIRFieldPath+Internal.h" +#import "FIRFieldValue+Internal.h" +#import "FIRFirestore+Internal.h" +#import "FIRGeoPoint.h" +#import "FIRSetOptions+Internal.h" +#import "FSTAssert.h" +#import "FSTDatabaseID.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTMutation.h" +#import "FSTPath.h" +#import "FSTTimestamp.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const RESERVED_FIELD_DESIGNATOR = @"__"; + +#pragma mark - FSTParsedSetData + +@implementation FSTParsedSetData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(nullable FSTFieldMask *)fieldMask + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray<FSTMutation *> *mutations = [NSMutableArray array]; + if (self.fieldMask) { + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + } else { + [mutations addObject:[[FSTSetMutation alloc] initWithKey:key + value:self.data + precondition:precondition]]; + } + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +#pragma mark - FSTParsedUpdateData + +@implementation FSTParsedUpdateData +- (instancetype)initWithData:(FSTObjectValue *)data + fieldMask:(FSTFieldMask *)fieldMask + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms { + self = [super init]; + if (self) { + _data = data; + _fieldMask = fieldMask; + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (NSArray<FSTMutation *> *)mutationsWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition { + NSMutableArray<FSTMutation *> *mutations = [NSMutableArray array]; + [mutations addObject:[[FSTPatchMutation alloc] initWithKey:key + fieldMask:self.fieldMask + value:self.data + precondition:precondition]]; + if (self.fieldTransforms.count > 0) { + [mutations addObject:[[FSTTransformMutation alloc] initWithKey:key + fieldTransforms:self.fieldTransforms]]; + } + return mutations; +} + +@end + +/** + * Represents what type of API method provided the data being parsed; useful for determining which + * error conditions apply during parsing and providing better error messages. + */ +typedef NS_ENUM(NSInteger, FSTUserDataSource) { + FSTUserDataSourceSet, + FSTUserDataSourceUpdate, + FSTUserDataSourceQueryValue, // from a where clause or cursor bound. +}; + +#pragma mark - FSTParseContext + +/** + * A "context" object passed around while parsing user data. + */ +@interface FSTParseContext : NSObject +/** The current path being parsed. */ +// TODO(b/34871131): path should never be nil, but we don't support array paths right now. +@property(strong, nonatomic, readonly, nullable) FSTFieldPath *path; + +/** + * What type of API method provided the data being parsed; useful for determining which error + * conditions apply during parsing and providing better error messages. + */ +@property(nonatomic, assign) FSTUserDataSource dataSource; +@property(nonatomic, strong, readonly) NSMutableArray<FSTFieldTransform *> *fieldTransforms; +@property(nonatomic, strong, readonly) NSMutableArray<FSTFieldPath *> *fieldMask; + +- (instancetype)init NS_UNAVAILABLE; +/** + * Initializes a FSTParseContext with the given source and path. + * + * @param dataSource Indicates what kind of API method this data came from. + * @param path A path within the object being parsed. This could be an empty path (in which case + * the context represents the root of the data being parsed), or a nonempty path (indicating the + * context represents a nested location within the data). + * + * TODO(b/34871131): We don't support array paths right now, so path can be nil to indicate + * the context represents any location within an array (in which case certain features will not work + * and errors will be somewhat compromised). + */ +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + fieldTransforms:(NSMutableArray<FSTFieldTransform *> *)fieldTransforms + fieldMask:(NSMutableArray<FSTFieldPath *> *)fieldMask + NS_DESIGNATED_INITIALIZER; + +// Helpers to get a FSTParseContext for a child field. +- (instancetype)contextForField:(NSString *)fieldName; +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath; +- (instancetype)contextForArrayIndex:(NSUInteger)index; +@end + +@implementation FSTParseContext + ++ (instancetype)contextWithSource:(FSTUserDataSource)dataSource path:(nullable FSTFieldPath *)path { + FSTParseContext *context = [[FSTParseContext alloc] initWithSource:dataSource + path:path + fieldTransforms:[NSMutableArray array] + fieldMask:[NSMutableArray array]]; + [context validatePath]; + return context; +} + +- (instancetype)initWithSource:(FSTUserDataSource)dataSource + path:(nullable FSTFieldPath *)path + fieldTransforms:(NSMutableArray<FSTFieldTransform *> *)fieldTransforms + fieldMask:(NSMutableArray<FSTFieldPath *> *)fieldMask { + if (self = [super init]) { + _dataSource = dataSource; + _path = path; + _fieldTransforms = fieldTransforms; + _fieldMask = fieldMask; + } + return self; +} + +- (instancetype)contextForField:(NSString *)fieldName { + FSTParseContext *context = + [[FSTParseContext alloc] initWithSource:self.dataSource + path:[self.path pathByAppendingSegment:fieldName] + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; + [context validatePathSegment:fieldName]; + return context; +} + +- (instancetype)contextForFieldPath:(FSTFieldPath *)fieldPath { + FSTParseContext *context = + [[FSTParseContext alloc] initWithSource:self.dataSource + path:[self.path pathByAppendingPath:fieldPath] + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; + [context validatePath]; + return context; +} + +- (instancetype)contextForArrayIndex:(NSUInteger)index { + // TODO(b/34871131): We don't support array paths right now; so make path nil. + return [[FSTParseContext alloc] initWithSource:self.dataSource + path:nil + fieldTransforms:self.fieldTransforms + fieldMask:self.fieldMask]; +} + +/** + * Returns a string that can be appended to error messages indicating what field caused the error. + */ +- (NSString *)fieldDescription { + // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. + if (!self.path || self.path.empty) { + return @""; + } else { + return [NSString stringWithFormat:@" (found in field %@)", self.path]; + } +} + +- (BOOL)isWrite { + return _dataSource == FSTUserDataSourceSet || _dataSource == FSTUserDataSourceUpdate; +} + +- (void)validatePath { + // TODO(b/34871131): Remove nil check once we have proper paths for fields within arrays. + if (self.path == nil) { + return; + } + for (int i = 0; i < self.path.length; i++) { + [self validatePathSegment:[self.path segmentAtIndex:i]]; + } +} + +- (void)validatePathSegment:(NSString *)segment { + if ([self isWrite] && [segment hasPrefix:RESERVED_FIELD_DESIGNATOR] && + [segment hasSuffix:RESERVED_FIELD_DESIGNATOR]) { + FSTThrowInvalidArgument(@"Document fields cannot begin and end with %@%@", + RESERVED_FIELD_DESIGNATOR, [self fieldDescription]); + } +} + +@end + +#pragma mark - FSTDocumentKeyReference + +@implementation FSTDocumentKeyReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key databaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _key = key; + _databaseID = databaseID; + } + return self; +} + +@end + +#pragma mark - FSTUserDataConverter + +@interface FSTUserDataConverter () +@property(strong, nonatomic, readonly) FSTDatabaseID *databaseID; +@property(strong, nonatomic, readonly) FSTPreConverterBlock preConverter; +@end + +@implementation FSTUserDataConverter + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + preConverter:(FSTPreConverterBlock)preConverter { + self = [super init]; + if (self) { + _databaseID = databaseID; + _preConverter = preConverter; + } + return self; +} + +- (FSTParsedSetData *)parsedSetData:(id)input options:(FIRSetOptions *)options { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceSet path:[FSTFieldPath emptyPath]]; + + __block FSTObjectValue *updateData = [FSTObjectValue objectValue]; + + [input enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + // Treat key as a complete field name (don't split on dots, etc.) + FSTFieldPath *path = [[FIRFieldPath alloc] initWithFields:@[ key ]].internalValue; + + value = self.preConverter(value); + + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForFieldPath:path]]; + if (parsedValue) { + updateData = [updateData objectBySettingValue:parsedValue forPath:path]; + } + }]; + + return [[FSTParsedSetData alloc] + initWithData:updateData + fieldMask:options.merge ? [[FSTFieldMask alloc] initWithFields:context.fieldMask] : nil + fieldTransforms:context.fieldTransforms]; +} + +- (FSTParsedUpdateData *)parsedUpdateData:(id)input { + // NOTE: The public API is typed as NSDictionary but we type 'input' as 'id' since we can't trust + // Obj-C to verify the type for us. + if (![input isKindOfClass:[NSDictionary class]]) { + FSTThrowInvalidArgument(@"Data to be written must be an NSDictionary."); + } + + NSDictionary *dict = input; + + NSMutableArray<FSTFieldPath *> *fieldMaskPaths = [NSMutableArray array]; + __block FSTObjectValue *updateData = [FSTObjectValue objectValue]; + + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceUpdate path:[FSTFieldPath emptyPath]]; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + FSTFieldPath *path; + + if ([key isKindOfClass:[NSString class]]) { + path = [FIRFieldPath pathWithDotSeparatedString:key].internalValue; + } else if ([key isKindOfClass:[FIRFieldPath class]]) { + path = ((FIRFieldPath *)key).internalValue; + } else { + FSTThrowInvalidArgument( + @"Dictionary keys in updateData: must be NSStrings or FIRFieldPaths."); + } + + value = self.preConverter(value); + if ([value isKindOfClass:[FSTDeleteFieldValue class]]) { + // Add it to the field mask, but don't add anything to updateData. + [fieldMaskPaths addObject:path]; + } else { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForFieldPath:path]]; + if (parsedValue) { + [fieldMaskPaths addObject:path]; + updateData = [updateData objectBySettingValue:parsedValue forPath:path]; + } + } + }]; + + FSTFieldMask *mask = [[FSTFieldMask alloc] initWithFields:fieldMaskPaths]; + return [[FSTParsedUpdateData alloc] initWithData:updateData + fieldMask:mask + fieldTransforms:context.fieldTransforms]; +} + +- (FSTFieldValue *)parsedQueryValue:(id)input { + FSTParseContext *context = + [FSTParseContext contextWithSource:FSTUserDataSourceQueryValue path:[FSTFieldPath emptyPath]]; + FSTFieldValue *_Nullable parsed = [self parseData:input context:context]; + FSTAssert(parsed, @"Parsed data should not be nil."); + FSTAssert(context.fieldTransforms.count == 0, @"Field transforms should have been disallowed."); + return parsed; +} + +/** + * Internal helper for parsing user data. + * + * @param input Data to be parsed. + * @param context A context object representing the current path being parsed, the source of the + * data being parsed, etc. + * + * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be + * included in the resulting parsed data. + */ +- (nullable FSTFieldValue *)parseData:(id)input context:(FSTParseContext *)context { + input = self.preConverter(input); + if ([input isKindOfClass:[NSArray class]]) { + // TODO(b/34871131): We may need a different way to detect nested arrays once we support array + // paths (at which point we should include the path containing the array in the error message). + if (!context.path) { + FSTThrowInvalidArgument(@"Nested arrays are not supported"); + } + NSArray *array = input; + NSMutableArray<FSTFieldValue *> *result = [NSMutableArray arrayWithCapacity:array.count]; + [array enumerateObjectsUsingBlock:^(id entry, NSUInteger idx, BOOL *stop) { + FSTFieldValue *_Nullable parsedEntry = + [self parseData:entry context:[context contextForArrayIndex:idx]]; + if (!parsedEntry) { + // Just include nulls in the array for fields being replaced with a sentinel. + parsedEntry = [FSTNullValue nullValue]; + } + [result addObject:parsedEntry]; + }]; + // We don't support field mask paths more granular than the top-level array. + [context.fieldMask addObject:context.path]; + return [[FSTArrayValue alloc] initWithValueNoCopy:result]; + + } else if ([input isKindOfClass:[NSDictionary class]]) { + NSDictionary *dict = input; + NSMutableDictionary<NSString *, FSTFieldValue *> *result = + [NSMutableDictionary dictionaryWithCapacity:dict.count]; + [dict enumerateKeysAndObjectsUsingBlock:^(NSString *key, id value, BOOL *stop) { + FSTFieldValue *_Nullable parsedValue = + [self parseData:value context:[context contextForField:key]]; + if (parsedValue) { + result[key] = parsedValue; + } + }]; + return [[FSTObjectValue alloc] initWithDictionary:result]; + + } else { + // If context.path is null, we are inside an array and we should have already added the root of + // the array to the field mask. + if (context.path) { + [context.fieldMask addObject:context.path]; + } + return [self parseScalarValue:input context:context]; + } +} + +/** + * Helper to parse a scalar value (i.e. not an NSDictionary or NSArray). + * + * Note that it handles all NSNumber values that are encodable as int64_t or doubles + * (depending on the underlying type of the NSNumber). Unsigned integer values are handled though + * any value outside what is representable by int64_t (a signed 64-bit value) will throw an + * exception. + * + * @return The parsed value, or nil if the value was a FieldValue sentinel that should not be + * included in the resulting parsed data. + */ +- (nullable FSTFieldValue *)parseScalarValue:(nullable id)input context:(FSTParseContext *)context { + if (!input || [input isMemberOfClass:[NSNull class]]) { + return [FSTNullValue nullValue]; + + } else if ([input isKindOfClass:[NSNumber class]]) { + // Recover the underlying type of the number, using the method described here: + // http://stackoverflow.com/questions/2518761/get-type-of-nsnumber + const char *cType = [input objCType]; + + // Type Encoding values taken from + // https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/ + // Articles/ocrtTypeEncodings.html + switch (cType[0]) { + case 'q': + return [FSTIntegerValue integerValue:[input longLongValue]]; + + case 'i': // Falls through. + case 's': // Falls through. + case 'l': // Falls through. + case 'I': // Falls through. + case 'S': + // Coerce integer values that aren't long long. Allow unsigned integer types that are + // guaranteed small enough to skip a length check. + return [FSTIntegerValue integerValue:[input longLongValue]]; + + case 'L': // Falls through. + case 'Q': + // Unsigned integers that could be too large. Note that the 'L' (long) case is handled here + // because when compiled for LP64, unsigned long is 64 bits and could overflow int64_t. + { + unsigned long long extended = [input unsignedLongLongValue]; + + if (extended > LLONG_MAX) { + FSTThrowInvalidArgument(@"NSNumber (%llu) is too large%@", + [input unsignedLongLongValue], [context fieldDescription]); + + } else { + return [FSTIntegerValue integerValue:(int64_t)extended]; + } + } + + case 'f': + return [FSTDoubleValue doubleValue:[input doubleValue]]; + + case 'd': + // Double values are already the right type, so just reuse the existing boxed double. + // + // Note that NSNumber already performs NaN normalization to a single shared instance + // so there's no need to treat NaN specially here. + return [FSTDoubleValue doubleValue:[input doubleValue]]; + + case 'B': // Falls through. + case 'c': // Falls through. + case 'C': + // Boolean values are weird. + // + // On arm64, objCType of a BOOL-valued NSNumber will be "c", even though @encode(BOOL) + // returns "B". "c" is the same as @encode(signed char). Unfortunately this means that + // legitimate usage of signed chars is impossible, but this should be rare. + // + // Additionally, for consistency, map unsigned chars to bools in the same way. + return [FSTBooleanValue booleanValue:[input boolValue]]; + + default: + // All documented codes should be handled above, so this shouldn't happen. + FSTCFail(@"Unknown NSNumber objCType %s on %@", cType, input); + } + + } else if ([input isKindOfClass:[NSString class]]) { + return [FSTStringValue stringValue:input]; + + } else if ([input isKindOfClass:[NSDate class]]) { + return [FSTTimestampValue timestampValue:[FSTTimestamp timestampWithDate:input]]; + + } else if ([input isKindOfClass:[FIRGeoPoint class]]) { + return [FSTGeoPointValue geoPointValue:input]; + + } else if ([input isKindOfClass:[NSData class]]) { + return [FSTBlobValue blobValue:input]; + + } else if ([input isKindOfClass:[FSTDocumentKeyReference class]]) { + FSTDocumentKeyReference *reference = input; + if (![reference.databaseID isEqual:self.databaseID]) { + FSTDatabaseID *other = reference.databaseID; + FSTThrowInvalidArgument( + @"Document Reference is for database %@/%@ but should be for database %@/%@%@", + other.projectID, other.databaseID, self.databaseID.projectID, self.databaseID.databaseID, + [context fieldDescription]); + } + return [FSTReferenceValue referenceValue:reference.key databaseID:self.databaseID]; + + } else if ([input isKindOfClass:[FIRFieldValue class]]) { + if ([input isKindOfClass:[FSTDeleteFieldValue class]]) { + // We shouldn't encounter delete sentinels here. Provide a good error. + if (context.dataSource != FSTUserDataSourceUpdate) { + FSTThrowInvalidArgument(@"FieldValue.delete() can only be used with updateData()."); + } else { + FSTAssert(context.path.length > 0, + @"FieldValue.delete() at the top level should have already been handled."); + FSTThrowInvalidArgument( + @"FieldValue.delete() can only appear at the top level of your " + "update data%@", + [context fieldDescription]); + } + } else if ([input isKindOfClass:[FSTServerTimestampFieldValue class]]) { + if (context.dataSource != FSTUserDataSourceSet && + context.dataSource != FSTUserDataSourceUpdate) { + FSTThrowInvalidArgument( + @"FieldValue.serverTimestamp() can only be used with setData() and updateData()."); + } + if (!context.path) { + FSTThrowInvalidArgument( + @"FieldValue.serverTimestamp() is not currently supported inside arrays%@", + [context fieldDescription]); + } + [context.fieldTransforms + addObject:[[FSTFieldTransform alloc] + initWithPath:context.path + transform:[FSTServerTimestampTransform serverTimestampTransform]]]; + + // Return nil so this value is omitted from the parsed result. + return nil; + } else { + FSTFail(@"Unknown FIRFieldValue type: %@", NSStringFromClass([input class])); + } + + } else { + FSTThrowInvalidArgument(@"Unsupported type: %@%@", NSStringFromClass([input class]), + [context fieldDescription]); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTCredentialsProvider.h b/Firestore/Source/Auth/FSTCredentialsProvider.h new file mode 100644 index 0000000..eb591ab --- /dev/null +++ b/Firestore/Source/Auth/FSTCredentialsProvider.h @@ -0,0 +1,113 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@class FIRApp; +@class FSTDispatchQueue; +@class FSTUser; + +#pragma mark - FSTGetTokenResult + +/** + * The current FSTUser and the authentication token provided by the underlying authentication + * mechanism. This is the result of calling -[FSTCredentialsProvider getTokenForcingRefresh]. + * + * ## Portability notes: no TokenType on iOS + * + * The TypeScript client supports 1st party Oauth tokens (for the Firebase Console to auth as the + * developer) and OAuth2 tokens for the node.js sdk to auth with a service account. We don't have + * plans to support either case on mobile so there's no TokenType here. + */ +// TODO(mcg): Rename FSTToken, change parameter order to line up with the other platforms. +@interface FSTGetTokenResult : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithUser:(FSTUser *)user + token:(NSString *_Nullable)token NS_DESIGNATED_INITIALIZER; + +/** The user with which the token is associated (used for persisting user state on disk, etc.). */ +@property(nonatomic, nonnull, readonly) FSTUser *user; + +/** The actual raw token. */ +@property(nonatomic, copy, nullable, readonly) NSString *token; + +@end + +#pragma mark - Typedefs + +/** + * `FSTVoidTokenErrorBlock` is a block that gets a token or an error. + * + * @param token An auth token as a string. + * @param error The error if one occurred, or else `nil`. + */ +typedef void (^FSTVoidGetTokenResultBlock)(FSTGetTokenResult *_Nullable token, + NSError *_Nullable error); + +/** Listener block notified with an FSTUser. */ +typedef void (^FSTVoidUserBlock)(FSTUser *user); + +#pragma mark - FSTCredentialsProvider + +/** Provides methods for getting the uid and token for the current user and listen for changes. */ +@protocol FSTCredentialsProvider<NSObject> + +/** Requests token for the current user, optionally forcing a refreshed token to be fetched. */ +- (void)getTokenForcingRefresh:(BOOL)forceRefresh completion:(FSTVoidGetTokenResultBlock)completion; + +/** + * A listener to be notified of user changes (sign-in / sign-out). It is immediately called once + * with the initial user. + * + * Note that this block will be called back on an arbitrary thread that is not the normal Firestore + * worker thread. + */ +@property(nonatomic, copy, nullable, readwrite) FSTVoidUserBlock userChangeListener; + +@end + +#pragma mark - FSTFirebaseCredentialsProvider + +/** + * `FSTFirebaseCredentialsProvider` uses Firebase Auth via `FIRApp` to get an auth token. + * + * NOTE: To simplify the implementation, it requires that you set `userChangeListener` with a + * non-`nil` value no more than once and don't call `getTokenForcingRefresh:` after setting + * it to `nil`. + * + * This class must be implemented in a thread-safe manner since it is accessed from the thread + * backing our internal worker queue and the callbacks from FIRAuth will be executed on an + * arbitrary different thread. + */ +@interface FSTFirebaseCredentialsProvider : NSObject<FSTCredentialsProvider> + +/** + * Initializes a new FSTFirebaseCredentialsProvider. + * + * @param app The Firebase app from which to get credentials. + * + * @return A new instance of FSTFirebaseCredentialsProvider. + */ +- (instancetype)initWithApp:(FIRApp *)app NS_DESIGNATED_INITIALIZER; + +- (id)init NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTCredentialsProvider.m b/Firestore/Source/Auth/FSTCredentialsProvider.m new file mode 100644 index 0000000..cec7c2b --- /dev/null +++ b/Firestore/Source/Auth/FSTCredentialsProvider.m @@ -0,0 +1,161 @@ +/* + * 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 "FSTCredentialsProvider.h" + +#import <FirebaseCommunity/FIRApp.h> +#import <FirebaseCommunity/FIRAuth.h> +#import <FirebaseCommunity/FIRUser.h> +#import <GRPCClient/GRPCCall.h> + +// This is not an exported header so it's not visible via FirebaseCommunity +#import "FIRAppInternal.h" + +#import "FIRFirestoreErrors.h" +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTDispatchQueue.h" +#import "FSTUser.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTGetTokenResult + +@implementation FSTGetTokenResult +- (instancetype)initWithUser:(FSTUser *)user token:(NSString *_Nullable)token { + if (self = [super init]) { + _user = user; + _token = token; + } + return self; +} +@end + +#pragma mark - FSTFirebaseCredentialsProvider +// TODO(mikelehen): Currently, we have a strong dependency on FIRAuth but we should ideally use +// only internal APIs on FIRApp instead. However, currently the FIRApp internal APIs don't expose +// the uid of the current user and don't expose an auth state change listener. So we use FIRAuth. +@interface FSTFirebaseCredentialsProvider () + +@property(nonatomic, strong, readonly) FIRApp *app; +@property(nonatomic, strong, readonly) FIRAuth *auth; + +/** Handle used to stop receiving auth changes once userChangeListener is removed. */ +@property(nonatomic, strong, nullable, readwrite) + FIRAuthStateDidChangeListenerHandle authListenerHandle; + +/** The current user as reported to us via our AuthStateDidChangeListener. */ +@property(nonatomic, strong, nonnull, readwrite) FSTUser *currentUser; + +/** + * Counter used to detect if the user changed while a -getTokenForcingRefresh: request was + * outstanding. + */ +@property(nonatomic, assign, readwrite) int userCounter; + +@end + +@implementation FSTFirebaseCredentialsProvider { + FSTVoidUserBlock _userChangeListener; +} + +- (instancetype)initWithApp:(FIRApp *)app { + self = [super init]; + if (self) { + _app = app; + _auth = [FIRAuth authWithApp:app]; + _currentUser = [[FSTUser alloc] initWithUID:self.auth.currentUser.uid]; + _userCounter = 0; + + // Register for user changes so that we can internally track the current user. + FSTWeakify(self); + _authListenerHandle = [self.auth addAuthStateDidChangeListener:^(FIRAuth *auth, FIRUser *user) { + FSTStrongify(self); + if (self) { + @synchronized(self) { + FSTUser *newUser = [[FSTUser alloc] initWithUID:user.uid]; + if (![newUser isEqual:self.currentUser]) { + self.currentUser = newUser; + self.userCounter++; + FSTVoidUserBlock listenerBlock = self.userChangeListener; + if (listenerBlock) { + listenerBlock(self.currentUser); + } + } + } + } + }]; + } + return self; +} + +- (void)getTokenForcingRefresh:(BOOL)forceRefresh + completion:(FSTVoidGetTokenResultBlock)completion { + FSTAssert(self.authListenerHandle, @"getToken cannot be called after listener removed."); + + // Take note of the current value of the userCounter so that this method can fail (with a + // FIRFirestoreErrorCodeAborted error) if there is a user change while the request is outstanding. + int initialUserCounter = self.userCounter; + + void (^getTokenCallback)(NSString *, NSError *) = ^(NSString *_Nullable token, + NSError *_Nullable error) { + @synchronized(self) { + if (initialUserCounter != self.userCounter) { + // Cancel the request since the user changed while the request was outstanding so the + // response is likely for a previous user (which user, we can't be sure). + NSDictionary *errorInfo = @{ @"details" : @"getToken aborted due to user change." }; + NSError *cancelError = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeAborted + userInfo:errorInfo]; + completion(nil, cancelError); + } else { + FSTGetTokenResult *result = + [[FSTGetTokenResult alloc] initWithUser:self.currentUser token:token]; + completion(result, error); + } + }; + }; + + [self.app getTokenForcingRefresh:forceRefresh withCallback:getTokenCallback]; +} + +- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block { + @synchronized(self) { + if (block) { + FSTAssert(!_userChangeListener, @"UserChangeListener set twice!"); + + // Fire initial event. + block(self.currentUser); + } else { + FSTAssert(self.authListenerHandle, @"UserChangeListener removed twice!"); + FSTAssert(_userChangeListener, @"UserChangeListener removed without being set!"); + [self.auth removeAuthStateDidChangeListener:self.authListenerHandle]; + + self.authListenerHandle = nil; + } + _userChangeListener = block; + } +} + +- (nullable FSTVoidUserBlock)userChangeListener { + @synchronized(self) { + return _userChangeListener; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.h b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.h new file mode 100644 index 0000000..c0074c2 --- /dev/null +++ b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.h @@ -0,0 +1,28 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTCredentialsProvider.h" + +NS_ASSUME_NONNULL_BEGIN + +/** `FSTEmptyCredentialsProvider` always yields an empty token. */ +@interface FSTEmptyCredentialsProvider : NSObject<FSTCredentialsProvider> + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m new file mode 100644 index 0000000..266e09b --- /dev/null +++ b/Firestore/Source/Auth/FSTEmptyCredentialsProvider.m @@ -0,0 +1,47 @@ +/* + * 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 "FSTEmptyCredentialsProvider.h" + +#import "FSTUser.h" +#import "FSTAssert.h" +#import "FSTDispatchQueue.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTEmptyCredentialsProvider + +- (void)getTokenForcingRefresh:(BOOL)forceRefresh + completion:(FSTVoidGetTokenResultBlock)completion { + completion(nil, nil); +} + +- (void)setUserChangeListener:(nullable FSTVoidUserBlock)block { + // Since the user never changes, we just need to fire the initial event and don't need to hang + // onto the block. + if (block) { + block([FSTUser unauthenticatedUser]); + } +} + +- (nullable FSTVoidUserBlock)userChangeListener { + // TODO(mikelehen): Implementation omitted for convenience since it's not actually required. + FSTFail(@"Not implemented."); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTUser.h b/Firestore/Source/Auth/FSTUser.h new file mode 100644 index 0000000..83b1962 --- /dev/null +++ b/Firestore/Source/Auth/FSTUser.h @@ -0,0 +1,43 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** + * Simple wrapper around a nullable UID. Mostly exists to make code more readable and for use as + * a key in dictionaries (since keys cannot be nil). + */ +@interface FSTUser : NSObject<NSCopying> + +/** Returns an FSTUser with a nil UID. */ ++ (instancetype)unauthenticatedUser; + +// Porting note: no GOOGLE_CREDENTIALS or FIRST_PARTY equivalent on iOS, see FSTGetTokenResult for +// more details. + +/** Initializes an FSTUser with the given UID. */ +- (instancetype)initWithUID:(NSString *_Nullable)UID NS_DESIGNATED_INITIALIZER; +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, copy, nullable, readonly) NSString *UID; + +@property(nonatomic, assign, readonly, getter=isUnauthenticated) BOOL unauthenticated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Auth/FSTUser.m b/Firestore/Source/Auth/FSTUser.m new file mode 100644 index 0000000..a7492b2 --- /dev/null +++ b/Firestore/Source/Auth/FSTUser.m @@ -0,0 +1,68 @@ +/* + * 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 "FSTUser.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTUser + +@implementation FSTUser + +@dynamic unauthenticated; + ++ (instancetype)unauthenticatedUser { + return [[FSTUser alloc] initWithUID:nil]; +} + +- (instancetype)initWithUID:(NSString *_Nullable)UID { + if (self = [super init]) { + _UID = UID; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } else if (![object isKindOfClass:[FSTUser class]]) { + return NO; + } else { + FSTUser *other = object; + return (self.isUnauthenticated && other.isUnauthenticated) || + [self.UID isEqualToString:other.UID]; + } +} + +- (NSUInteger)hash { + return [self.UID hash]; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + return self; // since FSTUser objects are immutable +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTUser uid=%@>", self.UID]; +} + +- (BOOL)isUnauthenticated { + return self.UID == nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTDatabaseInfo.h b/Firestore/Source/Core/FSTDatabaseInfo.h new file mode 100644 index 0000000..fae884f --- /dev/null +++ b/Firestore/Source/Core/FSTDatabaseInfo.h @@ -0,0 +1,55 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTDatabaseID; + +NS_ASSUME_NONNULL_BEGIN + +/** FSTDatabaseInfo contains data about the database. */ +@interface FSTDatabaseInfo : NSObject + +/** + * Creates and returns a new FSTDatabaseInfo. + * + * @param databaseID The project/database to use. + * @param persistenceKey A unique identifier for this Firestore's local storage. Usually derived + * from -[FIRApp appName]. + * @param host The hostname of the datastore backend. + * @param sslEnabled Whether to use SSL when connecting. + * @return A new instance of FSTDatabaseInfo. + */ ++ (instancetype)databaseInfoWithDatabaseID:(FSTDatabaseID *)databaseID + persistenceKey:(NSString *)persistenceKey + host:(NSString *)host + sslEnabled:(BOOL)sslEnabled; + +/** The database info. */ +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; + +/** The application name, taken from FIRApp. */ +@property(nonatomic, copy, readonly) NSString *persistenceKey; + +/** The hostname of the backend. */ +@property(nonatomic, copy, readonly) NSString *host; + +/** Whether to use SSL when connecting. */ +@property(nonatomic, readonly, getter=isSSLEnabled) BOOL sslEnabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTDatabaseInfo.m b/Firestore/Source/Core/FSTDatabaseInfo.m new file mode 100644 index 0000000..d2cd0ed --- /dev/null +++ b/Firestore/Source/Core/FSTDatabaseInfo.m @@ -0,0 +1,70 @@ +/* + * 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 "FSTDatabaseInfo.h" + +#import "FSTDatabaseID.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTDatabaseInfo + +@implementation FSTDatabaseInfo + +#pragma mark - Constructors + ++ (instancetype)databaseInfoWithDatabaseID:(FSTDatabaseID *)databaseID + persistenceKey:(NSString *)persistenceKey + host:(NSString *)host + sslEnabled:(BOOL)sslEnabled { + return [[FSTDatabaseInfo alloc] initWithDatabaseID:databaseID + persistenceKey:persistenceKey + host:host + sslEnabled:sslEnabled]; +} + +/** + * Designated initializer. + * + * @param databaseID The database in the datastore. + * @param persistenceKey A unique identifier for this Firestore's local storage. Usually derived + * from -[FIRApp appName]. + * @param host The Firestore server hostname. + * @param sslEnabled Whether to use SSL when connecting. + */ +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID + persistenceKey:(NSString *)persistenceKey + host:(NSString *)host + sslEnabled:(BOOL)sslEnabled { + if (self = [super init]) { + _databaseID = databaseID; + _persistenceKey = [persistenceKey copy]; + _host = [host copy]; + _sslEnabled = sslEnabled; + } + return self; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString + stringWithFormat:@"<FSTDatabaseInfo: databaseID:%@ host:%@>", self.databaseID, self.host]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTEventManager.h b/Firestore/Source/Core/FSTEventManager.h new file mode 100644 index 0000000..43ada66 --- /dev/null +++ b/Firestore/Source/Core/FSTEventManager.h @@ -0,0 +1,88 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTRemoteStore.h" +#import "FSTTypes.h" +#import "FSTViewSnapshot.h" + +@class FSTQuery; +@class FSTSyncEngine; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTListenOptions + +@interface FSTListenOptions : NSObject + ++ (instancetype)defaultOptions; + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, assign, readonly) BOOL includeQueryMetadataChanges; + +@property(nonatomic, assign, readonly) BOOL includeDocumentMetadataChanges; + +@property(nonatomic, assign, readonly) BOOL waitForSyncWhenOnline; + +@end + +#pragma mark - FSTQueryListener + +/** + * FSTQueryListener takes a series of internal view snapshots and determines when to raise + * user-facing events. + */ +@interface FSTQueryListener : NSObject + +- (instancetype)initWithQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot; +- (void)queryDidError:(NSError *)error; +- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState; + +@property(nonatomic, strong, readonly) FSTQuery *query; + +@end + +#pragma mark - FSTEventManager + +/** + * EventManager is responsible for mapping queries to query event emitters. It handles "fan-out." + * (Identical queries will re-use the same watch on the backend.) + */ +@interface FSTEventManager : NSObject <FSTOnlineStateDelegate> + ++ (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine; + +- (instancetype)init __attribute__((unavailable("Use static constructor method."))); + +- (FSTTargetID)addListener:(FSTQueryListener *)listener; +- (void)removeListener:(FSTQueryListener *)listener; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTEventManager.m b/Firestore/Source/Core/FSTEventManager.m new file mode 100644 index 0000000..17a0546 --- /dev/null +++ b/Firestore/Source/Core/FSTEventManager.m @@ -0,0 +1,335 @@ +/* + * 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 "FSTEventManager.h" + +#import "FSTAssert.h" +#import "FSTDocumentSet.h" +#import "FSTQuery.h" +#import "FSTSyncEngine.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTListenOptions + +@implementation FSTListenOptions + ++ (instancetype)defaultOptions { + static FSTListenOptions *defaultOptions; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultOptions = [[FSTListenOptions alloc] initWithIncludeQueryMetadataChanges:NO + includeDocumentMetadataChanges:NO + waitForSyncWhenOnline:NO]; + }); + return defaultOptions; +} + +- (instancetype)initWithIncludeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + waitForSyncWhenOnline:(BOOL)waitForSyncWhenOnline { + if (self = [super init]) { + _includeQueryMetadataChanges = includeQueryMetadataChanges; + _includeDocumentMetadataChanges = includeDocumentMetadataChanges; + _waitForSyncWhenOnline = waitForSyncWhenOnline; + } + return self; +} + +- (instancetype)init { + FSTFail(@"FSTListenOptions init not supported"); + return nil; +} + +@end + +#pragma mark - FSTQueryListenersInfo + +/** + * Holds the listeners and the last received ViewSnapshot for a query being tracked by + * EventManager. + */ +@interface FSTQueryListenersInfo : NSObject +@property(nonatomic, strong, nullable, readwrite) FSTViewSnapshot *viewSnapshot; +@property(nonatomic, assign, readwrite) FSTTargetID targetID; +@property(nonatomic, strong, readonly) NSMutableArray<FSTQueryListener *> *listeners; +@end + +@implementation FSTQueryListenersInfo +- (instancetype)init { + if (self = [super init]) { + _listeners = [NSMutableArray array]; + } + return self; +} + +@end + +#pragma mark - FSTQueryListener + +@interface FSTQueryListener () + +/** The last received view snapshot. */ +@property(nonatomic, strong, nullable) FSTViewSnapshot *snapshot; + +@property(nonatomic, strong, readonly) FSTListenOptions *options; + +/** + * Initial snapshots (e.g. from cache) may not be propagated to the FSTViewSnapshotHandler. + * This flag is set to YES once we've actually raised an event. + */ +@property(nonatomic, assign, readwrite) BOOL raisedInitialEvent; + +/** The last online state this query listener got. */ +@property(nonatomic, assign, readwrite) FSTOnlineState onlineState; + +/** The FSTViewSnapshotHandler associated with this query listener. */ +@property(nonatomic, copy, nullable) FSTViewSnapshotHandler viewSnapshotHandler; + +@end + +@implementation FSTQueryListener + +- (instancetype)initWithQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { + if (self = [super init]) { + _query = query; + _options = options; + _viewSnapshotHandler = viewSnapshotHandler; + _raisedInitialEvent = NO; + } + return self; +} + +- (void)queryDidChangeViewSnapshot:(FSTViewSnapshot *)snapshot { + FSTAssert(snapshot.documentChanges.count > 0 || snapshot.syncStateChanged, + @"We got a new snapshot with no changes?"); + + if (!self.options.includeDocumentMetadataChanges) { + // Remove the metadata-only changes. + NSMutableArray<FSTDocumentViewChange *> *changes = [NSMutableArray array]; + for (FSTDocumentViewChange *change in snapshot.documentChanges) { + if (change.type != FSTDocumentViewChangeTypeMetadata) { + [changes addObject:change]; + } + } + snapshot = [[FSTViewSnapshot alloc] initWithQuery:snapshot.query + documents:snapshot.documents + oldDocuments:snapshot.oldDocuments + documentChanges:changes + fromCache:snapshot.fromCache + hasPendingWrites:snapshot.hasPendingWrites + syncStateChanged:snapshot.syncStateChanged]; + } + + if (!self.raisedInitialEvent) { + if ([self shouldRaiseInitialEventForSnapshot:snapshot onlineState:self.onlineState]) { + [self raiseInitialEventForSnapshot:snapshot]; + } + } else if ([self shouldRaiseEventForSnapshot:snapshot]) { + self.viewSnapshotHandler(snapshot, nil); + } + + self.snapshot = snapshot; +} + +- (void)queryDidError:(NSError *)error { + self.viewSnapshotHandler(nil, error); +} + +- (void)clientDidChangeOnlineState:(FSTOnlineState)onlineState { + self.onlineState = onlineState; + if (self.snapshot && !self.raisedInitialEvent && + [self shouldRaiseInitialEventForSnapshot:self.snapshot onlineState:onlineState]) { + [self raiseInitialEventForSnapshot:self.snapshot]; + } +} + +- (BOOL)shouldRaiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot + onlineState:(FSTOnlineState)onlineState { + FSTAssert(!self.raisedInitialEvent, + @"Determining whether to raise initial event, but already had first event."); + + // Always raise the first event when we're synced + if (!snapshot.fromCache) { + return YES; + } + + // NOTE: We consider OnlineState.Unknown as online (it should become Failed + // or Online if we wait long enough). + BOOL maybeOnline = onlineState != FSTOnlineStateFailed; + // Don't raise the event if we're online, aren't synced yet (checked + // above) and are waiting for a sync. + if (self.options.waitForSyncWhenOnline && maybeOnline) { + FSTAssert(snapshot.fromCache, @"Waiting for sync, but snapshot is not from cache."); + return NO; + } + + // Raise data from cache if we have any documents or we are offline + return !snapshot.documents.isEmpty || onlineState == FSTOnlineStateFailed; +} + +- (BOOL)shouldRaiseEventForSnapshot:(FSTViewSnapshot *)snapshot { + // We don't need to handle includeDocumentMetadataChanges here because the Metadata only changes + // have already been stripped out if needed. At this point the only changes we will see are the + // ones we should propagate. + if (snapshot.documentChanges.count > 0) { + return YES; + } + + BOOL hasPendingWritesChanged = + self.snapshot && self.snapshot.hasPendingWrites != snapshot.hasPendingWrites; + if (snapshot.syncStateChanged || hasPendingWritesChanged) { + return self.options.includeQueryMetadataChanges; + } + + // Generally we should have hit one of the cases above, but it's possible to get here if there + // were only metadata docChanges and they got stripped out. + return NO; +} + +- (void)raiseInitialEventForSnapshot:(FSTViewSnapshot *)snapshot { + FSTAssert(!self.raisedInitialEvent, @"Trying to raise initial events for second time"); + snapshot = [[FSTViewSnapshot alloc] + initWithQuery:snapshot.query + documents:snapshot.documents + oldDocuments:[FSTDocumentSet documentSetWithComparator:snapshot.query.comparator] + documentChanges:[FSTQueryListener getInitialViewChangesFor:snapshot] + fromCache:snapshot.fromCache + hasPendingWrites:snapshot.hasPendingWrites + syncStateChanged:YES]; + self.raisedInitialEvent = YES; + self.viewSnapshotHandler(snapshot, nil); +} + ++ (NSArray<FSTDocumentViewChange *> *)getInitialViewChangesFor:(FSTViewSnapshot *)snapshot { + NSMutableArray<FSTDocumentViewChange *> *result = [NSMutableArray array]; + for (FSTDocument *doc in snapshot.documents.documentEnumerator) { + [result addObject:[FSTDocumentViewChange changeWithDocument:doc + type:FSTDocumentViewChangeTypeAdded]]; + } + return result; +} + +@end + +#pragma mark - FSTEventManager + +@interface FSTEventManager () <FSTSyncEngineDelegate> + +- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTQuery *, FSTQueryListenersInfo *> *queries; +@property(nonatomic, assign) FSTOnlineState onlineState; + +@end + +@implementation FSTEventManager + ++ (instancetype)eventManagerWithSyncEngine:(FSTSyncEngine *)syncEngine { + return [[FSTEventManager alloc] initWithSyncEngine:syncEngine]; +} + +- (instancetype)initWithSyncEngine:(FSTSyncEngine *)syncEngine { + if (self = [super init]) { + _syncEngine = syncEngine; + _queries = [NSMutableDictionary dictionary]; + + _syncEngine.delegate = self; + } + return self; +} + +- (FSTTargetID)addListener:(FSTQueryListener *)listener { + FSTQuery *query = listener.query; + BOOL firstListen = NO; + + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (!queryInfo) { + firstListen = YES; + queryInfo = [[FSTQueryListenersInfo alloc] init]; + self.queries[query] = queryInfo; + } + [queryInfo.listeners addObject:listener]; + + [listener clientDidChangeOnlineState:self.onlineState]; + + if (queryInfo.viewSnapshot) { + [listener queryDidChangeViewSnapshot:queryInfo.viewSnapshot]; + } + + if (firstListen) { + queryInfo.targetID = [self.syncEngine listenToQuery:query]; + } + return queryInfo.targetID; +} + +- (void)removeListener:(FSTQueryListener *)listener { + FSTQuery *query = listener.query; + BOOL lastListen = NO; + + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + [queryInfo.listeners removeObject:listener]; + lastListen = (queryInfo.listeners.count == 0); + } + + if (lastListen) { + [self.queries removeObjectForKey:query]; + [self.syncEngine stopListeningToQuery:query]; + } +} + +- (void)handleViewSnapshots:(NSArray<FSTViewSnapshot *> *)viewSnapshots { + for (FSTViewSnapshot *viewSnapshot in viewSnapshots) { + FSTQuery *query = viewSnapshot.query; + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + for (FSTQueryListener *listener in queryInfo.listeners) { + [listener queryDidChangeViewSnapshot:viewSnapshot]; + } + queryInfo.viewSnapshot = viewSnapshot; + } + } +} + +- (void)handleError:(NSError *)error forQuery:(FSTQuery *)query { + FSTQueryListenersInfo *queryInfo = self.queries[query]; + if (queryInfo) { + for (FSTQueryListener *listener in queryInfo.listeners) { + [listener queryDidError:error]; + } + } + + // Remove all listeners. NOTE: We don't need to call [FSTSyncEngine stopListening] after an error. + [self.queries removeObjectForKey:query]; +} + +- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState { + self.onlineState = onlineState; + for (FSTQueryListenersInfo *info in self.queries.objectEnumerator) { + for (FSTQueryListener *listener in info.listeners) { + [listener clientDidChangeOnlineState:onlineState]; + } + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.h b/Firestore/Source/Core/FSTFirestoreClient.h new file mode 100644 index 0000000..45f13cc --- /dev/null +++ b/Firestore/Source/Core/FSTFirestoreClient.h @@ -0,0 +1,87 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTRemoteStore.h" +#import "FSTTypes.h" +#import "FSTViewSnapshot.h" + +@class FSTDatabaseID; +@class FSTDatabaseInfo; +@class FSTDispatchQueue; +@class FSTDocument; +@class FSTListenOptions; +@class FSTMutation; +@class FSTQuery; +@class FSTQueryListener; +@class FSTTransaction; +@protocol FSTCredentialsProvider; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FirestoreClient is a top-level class that constructs and owns all of the pieces of the client + * 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 + +/** + * Creates and returns a FSTFirestoreClient with the given parameters. + * + * All callbacks and events will be triggered on the provided userDispatchQueue. + */ ++ (instancetype)clientWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue; + +- (instancetype)init __attribute__((unavailable("Use static constructor method."))); + +/** Shuts down this client, cancels all writes / listeners, and releases all resources. */ +- (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion; + +/** Starts listening to a query. */ +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler; + +/** Stops listening to a query previously listened to. */ +- (void)removeListener:(FSTQueryListener *)listener; + +/** Write mutations. completion will be notified when it's written to the backend. */ +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations + completion:(nullable FSTVoidErrorBlock)completion; + +/** Tries to execute the transaction in updateBlock up to retries times. */ +- (void)transactionWithRetries:(int)retries + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion; + +/** The database ID of the databaseInfo this client was initialized with. */ +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; + +/** + * Dispatch queue for user callbacks / events. This will often be the "Main Dispatch Queue" of the + * app but the developer can configure it to a different queue if they so choose. + */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *userDispatchQueue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTFirestoreClient.m b/Firestore/Source/Core/FSTFirestoreClient.m new file mode 100644 index 0000000..2066ce9 --- /dev/null +++ b/Firestore/Source/Core/FSTFirestoreClient.m @@ -0,0 +1,271 @@ +/* + * 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 "FSTFirestoreClient.h" + +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTCredentialsProvider.h" +#import "FSTDatabaseInfo.h" +#import "FSTDatastore.h" +#import "FSTDispatchQueue.h" +#import "FSTEagerGarbageCollector.h" +#import "FSTEventManager.h" +#import "FSTLevelDB.h" +#import "FSTLocalSerializer.h" +#import "FSTLocalStore.h" +#import "FSTLogger.h" +#import "FSTMemoryPersistence.h" +#import "FSTNoOpGarbageCollector.h" +#import "FSTRemoteStore.h" +#import "FSTSerializerBeta.h" +#import "FSTSyncEngine.h" +#import "FSTTransaction.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTFirestoreClient () +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)queue NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; +@property(nonatomic, strong, readonly) FSTEventManager *eventManager; +@property(nonatomic, strong, readonly) id<FSTPersistence> persistence; +@property(nonatomic, strong, readonly) FSTSyncEngine *syncEngine; +@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; + +/** + * Dispatch queue responsible for all of our internal processing. When we get incoming work from + * the user (via public API) or the network (incoming GRPC messages), we should always dispatch + * onto this queue. This ensures our internal data structures are never accessed from multiple + * threads simultaneously. + */ +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; + +@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentialsProvider; + +@end + +@implementation FSTFirestoreClient + ++ (instancetype)clientWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + return [[FSTFirestoreClient alloc] initWithDatabaseInfo:databaseInfo + usePersistence:usePersistence + credentialsProvider:credentialsProvider + userDispatchQueue:userDispatchQueue + workerDispatchQueue:workerDispatchQueue]; +} + +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + usePersistence:(BOOL)usePersistence + credentialsProvider:(id<FSTCredentialsProvider>)credentialsProvider + userDispatchQueue:(FSTDispatchQueue *)userDispatchQueue + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue { + if (self = [super init]) { + _databaseInfo = databaseInfo; + _credentialsProvider = credentialsProvider; + _userDispatchQueue = userDispatchQueue; + _workerDispatchQueue = workerDispatchQueue; + + dispatch_semaphore_t initialUserAvailable = dispatch_semaphore_create(0); + __block FSTUser *initialUser; + FSTWeakify(self); + _credentialsProvider.userChangeListener = ^(FSTUser *user) { + FSTStrongify(self); + if (self) { + if (!initialUser) { + initialUser = user; + dispatch_semaphore_signal(initialUserAvailable); + } else { + [workerDispatchQueue dispatchAsync:^{ + [self userDidChange:user]; + }]; + } + } + }; + + // Defer initialization until we get the current user from the userChangeListener. This is + // guaranteed to be synchronously dispatched onto our worker queue, so we will be initialized + // before any subsequently queued work runs. + [_workerDispatchQueue dispatchAsync:^{ + dispatch_semaphore_wait(initialUserAvailable, DISPATCH_TIME_FOREVER); + + [self initializeWithUser:initialUser usePersistence:usePersistence]; + }]; + } + return self; +} + +- (void)initializeWithUser:(FSTUser *)user usePersistence:(BOOL)usePersistence { + // Do all of our initialization on our own dispatch queue. + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Note: The initialization work must all be synchronous (we can't dispatch more work) since + // external write/listen operations could get queued to run before that subsequent work + // completes. + id<FSTGarbageCollector> garbageCollector; + if (usePersistence) { + // TODO(http://b/33384523): For now we just disable garbage collection when persistence is + // enabled. + garbageCollector = [[FSTNoOpGarbageCollector alloc] init]; + + NSString *dir = [FSTLevelDB storageDirectoryForDatabaseInfo:self.databaseInfo + documentsDirectory:[FSTLevelDB documentsDirectory]]; + + FSTSerializerBeta *remoteSerializer = + [[FSTSerializerBeta alloc] initWithDatabaseID:self.databaseInfo.databaseID]; + FSTLocalSerializer *serializer = + [[FSTLocalSerializer alloc] initWithRemoteSerializer:remoteSerializer]; + + _persistence = [[FSTLevelDB alloc] initWithDirectory:dir serializer:serializer]; + } else { + garbageCollector = [[FSTEagerGarbageCollector alloc] init]; + _persistence = [FSTMemoryPersistence persistence]; + } + + NSError *error; + if (![_persistence start:&error]) { + // If local storage fails to start then just throw up our hands: the error is unrecoverable. + // There's nothing an end-user can do and nearly all failures indicate the developer is doing + // something grossly wrong so we should stop them cold in their tracks with a failure they + // can't ignore. + [NSException raise:NSInternalInconsistencyException format:@"Failed to open DB: %@", error]; + } + + _localStore = [[FSTLocalStore alloc] initWithPersistence:_persistence + garbageCollector:garbageCollector + initialUser:user]; + + FSTDatastore *datastore = [FSTDatastore datastoreWithDatabase:self.databaseInfo + workerDispatchQueue:self.workerDispatchQueue + credentials:self.credentialsProvider]; + + _remoteStore = [FSTRemoteStore remoteStoreWithLocalStore:_localStore datastore:datastore]; + + _syncEngine = [[FSTSyncEngine alloc] initWithLocalStore:_localStore + remoteStore:_remoteStore + initialUser:user]; + + _eventManager = [FSTEventManager eventManagerWithSyncEngine:_syncEngine]; + + // Setup wiring for remote store. + _remoteStore.syncEngine = _syncEngine; + + _remoteStore.onlineStateDelegate = _eventManager; + + // NOTE: RemoteStore depends on LocalStore (for persisting stream tokens, refilling mutation + // queue, etc.) so must be started after LocalStore. + [_localStore start]; + [_remoteStore start]; +} + +- (void)userDidChange:(FSTUser *)user { + [self.workerDispatchQueue verifyIsCurrentQueue]; + + FSTLog(@"User Changed: %@", user); + [self.syncEngine userDidChange:user]; +} + +- (void)shutdownWithCompletion:(nullable FSTVoidErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + self.credentialsProvider.userChangeListener = nil; + + [self.remoteStore shutdown]; + [self.localStore shutdown]; + [self.persistence shutdown]; + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(nil); + }]; + } + }]; +} + +- (FSTQueryListener *)listenToQuery:(FSTQuery *)query + options:(FSTListenOptions *)options + viewSnapshotHandler:(FSTViewSnapshotHandler)viewSnapshotHandler { + FSTQueryListener *listener = [[FSTQueryListener alloc] initWithQuery:query + options:options + viewSnapshotHandler:viewSnapshotHandler]; + + [self.workerDispatchQueue dispatchAsync:^{ + [self.eventManager addListener:listener]; + }]; + + return listener; +} + +- (void)removeListener:(FSTQueryListener *)listener { + [self.workerDispatchQueue dispatchAsync:^{ + [self.eventManager removeListener:listener]; + }]; +} + +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations + completion:(nullable FSTVoidErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + if (mutations.count == 0) { + [self.userDispatchQueue dispatchAsync:^{ + completion(nil); + }]; + } else { + [self.syncEngine writeMutations:mutations + completion:^(NSError *error) { + // Dispatch the result back onto the user dispatch queue. + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(error); + }]; + } + }]; + } + }]; +}; + +- (void)transactionWithRetries:(int)retries + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion { + [self.workerDispatchQueue dispatchAsync:^{ + [self.syncEngine transactionWithRetries:retries + workerDispatchQueue:self.workerDispatchQueue + updateBlock:updateBlock + completion:^(id _Nullable result, NSError *_Nullable error) { + // Dispatch the result back onto the user dispatch queue. + if (completion) { + [self.userDispatchQueue dispatchAsync:^{ + completion(result, error); + }]; + } + }]; + + }]; +} + +- (FSTDatabaseID *)databaseID { + return self.databaseInfo.databaseID; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTQuery.h b/Firestore/Source/Core/FSTQuery.h new file mode 100644 index 0000000..0562ae4 --- /dev/null +++ b/Firestore/Source/Core/FSTQuery.h @@ -0,0 +1,269 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTDocument; +@class FSTDocumentKey; +@class FSTFieldPath; +@class FSTFieldValue; +@class FSTResourcePath; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTRelationFilterOperator is a value relation operator that can be used to filter documents. + * It is similar to NSPredicateOperatorType, but only has operators supported by Firestore. + */ +typedef NS_ENUM(NSInteger, FSTRelationFilterOperator) { + FSTRelationFilterOperatorLessThan = 0, + FSTRelationFilterOperatorLessThanOrEqual, + FSTRelationFilterOperatorEqual, + FSTRelationFilterOperatorGreaterThanOrEqual, + FSTRelationFilterOperatorGreaterThan, +}; + +/** Interface used for all query filters. */ +@protocol FSTFilter <NSObject> + +/** Returns the field the Filter operates over. */ +- (FSTFieldPath *)field; + +/** Returns true if a document matches the filter. */ +- (BOOL)matchesDocument:(FSTDocument *)document; + +/** A unique ID identifying the filter; used when serializing queries. */ +- (NSString *)canonicalID; + +@end + +/** + * FSTRelationFilter is a document filter constraint on a query with a single relation operator. + * It is similar to NSComparisonPredicate, except customized for Firestore semantics. + */ +@interface FSTRelationFilter : NSObject <FSTFilter> + +/** + * Creates a new constraint for filtering documents. + * + * @param field A path to a field in the document to filter on. The LHS of the expression. + * @param filterOperator The binary operator to apply. + * @param value A constant value to compare @a field to. The RHS of the expression. + * @return A new instance of FSTRelationFilter. + */ ++ (instancetype)filterWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value; + +- (instancetype)init NS_UNAVAILABLE; + +/** Returns YES if the receiver is not an equality relation. */ +- (BOOL)isInequality; + +/** The left hand side of the relation. A path into a document field. */ +@property(nonatomic, strong, readonly) FSTFieldPath *field; + +/** The type of equality/inequality operator to use in the relation. */ +@property(nonatomic, assign, readonly) FSTRelationFilterOperator filterOperator; + +/** The right hand side of the relation. A constant value to compare to. */ +@property(nonatomic, strong, readonly) FSTFieldValue *value; + +@end + +/** Filter that matches NULL values. */ +@interface FSTNullFilter : NSObject <FSTFilter> +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithField:(FSTFieldPath *)field NS_DESIGNATED_INITIALIZER; +@end + +/** Filter that matches NAN values. */ +@interface FSTNanFilter : NSObject <FSTFilter> +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithField:(FSTFieldPath *)field NS_DESIGNATED_INITIALIZER; +@end + +/** FSTSortOrder is a field and direction to order query results by. */ +@interface FSTSortOrder : NSObject <NSCopying> + +/** Creates a new sort order with the given field and direction. */ ++ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending; + +- (instancetype)init NS_UNAVAILABLE; + +/** Compares two documents based on the field and direction of this sort order. */ +- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2; + +/** The direction of the sort. */ +@property(nonatomic, assign, readonly, getter=isAscending) BOOL ascending; + +/** The field to sort by. */ +@property(nonatomic, strong, readonly) FSTFieldPath *field; + +@end + +/** + * FSTBound represents a bound of a query. + * + * The bound is specified with the given components representing a position and whether it's just + * before or just after the position (relative to whatever the query order is). + * + * The position represents a logical index position for a query. It's a prefix of values for + * the (potentially implicit) order by clauses of a query. + * + * FSTBound provides a function to determine whether a document comes before or after a bound. + * This is influenced by whether the position is just before or just after the provided values. + */ +@interface FSTBound : NSObject <NSCopying> + +/** + * Creates a new bound. + * + * @param position The position relative to the sort order. + * @param isBefore Whether this bound is just before or just after the position. + */ ++ (instancetype)boundWithPosition:(NSArray<FSTFieldValue *> *)position isBefore:(BOOL)isBefore; + +/** Whether this bound is just before or just after the provided position */ +@property(nonatomic, assign, readonly, getter=isBefore) BOOL before; + +/** The index position of this bound represented as an array of field values. */ +@property(nonatomic, strong, readonly) NSArray<FSTFieldValue *> *position; + +/** Returns YES if a document comes before a bound using the provided sort order. */ +- (BOOL)sortsBeforeDocument:(FSTDocument *)document + usingSortOrder:(NSArray<FSTSortOrder *> *)sortOrder; + +@end + +/** FSTQuery represents the internal structure of a Firestore query. */ +@interface FSTQuery : NSObject <NSCopying> + +- (id)init NS_UNAVAILABLE; + +/** + * Initializes a query with all of its components directly. + */ +- (instancetype)initWithPath:(FSTResourcePath *)path + filterBy:(NSArray<id<FSTFilter>> *)filters + orderBy:(NSArray<FSTSortOrder *> *)sortOrders + limit:(NSInteger)limit + startAt:(nullable FSTBound *)startAtBound + endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER; + +/** + * Creates and returns a new FSTQuery. + * + * @param path The path to the collection to be queried over. + * @return A new instance of FSTQuery. + */ ++ (instancetype)queryWithPath:(FSTResourcePath *)path; + +/** + * Returns the list of ordering constraints that were explicitly requested on the query by the + * user. + * + * Note that the actual query performed might add additional sort orders to match the behavior + * of the backend. + */ +- (NSArray<FSTSortOrder *> *)explicitSortOrders; + +/** + * Returns the full list of ordering constraints on the query. + * + * This might include additional sort orders added implicitly to match the backend behavior. + */ +- (NSArray<FSTSortOrder *> *)sortOrders; + +/** + * Creates a new FSTQuery with an additional filter. + * + * @param filter The predicate to filter by. + * @return the new FSTQuery. + */ +- (instancetype)queryByAddingFilter:(id<FSTFilter>)filter; + +/** + * Creates a new FSTQuery with an additional ordering constraint. + * + * @param sortOrder The key and direction to order by. + * @return the new FSTQuery. + */ +- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder; + +/** + * Returns a new FSTQuery with the given limit on how many results can be returned. + * + * @param limit The maximum number of results to return. If @a limit <= 0, behavior is unspecified. + * If @a limit == NSNotFound, then no limit is applied. + */ +- (instancetype)queryBySettingLimit:(NSInteger)limit; + +/** + * Creates a new FSTQuery starting at the provided bound. + * + * @param bound The bound to start this query at. + * @return the new FSTQuery. + */ +- (instancetype)queryByAddingStartAt:(FSTBound *)bound; + +/** + * Creates a new FSTQuery ending at the provided bound. + * + * @param bound The bound to end this query at. + * @return the new FSTQuery. + */ +- (instancetype)queryByAddingEndAt:(FSTBound *)bound; + +/** Returns YES if the receiver is query for a specific document. */ +- (BOOL)isDocumentQuery; + +/** Returns YES if the @a document matches the constraints of the receiver. */ +- (BOOL)matchesDocument:(FSTDocument *)document; + +/** Returns a comparator that will sort documents according to the receiver's sort order. */ +- (NSComparator)comparator; + +/** Returns the field of the first filter on the receiver that's an inequality, or nil if none. */ +- (FSTFieldPath *_Nullable)inequalityFilterField; + +/** Returns the first field in an order-by constraint, or nil if none. */ +- (FSTFieldPath *_Nullable)firstSortOrderField; + +/** The base path of the query. */ +@property(nonatomic, strong, readonly) FSTResourcePath *path; + +/** The filters on the documents returned by the query. */ +@property(nonatomic, strong, readonly) NSArray<id<FSTFilter>> *filters; + +/** The maximum number of results to return, or NSNotFound if no limit. */ +@property(nonatomic, assign, readonly) NSInteger limit; + +/** + * A canonical string identifying the query. Two different instances of equivalent queries will + * return the same canonicalID. + */ +@property(nonatomic, strong, readonly) NSString *canonicalID; + +/** An optional bound to start the query at. */ +@property(nonatomic, nullable, strong, readonly) FSTBound *startAt; + +/** An optional bound to end the query at. */ +@property(nonatomic, nullable, strong, readonly) FSTBound *endAt; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTQuery.m b/Firestore/Source/Core/FSTQuery.m new file mode 100644 index 0000000..b220c7c --- /dev/null +++ b/Firestore/Source/Core/FSTQuery.m @@ -0,0 +1,759 @@ +/* + * 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 "FSTQuery.h" + +#import "FIRFirestore+Internal.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTRelationFilterOperator functions + +NSString *FSTStringFromQueryRelationOperator(FSTRelationFilterOperator filterOperator) { + switch (filterOperator) { + case FSTRelationFilterOperatorLessThan: + return @"<"; + case FSTRelationFilterOperatorLessThanOrEqual: + return @"<="; + case FSTRelationFilterOperatorEqual: + return @"=="; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return @">="; + case FSTRelationFilterOperatorGreaterThan: + return @">"; + default: + FSTCFail(@"Unknown FSTRelationFilterOperator %lu", (unsigned long)filterOperator); + } +} + +#pragma mark - FSTRelationFilter + +@interface FSTRelationFilter () + +/** + * Initializes the receiver relation filter. + * + * @param field A path to a field in the document to filter on. The LHS of the expression. + * @param filterOperator The binary operator to apply. + * @param value A constant value to compare @a field to. The RHS of the expression. + */ +- (instancetype)initWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value NS_DESIGNATED_INITIALIZER; + +/** Returns YES if @a document matches the receiver's constraint. */ +- (BOOL)matchesDocument:(FSTDocument *)document; + +/** + * A canonical string identifying the filter. Two different instances of equivalent filters will + * return the same canonicalID. + */ +- (NSString *)canonicalID; + +@end + +@implementation FSTRelationFilter + +#pragma mark - Constructor methods + ++ (instancetype)filterWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value { + return [[FSTRelationFilter alloc] initWithField:field filterOperator:filterOperator value:value]; +} + +- (instancetype)initWithField:(FSTFieldPath *)field + filterOperator:(FSTRelationFilterOperator)filterOperator + value:(FSTFieldValue *)value { + self = [super init]; + if (self) { + _field = field; + _filterOperator = filterOperator; + _value = value; + } + return self; +} + +#pragma mark - Public Methods + +- (BOOL)isInequality { + return self.filterOperator != FSTRelationFilterOperatorEqual; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"%@ %@ %@", [self.field canonicalString], + FSTStringFromQueryRelationOperator(self.filterOperator), + self.value]; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTRelationFilter class]]) { + return NO; + } + return [self isEqualToFilter:(FSTRelationFilter *)other]; +} + +#pragma mark - Private methods + +- (BOOL)matchesDocument:(FSTDocument *)document { + if ([self.field isKeyFieldPath]) { + FSTAssert([self.value isKindOfClass:[FSTReferenceValue class]], + @"Comparing on key, but filter value not a FSTReferenceValue."); + FSTReferenceValue *refValue = (FSTReferenceValue *)self.value; + NSComparisonResult comparison = FSTDocumentKeyComparator(document.key, refValue.value); + return [self matchesComparison:comparison]; + } else { + return [self matchesValue:[document fieldForPath:self.field]]; + } +} + +- (NSString *)canonicalID { + // TODO(b/37283291): This should be collision robust and avoid relying on |description| methods. + return [NSString stringWithFormat:@"%@%@%@", [self.field canonicalString], + FSTStringFromQueryRelationOperator(self.filterOperator), + [self.value value]]; +} + +- (BOOL)isEqualToFilter:(FSTRelationFilter *)other { + if (self.filterOperator != other.filterOperator) { + return NO; + } + if (![self.field isEqual:other.field]) { + return NO; + } + if (![self.value isEqual:other.value]) { + return NO; + } + return YES; +} + +/** Returns YES if receiver is true with the given value as its LHS. */ +- (BOOL)matchesValue:(FSTFieldValue *)other { + // Only compare types with matching backend order (such as double and int). + return self.value.typeOrder == other.typeOrder && + [self matchesComparison:[other compare:self.value]]; +} + +- (BOOL)matchesComparison:(NSComparisonResult)comparison { + switch (self.filterOperator) { + case FSTRelationFilterOperatorLessThan: + return comparison == NSOrderedAscending; + case FSTRelationFilterOperatorLessThanOrEqual: + return comparison == NSOrderedAscending || comparison == NSOrderedSame; + case FSTRelationFilterOperatorEqual: + return comparison == NSOrderedSame; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return comparison == NSOrderedDescending || comparison == NSOrderedSame; + case FSTRelationFilterOperatorGreaterThan: + return comparison == NSOrderedDescending; + default: + FSTFail(@"Unknown operator: %ld", (long)self.filterOperator); + } +} + +@end + +#pragma mark - FSTNullFilter + +@interface FSTNullFilter () +@property(nonatomic, strong, readonly) FSTFieldPath *field; +@end + +@implementation FSTNullFilter +- (instancetype)initWithField:(FSTFieldPath *)field { + if (self = [super init]) { + _field = field; + } + return self; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + FSTFieldValue *fieldValue = [document fieldForPath:self.field]; + return fieldValue != nil && [fieldValue isEqual:[FSTNullValue nullValue]]; +} + +- (NSString *)canonicalID { + return [NSString stringWithFormat:@"%@ IS NULL", [self.field canonicalString]]; +} + +- (NSString *)description { + return [self canonicalID]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + + return [self.field isEqual:((FSTNullFilter *)other).field]; +} + +- (NSUInteger)hash { + return [self.field hash]; +} + +@end + +#pragma mark - FSTNanFilter + +@interface FSTNanFilter () +@property(nonatomic, strong, readonly) FSTFieldPath *field; +@end + +@implementation FSTNanFilter + +- (instancetype)initWithField:(FSTFieldPath *)field { + if (self = [super init]) { + _field = field; + } + return self; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + FSTFieldValue *fieldValue = [document fieldForPath:self.field]; + return fieldValue != nil && [fieldValue isEqual:[FSTDoubleValue nanValue]]; +} + +- (NSString *)canonicalID { + return [NSString stringWithFormat:@"%@ IS NaN", [self.field canonicalString]]; +} + +- (NSString *)description { + return [self canonicalID]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + + return [self.field isEqual:((FSTNanFilter *)other).field]; +} + +- (NSUInteger)hash { + return [self.field hash]; +} +@end + +#pragma mark - FSTSortOrder + +@interface FSTSortOrder () + +/** Creates a new sort order with the given field and direction. */ +- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending; + +- (NSString *)canonicalID; + +@end + +@implementation FSTSortOrder + +#pragma mark - Constructor methods + ++ (instancetype)sortOrderWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending { + return [[FSTSortOrder alloc] initWithFieldPath:fieldPath ascending:ascending]; +} + +- (instancetype)initWithFieldPath:(FSTFieldPath *)fieldPath ascending:(BOOL)ascending { + self = [super init]; + if (self) { + _field = fieldPath; + _ascending = ascending; + } + return self; +} + +#pragma mark - Public methods + +- (NSComparisonResult)compareDocument:(FSTDocument *)document1 toDocument:(FSTDocument *)document2 { + int modifier = self.isAscending ? 1 : -1; + if ([self.field isEqual:[FSTFieldPath keyFieldPath]]) { + return (NSComparisonResult)(modifier * FSTDocumentKeyComparator(document1.key, document2.key)); + } else { + FSTFieldValue *value1 = [document1 fieldForPath:self.field]; + FSTFieldValue *value2 = [document2 fieldForPath:self.field]; + FSTAssert(value1 != nil && value2 != nil, + @"Trying to compare documents on fields that don't exist."); + return modifier * [value1 compare:value2]; + } +} + +- (NSString *)canonicalID { + return [NSString + stringWithFormat:@"%@%@", self.field.canonicalString, self.isAscending ? @"asc" : @"desc"]; +} + +- (BOOL)isEqualToSortOrder:(FSTSortOrder *)other { + return [self.field isEqual:other.field] && self.isAscending == other.isAscending; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTSortOrder: path:%@ dir:%@>", self.field, + self.ascending ? @"asc" : @"desc"]; +} + +- (BOOL)isEqual:(NSObject *)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTSortOrder class]]) { + return NO; + } + return [self isEqualToSortOrder:(FSTSortOrder *)other]; +} + +- (NSUInteger)hash { + return [self.canonicalID hash]; +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +@end + +#pragma mark - FSTBound + +@implementation FSTBound + +- (instancetype)initWithPosition:(NSArray<FSTFieldValue *> *)position isBefore:(BOOL)isBefore { + if (self = [super init]) { + _position = position; + _before = isBefore; + } + return self; +} + ++ (instancetype)boundWithPosition:(NSArray<FSTFieldValue *> *)position isBefore:(BOOL)isBefore { + return [[FSTBound alloc] initWithPosition:position isBefore:isBefore]; +} + +- (NSString *)canonicalString { + // TODO(b/29183165): Make this collision robust. + NSMutableString *string = [NSMutableString string]; + if (self.isBefore) { + [string appendString:@"b:"]; + } else { + [string appendString:@"a:"]; + } + for (FSTFieldValue *component in self.position) { + [string appendFormat:@"%@", component]; + } + return string; +} + +- (BOOL)sortsBeforeDocument:(FSTDocument *)document + usingSortOrder:(NSArray<FSTSortOrder *> *)sortOrder { + FSTAssert(self.position.count <= sortOrder.count, + @"FSTIndexPosition has more components than provided sort order."); + __block NSComparisonResult result = NSOrderedSame; + [self.position enumerateObjectsUsingBlock:^(FSTFieldValue *fieldValue, NSUInteger idx, + BOOL *stop) { + FSTSortOrder *sortOrderComponent = sortOrder[idx]; + NSComparisonResult comparison; + if ([sortOrderComponent.field isEqual:[FSTFieldPath keyFieldPath]]) { + FSTAssert([fieldValue isKindOfClass:[FSTReferenceValue class]], + @"FSTBound has a non-key value where the key path is being used %@", fieldValue); + comparison = [fieldValue.value compare:document.key]; + } else { + FSTFieldValue *docValue = [document fieldForPath:sortOrderComponent.field]; + FSTAssert(docValue != nil, @"Field should exist since document matched the orderBy already."); + comparison = [fieldValue compare:docValue]; + } + + if (!sortOrderComponent.isAscending) { + comparison = comparison * -1; + } + + if (comparison != 0) { + result = comparison; + *stop = YES; + } + }]; + + return self.isBefore ? result <= NSOrderedSame : result < NSOrderedSame; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTBound: position:%@ before:%@>", self.position, + self.isBefore ? @"YES" : @"NO"]; +} + +- (BOOL)isEqual:(NSObject *)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTBound class]]) { + return NO; + } + + FSTBound *otherBound = (FSTBound *)other; + + return [self.position isEqualToArray:otherBound.position] && self.isBefore == otherBound.isBefore; +} + +- (NSUInteger)hash { + return 31 * self.position.hash + (self.isBefore ? 0 : 1); +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +@end + +#pragma mark - FSTQuery + +@interface FSTQuery () { + // Cached value of the canonicalID property. + NSString *_canonicalID; +} + +/** + * Initializes the receiver with the given query constraints. + * + * @param path The base path of the query. + * @param filters Filters specify which documents to include in the results. + * @param sortOrders The fields and directions to sort the results. + * @param limit If not NSNotFound, only this many results will be returned. + */ +- (instancetype)initWithPath:(FSTResourcePath *)path + filterBy:(NSArray<id<FSTFilter>> *)filters + orderBy:(NSArray<FSTSortOrder *> *)sortOrders + limit:(NSInteger)limit + startAt:(nullable FSTBound *)startAtBound + endAt:(nullable FSTBound *)endAtBound NS_DESIGNATED_INITIALIZER; + +/** A list of fields given to sort by. This does not include the implicit key sort at the end. */ +@property(nonatomic, strong, readonly) NSArray<FSTSortOrder *> *explicitSortOrders; + +/** The memoized list of sort orders */ +@property(nonatomic, nullable, strong, readwrite) NSArray<FSTSortOrder *> *memoizedSortOrders; + +@end + +@implementation FSTQuery + +#pragma mark - Constructors + ++ (instancetype)queryWithPath:(FSTResourcePath *)path { + return [[FSTQuery alloc] initWithPath:path + filterBy:@[] + orderBy:@[] + limit:NSNotFound + startAt:nil + endAt:nil]; +} + +- (instancetype)initWithPath:(FSTResourcePath *)path + filterBy:(NSArray<id<FSTFilter>> *)filters + orderBy:(NSArray<FSTSortOrder *> *)sortOrders + limit:(NSInteger)limit + startAt:(nullable FSTBound *)startAtBound + endAt:(nullable FSTBound *)endAtBound { + if (self = [super init]) { + _path = path; + _filters = filters; + _explicitSortOrders = sortOrders; + _limit = limit; + _startAt = startAtBound; + _endAt = endAtBound; + } + return self; +} + +#pragma mark - NSObject methods + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTQuery: canonicalID:%@>", self.canonicalID]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTQuery class]]) { + return NO; + } + return [self isEqualToQuery:(FSTQuery *)object]; +} + +- (NSUInteger)hash { + return [self.canonicalID hash]; +} + +- (instancetype)copyWithZone:(nullable NSZone *)zone { + return self; +} + +#pragma mark - Public methods + +- (NSArray *)sortOrders { + if (self.memoizedSortOrders == nil) { + FSTFieldPath *_Nullable inequalityField = [self inequalityFilterField]; + FSTFieldPath *_Nullable firstSortOrderField = [self firstSortOrderField]; + if (inequalityField && !firstSortOrderField) { + // In order to implicitly add key ordering, we must also add the inequality filter field for + // it to be a valid query. Note that the default inequality field and key ordering is + // ascending. + if ([inequalityField isKeyFieldPath]) { + self.memoizedSortOrders = + @[ [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] ]; + } else { + self.memoizedSortOrders = @[ + [FSTSortOrder sortOrderWithFieldPath:inequalityField ascending:YES], + [FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] ascending:YES] + ]; + } + } else { + FSTAssert(!inequalityField || [inequalityField isEqual:firstSortOrderField], + @"First orderBy %@ should match inequality field %@.", firstSortOrderField, + inequalityField); + + __block BOOL foundKeyOrder = NO; + + NSMutableArray *result = [NSMutableArray array]; + for (FSTSortOrder *sortOrder in self.explicitSortOrders) { + [result addObject:sortOrder]; + if ([sortOrder.field isEqual:[FSTFieldPath keyFieldPath]]) { + foundKeyOrder = YES; + } + } + + if (!foundKeyOrder) { + // The direction of the implicit key ordering always matches the direction of the last + // explicit sort order + BOOL lastIsAscending = + self.explicitSortOrders.count > 0 ? self.explicitSortOrders.lastObject.ascending : YES; + [result addObject:[FSTSortOrder sortOrderWithFieldPath:[FSTFieldPath keyFieldPath] + ascending:lastIsAscending]]; + } + + self.memoizedSortOrders = result; + } + } + return self.memoizedSortOrders; +} + +- (instancetype)queryByAddingFilter:(id<FSTFilter>)filter { + FSTAssert(![FSTDocumentKey isDocumentKey:self.path], @"No filtering allowed for document query"); + + FSTFieldPath *_Nullable newInequalityField = nil; + if ([filter isKindOfClass:[FSTRelationFilter class]] && + [((FSTRelationFilter *)filter)isInequality]) { + newInequalityField = filter.field; + } + FSTFieldPath *_Nullable queryInequalityField = [self inequalityFilterField]; + FSTAssert(!queryInequalityField || !newInequalityField || + [queryInequalityField isEqual:newInequalityField], + @"Query must only have one inequality field."); + + return [[FSTQuery alloc] initWithPath:self.path + filterBy:[self.filters arrayByAddingObject:filter] + orderBy:self.explicitSortOrders + limit:self.limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryByAddingSortOrder:(FSTSortOrder *)sortOrder { + FSTAssert(![FSTDocumentKey isDocumentKey:self.path], + @"No ordering is allowed for a document query."); + + // TODO(klimt): Validate that the same key isn't added twice. + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:[self.explicitSortOrders arrayByAddingObject:sortOrder] + limit:self.limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryBySettingLimit:(NSInteger)limit { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:limit + startAt:self.startAt + endAt:self.endAt]; +} + +- (instancetype)queryByAddingStartAt:(FSTBound *)bound { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:self.limit + startAt:bound + endAt:self.endAt]; +} + +- (instancetype)queryByAddingEndAt:(FSTBound *)bound { + return [[FSTQuery alloc] initWithPath:self.path + filterBy:self.filters + orderBy:self.explicitSortOrders + limit:self.limit + startAt:self.startAt + endAt:bound]; +} + +- (BOOL)isDocumentQuery { + return [FSTDocumentKey isDocumentKey:self.path] && self.filters.count == 0; +} + +- (BOOL)matchesDocument:(FSTDocument *)document { + return [self pathMatchesDocument:document] && [self orderByMatchesDocument:document] && + [self filtersMatchDocument:document] && [self boundsMatchDocument:document]; +} + +- (NSComparator)comparator { + return ^NSComparisonResult(id document1, id document2) { + BOOL didCompareOnKeyField = NO; + for (FSTSortOrder *orderBy in self.sortOrders) { + NSComparisonResult comp = [orderBy compareDocument:document1 toDocument:document2]; + if (comp != NSOrderedSame) { + return comp; + } + didCompareOnKeyField = + didCompareOnKeyField || [orderBy.field isEqual:[FSTFieldPath keyFieldPath]]; + } + FSTAssert(didCompareOnKeyField, @"sortOrder of query did not include key ordering"); + return NSOrderedSame; + }; +} + +- (FSTFieldPath *_Nullable)inequalityFilterField { + for (id<FSTFilter> filter in self.filters) { + if ([filter isKindOfClass:[FSTRelationFilter class]] && + ((FSTRelationFilter *)filter).filterOperator != FSTRelationFilterOperatorEqual) { + return filter.field; + } + } + return nil; +} + +- (FSTFieldPath *_Nullable)firstSortOrderField { + return self.explicitSortOrders.firstObject.field; +} + +#pragma mark - Private properties + +- (NSString *)canonicalID { + if (_canonicalID) { + return _canonicalID; + } + + NSMutableString *canonicalID = [[self.path canonicalString] mutableCopy]; + + // Add filters. + [canonicalID appendString:@"|f:"]; + for (id<FSTFilter> predicate in self.filters) { + [canonicalID appendFormat:@"%@", [predicate canonicalID]]; + } + + // Add order by. + [canonicalID appendString:@"|ob:"]; + for (FSTSortOrder *orderBy in self.sortOrders) { + [canonicalID appendString:orderBy.canonicalID]; + } + + // Add limit. + if (self.limit != NSNotFound) { + [canonicalID appendFormat:@"|l:%ld", (long)self.limit]; + } + + if (self.startAt) { + [canonicalID appendFormat:@"|lb:%@", self.startAt.canonicalString]; + } + + if (self.endAt) { + [canonicalID appendFormat:@"|ub:%@", self.endAt.canonicalString]; + } + + _canonicalID = canonicalID; + return canonicalID; +} + +#pragma mark - Private methods + +- (BOOL)isEqualToQuery:(FSTQuery *)other { + return [self.path isEqual:other.path] && self.limit == other.limit && + [self.filters isEqual:other.filters] && [self.sortOrders isEqual:other.sortOrders] && + (self.startAt == other.startAt || [self.startAt isEqual:other.startAt]) && + (self.endAt == other.endAt || [self.endAt isEqual:other.endAt]); +} + +/* Returns YES if the document matches the path for the receiver. */ +- (BOOL)pathMatchesDocument:(FSTDocument *)document { + FSTResourcePath *documentPath = document.key.path; + if ([FSTDocumentKey isDocumentKey:self.path]) { + // Exact match for document queries. + return [self.path isEqual:documentPath]; + } else { + // Shallow ancestor queries by default. + return [self.path isPrefixOfPath:documentPath] && self.path.length == documentPath.length - 1; + } +} + +/** + * A document must have a value for every ordering clause in order to show up in the results. + */ +- (BOOL)orderByMatchesDocument:(FSTDocument *)document { + for (FSTSortOrder *orderBy in self.explicitSortOrders) { + FSTFieldPath *fieldPath = orderBy.field; + // order by key always matches + if (![fieldPath isEqual:[FSTFieldPath keyFieldPath]] && + [document fieldForPath:fieldPath] == nil) { + return NO; + } + } + return YES; +} + +/** Returns YES if the document matches all of the filters in the receiver. */ +- (BOOL)filtersMatchDocument:(FSTDocument *)document { + for (id<FSTFilter> filter in self.filters) { + if (![filter matchesDocument:document]) { + return NO; + } + } + return YES; +} + +- (BOOL)boundsMatchDocument:(FSTDocument *)document { + if (self.startAt && ![self.startAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) { + return NO; + } + if (self.endAt && [self.endAt sortsBeforeDocument:document usingSortOrder:self.sortOrders]) { + return NO; + } + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSnapshotVersion.h b/Firestore/Source/Core/FSTSnapshotVersion.h new file mode 100644 index 0000000..b72e4a2 --- /dev/null +++ b/Firestore/Source/Core/FSTSnapshotVersion.h @@ -0,0 +1,43 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@class FSTTimestamp; + +/** + * A version of a document in Firestore. This corresponds to the version timestamp, such as + * update_time or read_time. + */ +@interface FSTSnapshotVersion : NSObject <NSCopying> + +/** Creates a new version that is smaller than all other versions. */ ++ (instancetype)noVersion; + +/** Creates a new version representing the given timestamp. */ ++ (instancetype)versionWithTimestamp:(FSTTimestamp *)timestamp; + +- (instancetype)init NS_UNAVAILABLE; + +- (NSComparisonResult)compare:(FSTSnapshotVersion *)other; + +@property(nonatomic, strong, readonly) FSTTimestamp *timestamp; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSnapshotVersion.m b/Firestore/Source/Core/FSTSnapshotVersion.m new file mode 100644 index 0000000..68d5d7f --- /dev/null +++ b/Firestore/Source/Core/FSTSnapshotVersion.m @@ -0,0 +1,80 @@ +/* + * 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 "FSTSnapshotVersion.h" + +#import "FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTSnapshotVersion + ++ (instancetype)noVersion { + static FSTSnapshotVersion *min; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + FSTTimestamp *timestamp = [[FSTTimestamp alloc] initWithSeconds:0 nanos:0]; + min = [FSTSnapshotVersion versionWithTimestamp:timestamp]; + }); + return min; +} + ++ (instancetype)versionWithTimestamp:(FSTTimestamp *)timestamp { + return [[FSTSnapshotVersion alloc] initWithTimestamp:timestamp]; +} + +- (instancetype)initWithTimestamp:(FSTTimestamp *)timestamp { + self = [super init]; + if (self) { + _timestamp = timestamp; + } + return self; +} + +#pragma mark - NSObject methods + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTSnapshotVersion class]]) { + return NO; + } + return [self.timestamp isEqual:((FSTSnapshotVersion *)object).timestamp]; +} + +- (NSUInteger)hash { + return self.timestamp.hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTSnapshotVersion: %@>", self.timestamp]; +} + +- (id)copyWithZone:(NSZone *_Nullable)zone { + // Implements NSCopying without actually copying because timestamps are immutable. + return self; +} + +#pragma mark - Public methods + +- (NSComparisonResult)compare:(FSTSnapshotVersion *)other { + return [self.timestamp compare:other.timestamp]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.h b/Firestore/Source/Core/FSTSyncEngine.h new file mode 100644 index 0000000..1348ce1 --- /dev/null +++ b/Firestore/Source/Core/FSTSyncEngine.h @@ -0,0 +1,105 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTRemoteStore.h" +#import "FSTTypes.h" + +@class FSTDispatchQueue; +@class FSTLocalStore; +@class FSTMutation; +@class FSTQuery; +@class FSTRemoteEvent; +@class FSTRemoteStore; +@class FSTUser; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTSyncEngineDelegate + +/** A Delegate to be notified when the sync engine produces new view snapshots or errors. */ +@protocol FSTSyncEngineDelegate +- (void)handleViewSnapshots:(NSArray<FSTViewSnapshot *> *)viewSnapshots; +- (void)handleError:(NSError *)error forQuery:(FSTQuery *)query; +@end + +/** + * SyncEngine is the central controller in the client SDK architecture. It is the glue code + * between the EventManager, LocalStore, and RemoteStore. Some of SyncEngine's responsibilities + * include: + * 1. Coordinating client requests and remote events between the EventManager and the local and + * remote data stores. + * 2. Managing a View object for each query, providing the unified view between the local and + * remote data stores. + * 3. Notifying the RemoteStore when the LocalStore has new mutations in its queue that need + * sending to the backend. + * + * The SyncEngine’s methods should only ever be called by methods running on our own worker + * dispatch queue. + */ +@interface FSTSyncEngine : NSObject <FSTRemoteSyncer> + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + remoteStore:(FSTRemoteStore *)remoteStore + initialUser:(FSTUser *)user NS_DESIGNATED_INITIALIZER; + +/** + * A delegate to be notified when queries being listened to produce new view snapshots or + * errors. + */ +@property(nonatomic, weak) id<FSTSyncEngineDelegate> delegate; + +/** + * Initiates a new listen. The FSTLocalStore will be queried for initial data and the listen will + * be sent to the FSTRemoteStore to get remote data. The registered FSTSyncEngineDelegate will be + * notified of resulting view snapshots and/or listen errors. + * + * @return the target ID assigned to the query. + */ +- (FSTTargetID)listenToQuery:(FSTQuery *)query; + +/** Stops listening to a query previously listened to via listenToQuery:. */ +- (void)stopListeningToQuery:(FSTQuery *)query; + +/** + * Initiates the write of local mutation batch which involves adding the writes to the mutation + * queue, notifying the remote store about new mutations, and raising events for any changes this + * write caused. The provided completion block will be called once the write has been acked or + * rejected by the backend (or failed locally for any other reason). + */ +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations completion:(FSTVoidErrorBlock)completion; + +/** + * Runs the given transaction block up to retries times and then calls completion. + * + * @param retries The number of times to try before giving up. + * @param workerDispatchQueue The queue to dispatch sync engine calls to. + * @param updateBlock The block to call to execute the user's transaction. + * @param completion The block to call when the transaction is finished or failed. + */ +- (void)transactionWithRetries:(int)retries + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion; + +- (void)userDidChange:(FSTUser *)user; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTSyncEngine.m b/Firestore/Source/Core/FSTSyncEngine.m new file mode 100644 index 0000000..8698a97 --- /dev/null +++ b/Firestore/Source/Core/FSTSyncEngine.m @@ -0,0 +1,520 @@ +/* + * 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 "FSTSyncEngine.h" + +#import <GRPCClient/GRPCCall.h> + +#import "FIRFirestoreErrors.h" +#import "FSTAssert.h" +#import "FSTDispatchQueue.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTEagerGarbageCollector.h" +#import "FSTLocalStore.h" +#import "FSTLocalViewChanges.h" +#import "FSTLocalWriteResult.h" +#import "FSTLogger.h" +#import "FSTMutationBatch.h" +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTReferenceSet.h" +#import "FSTRemoteEvent.h" +#import "FSTSnapshotVersion.h" +#import "FSTTargetIDGenerator.h" +#import "FSTTransaction.h" +#import "FSTUser.h" +#import "FSTView.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTQueryView + +/** + * FSTQueryView contains all of the info that FSTSyncEngine needs to track for a particular + * query and view. + */ +@interface FSTQueryView : NSObject + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + resumeToken:(NSData *)resumeToken + view:(FSTView *)view NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The query itself. */ +@property(nonatomic, strong, readonly) FSTQuery *query; + +/** The targetID created by the client that is used in the watch stream to identify this query. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +/** + * An identifier from the datastore backend that indicates the last state of the results that + * was received. This can be used to indicate where to continue receiving new doc changes for the + * query. + */ +@property(nonatomic, copy, readonly) NSData *resumeToken; + +/** + * The view is responsible for computing the final merged truth of what docs are in the query. + * It gets notified of local and remote changes, and applies the query filters and limits to + * determine the most correct possible results. + */ +@property(nonatomic, strong, readonly) FSTView *view; + +@end + +@implementation FSTQueryView + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + resumeToken:(NSData *)resumeToken + view:(FSTView *)view { + if (self = [super init]) { + _query = query; + _targetID = targetID; + _resumeToken = resumeToken; + _view = view; + } + return self; +} + +@end + +#pragma mark - FSTSyncEngine + +@interface FSTSyncEngine () + +/** The local store, used to persist mutations and cached documents. */ +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; + +/** The remote store for sending writes, watches, etc. to the backend. */ +@property(nonatomic, strong, readonly) FSTRemoteStore *remoteStore; + +/** FSTQueryViews for all active queries, indexed by query. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTQuery *, FSTQueryView *> *queryViewsByQuery; + +/** FSTQueryViews for all active queries, indexed by target ID. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<NSNumber *, FSTQueryView *> *queryViewsByTarget; + +/** + * When a document is in limbo, we create a special listen to resolve it. This maps the + * FSTDocumentKey of each limbo document to the FSTTargetID of the listen resolving it. + */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *limboTargetsByKey; + +/** The inverse of limboTargetsByKey, a map of FSTTargetID to the key of the limbo doc. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTBoxedTargetID *, FSTDocumentKey *> *limboKeysByTarget; + +/** Used to track any documents that are currently in limbo. */ +@property(nonatomic, strong, readonly) FSTReferenceSet *limboDocumentRefs; + +/** The garbage collector used to collect documents that are no longer in limbo. */ +@property(nonatomic, strong, readonly) FSTEagerGarbageCollector *limboCollector; + +/** Stores user completion blocks, indexed by user and FSTBatchID. */ +@property(nonatomic, strong) + NSMutableDictionary<FSTUser *, NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *> + *mutationCompletionBlocks; + +/** Used for creating the FSTTargetIDs for the listens used to resolve limbo documents. */ +@property(nonatomic, strong, readonly) FSTTargetIDGenerator *targetIdGenerator; + +@property(nonatomic, strong) FSTUser *currentUser; + +@end + +@implementation FSTSyncEngine + +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + remoteStore:(FSTRemoteStore *)remoteStore + initialUser:(FSTUser *)initialUser { + if (self = [super init]) { + _localStore = localStore; + _remoteStore = remoteStore; + + _queryViewsByQuery = [NSMutableDictionary dictionary]; + _queryViewsByTarget = [NSMutableDictionary dictionary]; + + _limboTargetsByKey = [NSMutableDictionary dictionary]; + _limboKeysByTarget = [NSMutableDictionary dictionary]; + _limboCollector = [[FSTEagerGarbageCollector alloc] init]; + _limboDocumentRefs = [[FSTReferenceSet alloc] init]; + [_limboCollector addGarbageSource:_limboDocumentRefs]; + + _mutationCompletionBlocks = [NSMutableDictionary dictionary]; + _targetIdGenerator = [FSTTargetIDGenerator generatorForSyncEngineStartingAfterID:0]; + _currentUser = initialUser; + } + return self; +} + +- (FSTTargetID)listenToQuery:(FSTQuery *)query { + [self assertDelegateExistsForSelector:_cmd]; + FSTAssert(self.queryViewsByQuery[query] == nil, @"We already listen to query: %@", query); + + FSTQueryData *queryData = [self.localStore allocateQuery:query]; + FSTDocumentDictionary *docs = [self.localStore executeQuery:query]; + FSTDocumentKeySet *remoteKeys = [self.localStore remoteDocumentKeysForTarget:queryData.targetID]; + + FSTView *view = [[FSTView alloc] initWithQuery:query remoteDocuments:remoteKeys]; + FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:docs]; + FSTViewChange *viewChange = [view applyChangesToDocuments:viewDocChanges]; + FSTAssert(viewChange.limboChanges.count == 0, + @"View returned limbo docs before target ack from the server."); + + FSTQueryView *queryView = [[FSTQueryView alloc] initWithQuery:query + targetID:queryData.targetID + resumeToken:queryData.resumeToken + view:view]; + self.queryViewsByQuery[query] = queryView; + self.queryViewsByTarget[@(queryData.targetID)] = queryView; + [self.delegate handleViewSnapshots:@[ viewChange.snapshot ]]; + + [self.remoteStore listenToTargetWithQueryData:queryData]; + return queryData.targetID; +} + +- (void)stopListeningToQuery:(FSTQuery *)query { + [self assertDelegateExistsForSelector:_cmd]; + + FSTQueryView *queryView = self.queryViewsByQuery[query]; + FSTAssert(queryView, @"Trying to stop listening to a query not found"); + + [self.localStore releaseQuery:query]; + [self.remoteStore stopListeningToTargetID:queryView.targetID]; + [self removeAndCleanupQuery:queryView]; + [self.localStore collectGarbage]; +} + +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations + completion:(FSTVoidErrorBlock)completion { + [self assertDelegateExistsForSelector:_cmd]; + + FSTLocalWriteResult *result = [self.localStore locallyWriteMutations:mutations]; + [self addMutationCompletionBlock:completion batchID:result.batchID]; + + [self emitNewSnapshotsWithChanges:result.changes remoteEvent:nil]; + [self.remoteStore fillWritePipeline]; +} + +- (void)addMutationCompletionBlock:(FSTVoidErrorBlock)completion batchID:(FSTBatchID)batchID { + NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *completionBlocks = + self.mutationCompletionBlocks[self.currentUser]; + if (!completionBlocks) { + completionBlocks = [NSMutableDictionary dictionary]; + self.mutationCompletionBlocks[self.currentUser] = completionBlocks; + } + [completionBlocks setObject:completion forKey:@(batchID)]; +} + +/** + * Takes an updateBlock in which a set of reads and writes can be performed atomically. In the + * updateBlock, user code can read and write values using a transaction object. After the + * updateBlock, all changes will be committed. If someone else has changed any of the data + * referenced, then the updateBlock will be called again. If the updateBlock still fails after the + * given number of retries, then the transaction will be rejected. + * + * The transaction object passed to the updateBlock contains methods for accessing documents + * and collections. Unlike other firestore access, data accessed with the transaction will not + * reflect local changes that have not been committed. For this reason, it is required that all + * reads are performed before any writes. Transactions must be performed while online. + */ +- (void)transactionWithRetries:(int)retries + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + updateBlock:(FSTTransactionBlock)updateBlock + completion:(FSTVoidIDErrorBlock)completion { + [workerDispatchQueue verifyIsCurrentQueue]; + FSTAssert(retries >= 0, @"Got negative number of retries for transaction"); + FSTTransaction *transaction = [self.remoteStore transaction]; + updateBlock(transaction, ^(id _Nullable result, NSError *_Nullable error) { + [workerDispatchQueue dispatchAsync:^{ + if (error) { + completion(nil, error); + return; + } + [transaction commitWithCompletion:^(NSError *_Nullable transactionError) { + if (!transactionError) { + completion(result, nil); + return; + } + // TODO(b/35201829): Only retry on real transaction failures. + if (retries == 0) { + NSError *wrappedError = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeFailedPrecondition + userInfo:@{ + NSLocalizedDescriptionKey : @"Transaction failed all retries.", + NSUnderlyingErrorKey : transactionError + }]; + completion(nil, wrappedError); + return; + } + [workerDispatchQueue verifyIsCurrentQueue]; + return [self transactionWithRetries:(retries - 1) + workerDispatchQueue:workerDispatchQueue + updateBlock:updateBlock + completion:completion]; + }]; + }]; + }); +} + +- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { + [self assertDelegateExistsForSelector:_cmd]; + + // Make sure limbo documents are deleted if there were no results + [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( + FSTBoxedTargetID *_Nonnull targetID, + FSTTargetChange *_Nonnull targetChange, BOOL *_Nonnull stop) { + FSTDocumentKey *limboKey = self.limboKeysByTarget[targetID]; + if (limboKey && targetChange.currentStatusUpdate == FSTCurrentStatusUpdateMarkCurrent && + remoteEvent.documentUpdates[limboKey] == nil) { + // When listening to a query the server responds with a snapshot containing documents + // matching the query and a current marker telling us we're now in sync. It's possible for + // these to arrive as separate remote events or as a single remote event. For a document + // query, there will be no documents sent in the response if the document doesn't exist. + // + // If the snapshot arrives separately from the current marker, we handle it normally and + // updateTrackedLimboDocumentsWithChanges:targetID: will resolve the limbo status of the + // document, removing it from limboDocumentRefs. This works because clients only initiate + // limbo resolution when a target is current and because all current targets are always at a + // consistent snapshot. + // + // However, if the document doesn't exist and the current marker arrives, the document is + // not present in the snapshot and our normal view handling would consider the document to + // remain in limbo indefinitely because there are no updates to the document. To avoid this, + // we specially handle this just this case here: synthesizing a delete. + // + // TODO(dimond): Ideally we would have an explicit lookup query instead resulting in an + // explicit delete message and we could remove this special logic. + [remoteEvent + addDocumentUpdate:[FSTDeletedDocument documentWithKey:limboKey + version:remoteEvent.snapshotVersion]]; + } + }]; + + FSTMaybeDocumentDictionary *changes = [self.localStore applyRemoteEvent:remoteEvent]; + [self emitNewSnapshotsWithChanges:changes remoteEvent:remoteEvent]; +} + +- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error { + [self assertDelegateExistsForSelector:_cmd]; + + FSTDocumentKey *limboKey = self.limboKeysByTarget[targetID]; + if (limboKey) { + // Since this query failed, we won't want to manually unlisten to it. + // So go ahead and remove it from bookkeeping. + [self.limboTargetsByKey removeObjectForKey:limboKey]; + [self.limboKeysByTarget removeObjectForKey:targetID]; + + // TODO(dimond): Retry on transient errors? + + // It's a limbo doc. Create a synthetic event saying it was deleted. This is kind of a hack. + // Ideally, we would have a method in the local store to purge a document. However, it would + // be tricky to keep all of the local store's invariants with another method. + NSMutableDictionary<NSNumber *, FSTTargetChange *> *targetChanges = + [NSMutableDictionary dictionary]; + FSTDeletedDocument *doc = + [FSTDeletedDocument documentWithKey:limboKey version:[FSTSnapshotVersion noVersion]]; + NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *docUpdate = + [NSMutableDictionary dictionaryWithObject:doc forKey:limboKey]; + FSTRemoteEvent *event = [FSTRemoteEvent eventWithSnapshotVersion:[FSTSnapshotVersion noVersion] + targetChanges:targetChanges + documentUpdates:docUpdate]; + [self applyRemoteEvent:event]; + } else { + FSTQueryView *queryView = self.queryViewsByTarget[targetID]; + FSTAssert(queryView, @"Unknown targetId: %@", targetID); + [self.localStore releaseQuery:queryView.query]; + [self removeAndCleanupQuery:queryView]; + [self.delegate handleError:error forQuery:queryView.query]; + } +} + +- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult { + [self assertDelegateExistsForSelector:_cmd]; + + // The local store may or may not be able to apply the write result and raise events immediately + // (depending on whether the watcher is caught up), so we raise user callbacks first so that they + // consistently happen before listen events. + [self processUserCallbacksForBatchID:batchResult.batch.batchID error:nil]; + + FSTMaybeDocumentDictionary *changes = [self.localStore acknowledgeBatchWithResult:batchResult]; + [self emitNewSnapshotsWithChanges:changes remoteEvent:nil]; +} + +- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error { + [self assertDelegateExistsForSelector:_cmd]; + + // The local store may or may not be able to apply the write result and raise events immediately + // (depending on whether the watcher is caught up), so we raise user callbacks first so that they + // consistently happen before listen events. + [self processUserCallbacksForBatchID:batchID error:error]; + + FSTMaybeDocumentDictionary *changes = [self.localStore rejectBatchID:batchID]; + [self emitNewSnapshotsWithChanges:changes remoteEvent:nil]; +} + +- (void)processUserCallbacksForBatchID:(FSTBatchID)batchID error:(NSError *_Nullable)error { + NSMutableDictionary<NSNumber *, FSTVoidErrorBlock> *completionBlocks = + self.mutationCompletionBlocks[self.currentUser]; + + // NOTE: Mutations restored from persistence won't have completion blocks, so it's okay for + // this (or the completion below) to be nil. + if (completionBlocks) { + NSNumber *boxedBatchID = @(batchID); + FSTVoidErrorBlock completion = completionBlocks[boxedBatchID]; + if (completion) { + completion(error); + [completionBlocks removeObjectForKey:boxedBatchID]; + } + } +} + +- (void)assertDelegateExistsForSelector:(SEL)methodSelector { + FSTAssert(self.delegate, @"Tried to call '%@' before delegate was registered.", + NSStringFromSelector(methodSelector)); +} + +- (void)removeAndCleanupQuery:(FSTQueryView *)queryView { + [self.queryViewsByQuery removeObjectForKey:queryView.query]; + [self.queryViewsByTarget removeObjectForKey:@(queryView.targetID)]; + + [self.limboDocumentRefs removeReferencesForID:queryView.targetID]; + [self garbageCollectLimboDocuments]; +} + +/** + * Computes a new snapshot from the changes and calls the registered callback with the new snapshot. + */ +- (void)emitNewSnapshotsWithChanges:(FSTMaybeDocumentDictionary *)changes + remoteEvent:(FSTRemoteEvent *_Nullable)remoteEvent { + NSMutableArray<FSTViewSnapshot *> *newSnapshots = [NSMutableArray array]; + NSMutableArray<FSTLocalViewChanges *> *documentChangesInAllViews = [NSMutableArray array]; + + [self.queryViewsByQuery + enumerateKeysAndObjectsUsingBlock:^(FSTQuery *query, FSTQueryView *queryView, BOOL *stop) { + FSTView *view = queryView.view; + FSTViewDocumentChanges *viewDocChanges = [view computeChangesWithDocuments:changes]; + if (viewDocChanges.needsRefill) { + // The query has a limit and some docs were removed/updated, so we need to re-run the + // query against the local store to make sure we didn't lose any good docs that had been + // past the limit. + FSTDocumentDictionary *docs = [self.localStore executeQuery:queryView.query]; + viewDocChanges = [view computeChangesWithDocuments:docs previousChanges:viewDocChanges]; + } + FSTTargetChange *_Nullable targetChange = remoteEvent.targetChanges[@(queryView.targetID)]; + FSTViewChange *viewChange = + [queryView.view applyChangesToDocuments:viewDocChanges targetChange:targetChange]; + + [self updateTrackedLimboDocumentsWithChanges:viewChange.limboChanges + targetID:queryView.targetID]; + + if (viewChange.snapshot) { + [newSnapshots addObject:viewChange.snapshot]; + FSTLocalViewChanges *docChanges = + [FSTLocalViewChanges changesForViewSnapshot:viewChange.snapshot]; + [documentChangesInAllViews addObject:docChanges]; + } + }]; + + [self.delegate handleViewSnapshots:newSnapshots]; + [self.localStore notifyLocalViewChanges:documentChangesInAllViews]; + [self.localStore collectGarbage]; +} + +/** Updates the limbo document state for the given targetID. */ +- (void)updateTrackedLimboDocumentsWithChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges + targetID:(FSTTargetID)targetID { + for (FSTLimboDocumentChange *limboChange in limboChanges) { + switch (limboChange.type) { + case FSTLimboDocumentChangeTypeAdded: + [self.limboDocumentRefs addReferenceToKey:limboChange.key forID:targetID]; + [self trackLimboChange:limboChange]; + break; + + case FSTLimboDocumentChangeTypeRemoved: + FSTLog(@"Document no longer in limbo: %@", limboChange.key); + [self.limboDocumentRefs removeReferenceToKey:limboChange.key forID:targetID]; + break; + + default: + FSTFail(@"Unknown limbo change type: %ld", (long)limboChange.type); + } + } + [self garbageCollectLimboDocuments]; +} + +- (void)trackLimboChange:(FSTLimboDocumentChange *)limboChange { + FSTDocumentKey *key = limboChange.key; + + if (!self.limboTargetsByKey[key]) { + FSTLog(@"New document in limbo: %@", key); + FSTTargetID limboTargetID = [self.targetIdGenerator nextID]; + FSTQuery *query = [FSTQuery queryWithPath:key.path]; + FSTQueryData *queryData = [[FSTQueryData alloc] initWithQuery:query + targetID:limboTargetID + purpose:FSTQueryPurposeLimboResolution]; + self.limboKeysByTarget[@(limboTargetID)] = key; + [self.remoteStore listenToTargetWithQueryData:queryData]; + self.limboTargetsByKey[key] = @(limboTargetID); + } +} + +/** Garbage collect the limbo documents that we no longer need to track. */ +- (void)garbageCollectLimboDocuments { + NSSet<FSTDocumentKey *> *garbage = [self.limboCollector collectGarbage]; + for (FSTDocumentKey *key in garbage) { + FSTBoxedTargetID *limboTarget = self.limboTargetsByKey[key]; + if (!limboTarget) { + // This target already got removed, because the query failed. + return; + } + FSTTargetID limboTargetID = limboTarget.intValue; + [self.remoteStore stopListeningToTargetID:limboTargetID]; + [self.limboTargetsByKey removeObjectForKey:key]; + [self.limboKeysByTarget removeObjectForKey:limboTarget]; + } +} + +// Used for testing +- (NSDictionary<FSTDocumentKey *, FSTBoxedTargetID *> *)currentLimboDocuments { + // Return defensive copy + return [self.limboTargetsByKey copy]; +} + +- (void)userDidChange:(FSTUser *)user { + self.currentUser = user; + + // Notify local store and emit any resulting events from swapping out the mutation queue. + FSTMaybeDocumentDictionary *changes = [self.localStore userDidChange:user]; + [self emitNewSnapshotsWithChanges:changes remoteEvent:nil]; + + // Notify remote store so it can restart its streams. + [self.remoteStore userDidChange:user]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTargetIDGenerator.h b/Firestore/Source/Core/FSTTargetIDGenerator.h new file mode 100644 index 0000000..5b9db10 --- /dev/null +++ b/Firestore/Source/Core/FSTTargetIDGenerator.h @@ -0,0 +1,55 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTTargetIDGenerator generates monotonically increasing integer IDs. There are separate + * generators for different scopes. While these generators will operate independently of each + * other, they are scoped, such that no two generators will ever produce the same ID. This is + * useful, because sometimes the backend may group IDs from separate parts of the client into the + * same ID space. + */ +@interface FSTTargetIDGenerator : NSObject + +/** + * Creates and returns the FSTTargetIDGenerator for the local store. + * + * @param after An ID to start at. Every call to nextID will return an ID > @a after. + * @return A shared instance of FSTTargetIDGenerator. + */ ++ (instancetype)generatorForLocalStoreStartingAfterID:(FSTTargetID)after; + +/** + * Creates and returns the FSTTargetIDGenerator for the sync engine. + * + * @param after An ID to start at. Every call to nextID will return an ID > @a after. + * @return A shared instance of FSTTargetIDGenerator. + */ ++ (instancetype)generatorForSyncEngineStartingAfterID:(FSTTargetID)after; + +- (id)init __attribute__((unavailable("Use a static constructor method."))); + +/** Returns the next ID in the sequence. */ +- (FSTTargetID)nextID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTargetIDGenerator.m b/Firestore/Source/Core/FSTTargetIDGenerator.m new file mode 100644 index 0000000..86ded30 --- /dev/null +++ b/Firestore/Source/Core/FSTTargetIDGenerator.m @@ -0,0 +1,105 @@ +/* + * 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 "FSTTargetIDGenerator.h" + +#import <libkern/OSAtomic.h> + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTargetIDGenerator + +static const int kReservedBits = 1; + +/** FSTTargetIDGeneratorID is the set of all valid generators. */ +typedef NS_ENUM(NSInteger, FSTTargetIDGeneratorID) { + FSTTargetIDGeneratorIDLocalStore = 0, + FSTTargetIDGeneratorIDSyncEngine = 1 +}; + +@interface FSTTargetIDGenerator () { + // This is volatile so it can be used with OSAtomicAdd32. + volatile FSTTargetID _previousID; +} + +/** + * Initializes the generator. + * + * @param generatorID A unique ID indicating which generator this is. + * @param after Every call to nextID will return a number > @a after. + */ +- (instancetype)initWithGeneratorID:(FSTTargetIDGeneratorID)generatorID + startingAfterID:(FSTTargetID)after NS_DESIGNATED_INITIALIZER; + +// This is typed as FSTTargetID because we need to do bitwise operations with them together. +@property(nonatomic, assign) FSTTargetID generatorID; +@end + +@implementation FSTTargetIDGenerator + +#pragma mark - Constructors + +- (instancetype)initWithGeneratorID:(FSTTargetIDGeneratorID)generatorID + startingAfterID:(FSTTargetID)after { + self = [super init]; + if (self) { + _generatorID = generatorID; + + // Replace the generator part of |after| with this generator's ID. + FSTTargetID afterWithoutGenerator = (after >> kReservedBits) << kReservedBits; + FSTTargetID afterGenerator = after - afterWithoutGenerator; + if (afterGenerator >= _generatorID) { + // For example, if: + // self.generatorID = 0b0000 + // after = 0b1011 + // afterGenerator = 0b0001 + // Then: + // previous = 0b1010 + // next = 0b1100 + _previousID = afterWithoutGenerator | self.generatorID; + } else { + // For example, if: + // self.generatorID = 0b0001 + // after = 0b1010 + // afterGenerator = 0b0000 + // Then: + // previous = 0b1001 + // next = 0b1011 + _previousID = (afterWithoutGenerator | self.generatorID) - (1 << kReservedBits); + } + } + return self; +} + ++ (instancetype)generatorForLocalStoreStartingAfterID:(FSTTargetID)after { + return [[FSTTargetIDGenerator alloc] initWithGeneratorID:FSTTargetIDGeneratorIDLocalStore + startingAfterID:after]; +} + ++ (instancetype)generatorForSyncEngineStartingAfterID:(FSTTargetID)after { + return [[FSTTargetIDGenerator alloc] initWithGeneratorID:FSTTargetIDGeneratorIDSyncEngine + startingAfterID:after]; +} + +#pragma mark - Public methods + +- (FSTTargetID)nextID { + return OSAtomicAdd32(1 << kReservedBits, &_previousID); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTimestamp.h b/Firestore/Source/Core/FSTTimestamp.h new file mode 100644 index 0000000..f86779d --- /dev/null +++ b/Firestore/Source/Core/FSTTimestamp.h @@ -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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** + * An FSTTimestamp represents an absolute time from the backend at up to nanosecond precision. + * An FSTTimestamp is represented in terms of UTC and does not have an associated timezone. + */ +@interface FSTTimestamp : NSObject <NSCopying> + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a new timestamp. + * + * @param seconds the number of seconds since epoch. + * @param nanos the number of nanoseconds after the seconds. + */ +- (instancetype)initWithSeconds:(int64_t)seconds nanos:(int32_t)nanos NS_DESIGNATED_INITIALIZER; + +/** Creates a new timestamp with the current date / time. */ ++ (instancetype)timestamp; + +/** Creates a new timestamp from the given date. */ ++ (instancetype)timestampWithDate:(NSDate *)date; + +/** Returns a new NSDate corresponding to this timestamp. This may lose precision. */ +- (NSDate *)approximateDateValue; + +/** + * Converts the given date to a an ISO 8601 timestamp string, useful for rendering in JSON. + * + * ISO 8601 dates times in UTC look like this: "1912-04-14T23:40:00.000000000Z". + * + * @see http://www.ecma-international.org/ecma-262/6.0/#sec-date-time-string-format + */ +- (NSString *)ISO8601String; + +- (NSComparisonResult)compare:(FSTTimestamp *)other; + +/** + * Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. + * Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive. + */ +@property(nonatomic, assign, readonly) int64_t seconds; + +/** + * Non-negative fractions of a second at nanosecond resolution. Negative second values with + * fractions must still have non-negative nanos values that count forward in time. + * Must be from 0 to 999,999,999 inclusive. + */ +@property(nonatomic, assign, readonly) int32_t nanos; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTimestamp.m b/Firestore/Source/Core/FSTTimestamp.m new file mode 100644 index 0000000..941217a --- /dev/null +++ b/Firestore/Source/Core/FSTTimestamp.m @@ -0,0 +1,122 @@ +/* + * 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 "FSTTimestamp.h" + +#import "FSTAssert.h" +#import "FSTComparison.h" + +NS_ASSUME_NONNULL_BEGIN + +static const int kNanosPerSecond = 1000000000; + +@implementation FSTTimestamp + +#pragma mark - Constructors + ++ (instancetype)timestamp { + return [FSTTimestamp timestampWithDate:[NSDate date]]; +} + ++ (instancetype)timestampWithDate:(NSDate *)date { + double secondsDouble; + double fraction = modf(date.timeIntervalSince1970, &secondsDouble); + // GCP Timestamps always have non-negative nanos. + if (fraction < 0) { + fraction += 1.0; + secondsDouble -= 1.0; + } + int64_t seconds = (int64_t)secondsDouble; + int32_t nanos = (int32_t)(fraction * kNanosPerSecond); + return [[FSTTimestamp alloc] initWithSeconds:seconds nanos:nanos]; +} + +- (instancetype)initWithSeconds:(int64_t)seconds nanos:(int32_t)nanos { + self = [super init]; + if (self) { + FSTAssert(nanos >= 0, @"timestamp nanoseconds out of range: %d", nanos); + FSTAssert(nanos < 1e9, @"timestamp nanoseconds out of range: %d", nanos); + // Midnight at the beginning of 1/1/1 is the earliest timestamp Firestore supports. + FSTAssert(seconds >= -62135596800L, @"timestamp seconds out of range: %lld", seconds); + // This will break in the year 10,000. + FSTAssert(seconds < 253402300800L, @"timestamp seconds out of range: %lld", seconds); + + _seconds = seconds; + _nanos = nanos; + } + return self; +} + +#pragma mark - NSObject methods + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTTimestamp class]]) { + return NO; + } + return [self isEqualToTimestamp:(FSTTimestamp *)object]; +} + +- (NSUInteger)hash { + return (NSUInteger)((self.seconds >> 32) ^ self.seconds ^ self.nanos); +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"<FSTTimestamp: seconds=%lld nanos=%d>", self.seconds, self.nanos]; +} + +/** Implements NSCopying without actually copying because timestamps are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +#pragma mark - Public methods + +- (NSDate *)approximateDateValue { + NSTimeInterval interval = (NSTimeInterval)self.seconds + ((NSTimeInterval)self.nanos) / 1e9; + return [NSDate dateWithTimeIntervalSince1970:interval]; +} + +- (BOOL)isEqualToTimestamp:(FSTTimestamp *)other { + return [self compare:other] == NSOrderedSame; +} + +- (NSString *)ISO8601String { + NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss"; + formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"]; + NSDate *secondsDate = [NSDate dateWithTimeIntervalSince1970:self.seconds]; + NSString *secondsString = [formatter stringFromDate:secondsDate]; + FSTAssert(secondsString.length == 19, @"Invalid ISO string: %@", secondsString); + + NSString *nanosString = [NSString stringWithFormat:@"%09d", self.nanos]; + return [NSString stringWithFormat:@"%@.%@Z", secondsString, nanosString]; +} + +- (NSComparisonResult)compare:(FSTTimestamp *)other { + NSComparisonResult result = FSTCompareInt64s(self.seconds, other.seconds); + if (result != NSOrderedSame) { + return result; + } + return FSTCompareInt32s(self.nanos, other.nanos); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTransaction.h b/Firestore/Source/Core/FSTTransaction.h new file mode 100644 index 0000000..7fa3a10 --- /dev/null +++ b/Firestore/Source/Core/FSTTransaction.h @@ -0,0 +1,73 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTTypes.h" + +@class FIRSetOptions; +@class FSTDatastore; +@class FSTDocumentKey; +@class FSTFieldMask; +@class FSTFieldTransform; +@class FSTMaybeDocument; +@class FSTObjectValue; +@class FSTParsedSetData; +@class FSTParsedUpdateData; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTransaction + +/** Provides APIs to use in a transaction context. */ +@interface FSTTransaction : NSObject + +/** Creates a new transaction object, which can only be used for one transaction attempt. **/ ++ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore; + +/** + * Takes a set of keys and asynchronously attempts to fetch all the documents from the backend, + * ignoring any local changes. + */ +- (void)lookupDocumentsForKeys:(NSArray<FSTDocumentKey *> *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion; + +/** + * Stores mutation for the given key and set data, to be committed when commitWithCompletion is + * called. + */ +- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key; + +/** + * Stores mutations for the given key and update data, to be committed when commitWithCompletion + * is called. + */ +- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key; + +/** + * Stores a delete mutation for the given key, to be committed when commitWithCompletion is called. + */ +- (void)deleteDocument:(FSTDocumentKey *)key; + +/** + * Attempts to commit the mutations set on this transaction. Calls the given completion block when + * finished. Once this is called, no other mutations or commits are allowed on the transaction. + */ +- (void)commitWithCompletion:(FSTVoidErrorBlock)completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTransaction.m b/Firestore/Source/Core/FSTTransaction.m new file mode 100644 index 0000000..26c69e0 --- /dev/null +++ b/Firestore/Source/Core/FSTTransaction.m @@ -0,0 +1,250 @@ +/* + * 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 "FSTTransaction.h" + +#import <GRPCClient/GRPCCall.h> + +#import "FIRFirestoreErrors.h" +#import "FIRSetOptions.h" +#import "FSTAssert.h" +#import "FSTDatastore.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentKeySet.h" +#import "FSTMutation.h" +#import "FSTSnapshotVersion.h" +#import "FSTUsageValidation.h" +#import "FSTUserDataConverter.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTransaction + +@interface FSTTransaction () +@property(nonatomic, strong, readonly) FSTDatastore *datastore; +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTDocumentKey *, FSTSnapshotVersion *> *readVersions; +@property(nonatomic, strong, readonly) NSMutableArray *mutations; +@property(nonatomic, assign) BOOL commitCalled; +/** + * An error that may have occurred as a consequence of a write. If set, needs to be raised in the + * completion handler instead of trying to commit. + */ +@property(nonatomic, strong, nullable) NSError *lastWriteError; +@end + +@implementation FSTTransaction + ++ (instancetype)transactionWithDatastore:(FSTDatastore *)datastore { + return [[FSTTransaction alloc] initWithDatastore:datastore]; +} + +- (instancetype)initWithDatastore:(FSTDatastore *)datastore { + self = [super init]; + if (self) { + _datastore = datastore; + _readVersions = [NSMutableDictionary dictionary]; + _mutations = [NSMutableArray array]; + _commitCalled = NO; + } + return self; +} + +/** + * Every time a document is read, this should be called to record its version. If we read two + * different versions of the same document, this will return an error through its out parameter. + * When the transaction is committed, the versions recorded will be set as preconditions on the + * writes sent to the backend. + */ +- (BOOL)recordVersionForDocument:(FSTMaybeDocument *)doc error:(NSError **)error { + FSTAssert(error != nil, @"nil error parameter"); + *error = nil; + FSTSnapshotVersion *docVersion = doc.version; + if ([doc isKindOfClass:[FSTDeletedDocument class]]) { + // For deleted docs, we must record an explicit no version to build the right precondition + // when writing. + docVersion = [FSTSnapshotVersion noVersion]; + } + FSTSnapshotVersion *existingVersion = self.readVersions[doc.key]; + if (existingVersion) { + if (error) { + *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeFailedPrecondition + userInfo:@{ + NSLocalizedDescriptionKey : + @"A document cannot be read twice within a single transaction." + }]; + } + return NO; + } else { + self.readVersions[doc.key] = docVersion; + return YES; + } +} + +- (void)lookupDocumentsForKeys:(NSArray<FSTDocumentKey *> *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { + [self ensureCommitNotCalled]; + if (self.mutations.count) { + FSTThrowInvalidUsage(@"FIRIllegalStateException", + @"All reads in a transaction must be done before any writes."); + } + [self.datastore + lookupDocuments:keys + completion:^(NSArray<FSTDocument *> *_Nullable documents, NSError *_Nullable error) { + if (error) { + completion(nil, error); + return; + } + for (FSTMaybeDocument *doc in documents) { + NSError *recordError = nil; + if (![self recordVersionForDocument:doc error:&recordError]) { + completion(nil, recordError); + return; + } + } + completion(documents, nil); + }]; +} + +/** Stores mutations to be written when commitWithCompletion is called. */ +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations { + [self ensureCommitNotCalled]; + [self.mutations addObjectsFromArray:mutations]; +} + +/** + * Returns version of this doc when it was read in this transaction as a precondition, or no + * precondition if it was not read. + */ +- (FSTPrecondition *)preconditionForDocumentKey:(FSTDocumentKey *)key { + FSTSnapshotVersion *_Nullable snapshotVersion = self.readVersions[key]; + if (snapshotVersion) { + return [FSTPrecondition preconditionWithUpdateTime:snapshotVersion]; + } else { + return [FSTPrecondition none]; + } +} + +/** + * Returns the precondition for a document if the operation is an update, based on the provided + * UpdateOptions. Will return nil if an error occurred, in which case it sets the error parameter. + */ +- (nullable FSTPrecondition *)preconditionForUpdateWithDocumentKey:(FSTDocumentKey *)key + error:(NSError **)error { + FSTSnapshotVersion *_Nullable version = self.readVersions[key]; + if (version && [version isEqual:[FSTSnapshotVersion noVersion]]) { + // The document was read, but doesn't exist. + // Return an error because the precondition is impossible + if (error) { + *error = [NSError + errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeAborted + userInfo:@{ + NSLocalizedDescriptionKey : @"Can't update a document that doesn't exist." + }]; + } + return nil; + } else if (version) { + // Document exists, just base precondition on document update time. + return [FSTPrecondition preconditionWithUpdateTime:version]; + } else { + // Document was not read, so we just use the preconditions for an update. + return [FSTPrecondition preconditionWithExists:YES]; + } +} + +- (void)setData:(FSTParsedSetData *)data forDocument:(FSTDocumentKey *)key { + [self writeMutations:[data mutationsWithKey:key + precondition:[self preconditionForDocumentKey:key]]]; +} + +- (void)updateData:(FSTParsedUpdateData *)data forDocument:(FSTDocumentKey *)key { + NSError *error = nil; + FSTPrecondition *_Nullable precondition = + [self preconditionForUpdateWithDocumentKey:key error:&error]; + if (precondition) { + [self writeMutations:[data mutationsWithKey:key precondition:precondition]]; + } else { + FSTAssert(error, @"Got nil precondition, but error was not set"); + self.lastWriteError = error; + } +} + +- (void)deleteDocument:(FSTDocumentKey *)key { + [self writeMutations:@[ [[FSTDeleteMutation alloc] + initWithKey:key + precondition:[self preconditionForDocumentKey:key]] ]]; + // Since the delete will be applied before all following writes, we need to ensure that the + // precondition for the next write will be exists: false. + self.readVersions[key] = [FSTSnapshotVersion noVersion]; +} + +- (void)commitWithCompletion:(FSTVoidErrorBlock)completion { + [self ensureCommitNotCalled]; + // Once commitWithCompletion is called once, mark this object so it can't be used again. + self.commitCalled = YES; + + // If there was an error writing, raise that error now + if (self.lastWriteError) { + completion(self.lastWriteError); + return; + } + + // Make a list of read documents that haven't been written. + __block FSTDocumentKeySet *unwritten = [FSTDocumentKeySet keySet]; + [self.readVersions enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTSnapshotVersion *version, BOOL *stop) { + unwritten = [unwritten setByAddingObject:key]; + }]; + // For each mutation, note that the doc was written. + for (FSTMutation *mutation in self.mutations) { + unwritten = [unwritten setByRemovingObject:mutation.key]; + } + if (unwritten.count) { + // TODO(klimt): This is a temporary restriction, until "verify" is supported on the backend. + completion([NSError + errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeFailedPrecondition + userInfo:@{ + NSLocalizedDescriptionKey : @"Every document read in a transaction must also be " + @"written in that transaction." + }]); + } else { + [self.datastore commitMutations:self.mutations + completion:^(NSError *_Nullable error) { + if (error) { + completion(error); + } else { + completion(nil); + } + }]; + } +} + +- (void)ensureCommitNotCalled { + if (self.commitCalled) { + FSTThrowInvalidUsage( + @"FIRIllegalStateException", + @"A transaction object cannot be used after its update block has completed."); + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTTypes.h b/Firestore/Source/Core/FSTTypes.h new file mode 100644 index 0000000..8f1183c --- /dev/null +++ b/Firestore/Source/Core/FSTTypes.h @@ -0,0 +1,90 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@class FSTMaybeDocument; +@class FSTTransaction; + +/** FSTBatchID is a locally assigned ID for a batch of mutations that have been applied. */ +typedef int32_t FSTBatchID; + +typedef int32_t FSTTargetID; + +typedef NSNumber FSTBoxedTargetID; + +/** + * FSTVoidBlock is a block that's called when a specific event happens but that otherwise has + * no information associated with it. + */ +typedef void (^FSTVoidBlock)(); + +/** + * FSTVoidErrorBlock is a block that gets an error, if one occurred. + * + * @param error The error if it occurred, or nil. + */ +typedef void (^FSTVoidErrorBlock)(NSError *_Nullable error); + +/** FSTVoidIDErrorBlock is a block that takes an optional value and error. */ +typedef void (^FSTVoidIDErrorBlock)(id _Nullable, NSError *_Nullable); + +/** + * FSTVoidMaybeDocumentErrorBlock is a block that gets either a list of documents or an error. + * + * @param documents The documents, if no error occurred, or nil. + * @param error The error, if one occurred, or nil. + */ +typedef void (^FSTVoidMaybeDocumentArrayErrorBlock)( + NSArray<FSTMaybeDocument *> *_Nullable documents, NSError *_Nullable error); + +/** + * FSTTransactionBlock is a block that wraps a user's transaction update block internally. + * + * @param transaction An object with methods for performing reads and writes within the + * transaction. + * @param completion To be called by the block once the user's code is finished. + */ +typedef void (^FSTTransactionBlock)(FSTTransaction *transaction, + void (^completion)(id _Nullable, NSError *_Nullable)); + +/** Describes the online state of the Firestore client */ +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. + */ + FSTOnlineStateUnknown, + + /** + * The client is connected and the connections are healthy. This state is reached after a + * successful connection and there has been at least one successful message received from the + * backends. + */ + 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. + */ + FSTOnlineStateFailed +}; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.h b/Firestore/Source/Core/FSTView.h new file mode 100644 index 0000000..2dbfac2 --- /dev/null +++ b/Firestore/Source/Core/FSTView.h @@ -0,0 +1,143 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKeySet.h" + +@class FSTDocumentKey; +@class FSTDocumentSet; +@class FSTDocumentViewChangeSet; +@class FSTMaybeDocument; +@class FSTQuery; +@class FSTTargetChange; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTViewDocumentChanges + +/** The result of applying a set of doc changes to a view. */ +@interface FSTViewDocumentChanges : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** The new set of docs that should be in the view. */ +@property(nonatomic, strong, readonly) FSTDocumentSet *documentSet; + +/** The diff of this these docs with the previous set of docs. */ +@property(nonatomic, strong, readonly) FSTDocumentViewChangeSet *changeSet; + +/** + * Whether the set of documents passed in was not sufficient to calculate the new state of the view + * and there needs to be another pass based on the local cache. + */ +@property(nonatomic, assign, readonly) BOOL needsRefill; + +@property(nonatomic, strong, readonly) FSTDocumentKeySet *mutatedKeys; + +@end + +#pragma mark - FSTLimboDocumentChange + +typedef NS_ENUM(NSInteger, FSTLimboDocumentChangeType) { + FSTLimboDocumentChangeTypeAdded = 0, + FSTLimboDocumentChangeTypeRemoved, +}; + +// A change to a particular document wrt to whether it is in "limbo". +@interface FSTLimboDocumentChange : NSObject + ++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key; + +- (id)init __attribute__((unavailable("Use a static constructor method."))); + +@property(nonatomic, assign, readonly) FSTLimboDocumentChangeType type; +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@end + +#pragma mark - FSTViewChange + +// A set of changes to a view. +@interface FSTViewChange : NSObject + +- (id)init __attribute__((unavailable("Use a static constructor method."))); + +@property(nonatomic, strong, readonly, nullable) FSTViewSnapshot *snapshot; +@property(nonatomic, strong, readonly) NSArray<FSTLimboDocumentChange *> *limboChanges; +@end + +#pragma mark - FSTView + +/** + * View is responsible for computing the final merged truth of what docs are in a query. It gets + * notified of local and remote changes to docs, and applies the query filters and limits to + * determine the most correct possible results. + */ +@interface FSTView : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithQuery:(FSTQuery *)query + remoteDocuments:(FSTDocumentKeySet *)remoteDocuments NS_DESIGNATED_INITIALIZER; + +/** + * Iterates over a set of doc changes, applies the query limit, and computes what the new results + * should be, what the changes were, and whether we may need to go back to the local cache for + * more results. Does not make any changes to the view. + * + * @param docChanges The doc changes to apply to this view. + * @return a new set of docs, changes, and refill flag. + */ +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges; + +/** + * Iterates over a set of doc changes, applies the query limit, and computes what the new results + * should be, what the changes were, and whether we may need to go back to the local cache for + * more results. Does not make any changes to the view. + * + * @param docChanges The doc changes to apply to this view. + * @param previousChanges If this is being called with a refill, then start with this set of docs + * and changes instead of the current view. + * @return a new set of docs, changes, and refill flag. + */ +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges + previousChanges: + (nullable FSTViewDocumentChanges *)previousChanges; + +/** + * Updates the view with the given ViewDocumentChanges. + * + * @param docChanges The set of changes to make to the view's docs. + * @return A new FSTViewChange with the given docs, changes, and sync state. + */ +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges; + +/** + * Updates the view with the given FSTViewDocumentChanges and updates limbo docs and sync state from + * the given (optional) target change. + * + * @param docChanges The set of changes to make to the view's docs. + * @param targetChange A target change to apply for computing limbo docs and sync state. + * @return A new FSTViewChange with the given docs, changes, and sync state. + */ +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges + targetChange:(nullable FSTTargetChange *)targetChange; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTView.m b/Firestore/Source/Core/FSTView.m new file mode 100644 index 0000000..719e303 --- /dev/null +++ b/Firestore/Source/Core/FSTView.m @@ -0,0 +1,451 @@ +/* + * 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 "FSTView.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTFieldValue.h" +#import "FSTQuery.h" +#import "FSTRemoteEvent.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTViewDocumentChanges + +/** The result of applying a set of doc changes to a view. */ +@interface FSTViewDocumentChanges () + +- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet + changeSet:(FSTDocumentViewChangeSet *)changeSet + needsRefill:(BOOL)needsRefill + mutatedKeys:(FSTDocumentKeySet *)mutatedKeys NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTViewDocumentChanges + +- (instancetype)initWithDocumentSet:(FSTDocumentSet *)documentSet + changeSet:(FSTDocumentViewChangeSet *)changeSet + needsRefill:(BOOL)needsRefill + mutatedKeys:(FSTDocumentKeySet *)mutatedKeys { + self = [super init]; + if (self) { + _documentSet = documentSet; + _changeSet = changeSet; + _needsRefill = needsRefill; + _mutatedKeys = mutatedKeys; + } + return self; +} + +@end + +#pragma mark - FSTLimboDocumentChange + +@interface FSTLimboDocumentChange () + ++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key; + +- (instancetype)initWithType:(FSTLimboDocumentChangeType)type + key:(FSTDocumentKey *)key NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTLimboDocumentChange + ++ (instancetype)changeWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key { + return [[FSTLimboDocumentChange alloc] initWithType:type key:key]; +} + +- (instancetype)initWithType:(FSTLimboDocumentChangeType)type key:(FSTDocumentKey *)key { + self = [super init]; + if (self) { + _type = type; + _key = key; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTLimboDocumentChange class]]) { + return NO; + } + FSTLimboDocumentChange *otherChange = (FSTLimboDocumentChange *)other; + return self.type == otherChange.type && [self.key isEqual:otherChange.key]; +} + +@end + +#pragma mark - FSTViewChange + +@interface FSTViewChange () + ++ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges; + +- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges + NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTViewChange + ++ (FSTViewChange *)changeWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges { + return [[self alloc] initWithSnapshot:snapshot limboChanges:limboChanges]; +} + +- (instancetype)initWithSnapshot:(nullable FSTViewSnapshot *)snapshot + limboChanges:(NSArray<FSTLimboDocumentChange *> *)limboChanges { + self = [super init]; + if (self) { + _snapshot = snapshot; + _limboChanges = limboChanges; + } + return self; +} + +@end + +#pragma mark - FSTView + +static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1, + FSTDocumentViewChangeType c2); + +@interface FSTView () + +@property(nonatomic, strong, readonly) FSTQuery *query; + +@property(nonatomic, assign) FSTSyncState syncState; + +/** + * A flag whether the view is current with the backend. A view is considered current after it + * has seen the current flag from the backend and did not lose consistency within the watch stream + * (e.g. because of an existence filter mismatch). + */ +@property(nonatomic, assign, getter=isCurrent) BOOL current; + +@property(nonatomic, strong) FSTDocumentSet *documentSet; + +/** Documents included in the remote target. */ +@property(nonatomic, strong) FSTDocumentKeySet *syncedDocuments; + +/** Documents in the view but not in the remote target */ +@property(nonatomic, strong) FSTDocumentKeySet *limboDocuments; + +/** Document Keys that have local changes. */ +@property(nonatomic, strong) FSTDocumentKeySet *mutatedKeys; + +@end + +@implementation FSTView + +- (instancetype)initWithQuery:(FSTQuery *)query + remoteDocuments:(nonnull FSTDocumentKeySet *)remoteDocuments { + self = [super init]; + if (self) { + _query = query; + _documentSet = [FSTDocumentSet documentSetWithComparator:query.comparator]; + _syncedDocuments = remoteDocuments; + _limboDocuments = [FSTDocumentKeySet keySet]; + _mutatedKeys = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges { + return [self computeChangesWithDocuments:docChanges previousChanges:nil]; +} + +- (FSTViewDocumentChanges *)computeChangesWithDocuments:(FSTMaybeDocumentDictionary *)docChanges + previousChanges: + (nullable FSTViewDocumentChanges *)previousChanges { + FSTDocumentViewChangeSet *changeSet = + previousChanges ? previousChanges.changeSet : [FSTDocumentViewChangeSet changeSet]; + FSTDocumentSet *oldDocumentSet = previousChanges ? previousChanges.documentSet : self.documentSet; + + __block FSTDocumentKeySet *newMutatedKeys = + previousChanges ? previousChanges.mutatedKeys : self.mutatedKeys; + __block FSTDocumentSet *newDocumentSet = oldDocumentSet; + __block BOOL needsRefill = NO; + + // Track the last doc in a (full) limit. This is necessary, because some update (a delete, or an + // update moving a doc past the old limit) might mean there is some other document in the local + // cache that either should come (1) between the old last limit doc and the new last document, + // in the case of updates, or (2) after the new last document, in the case of deletes. So we + // keep this doc at the old limit to compare the updates to. + // + // Note that this should never get used in a refill (when previousChanges is set), because there + // will only be adds -- no deletes or updates. + FSTDocument *_Nullable lastDocInLimit = + (self.query.limit && oldDocumentSet.count == self.query.limit) ? oldDocumentSet.lastDocument + : nil; + + [docChanges enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTMaybeDocument *maybeNewDoc, BOOL *stop) { + FSTDocument *_Nullable oldDoc = [oldDocumentSet documentForKey:key]; + FSTDocument *_Nullable newDoc = nil; + if ([maybeNewDoc isKindOfClass:[FSTDocument class]]) { + newDoc = (FSTDocument *)maybeNewDoc; + } + if (newDoc) { + FSTAssert([key isEqual:newDoc.key], @"Mismatching key in document changes: %@ != %@", key, + newDoc.key); + if (![self.query matchesDocument:newDoc]) { + newDoc = nil; + } + } + if (newDoc) { + newDocumentSet = [newDocumentSet documentSetByAddingDocument:newDoc]; + if (newDoc.hasLocalMutations) { + newMutatedKeys = [newMutatedKeys setByAddingObject:key]; + } else { + newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + } + } else { + newDocumentSet = [newDocumentSet documentSetByRemovingKey:key]; + newMutatedKeys = [newMutatedKeys setByRemovingObject:key]; + } + + // Calculate change + if (oldDoc && newDoc) { + BOOL docsEqual = [oldDoc.data isEqual:newDoc.data]; + if (!docsEqual || oldDoc.hasLocalMutations != newDoc.hasLocalMutations) { + // only report a change if document actually changed. + if (docsEqual) { + [changeSet addChange:[FSTDocumentViewChange + changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeMetadata]]; + } else { + [changeSet addChange:[FSTDocumentViewChange + changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeModified]]; + } + + if (lastDocInLimit && self.query.comparator(newDoc, lastDocInLimit) > 0) { + // This doc moved from inside the limit to after the limit. That means there may be some + // doc in the local cache that's actually less than this one. + needsRefill = YES; + } + } + } else if (!oldDoc && newDoc) { + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:newDoc + type:FSTDocumentViewChangeTypeAdded]]; + } else if (oldDoc && !newDoc) { + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:oldDoc + type:FSTDocumentViewChangeTypeRemoved]]; + if (lastDocInLimit) { + // A doc was removed from a full limit query. We'll need to re-query from the local cache + // to see if we know about some other doc that should be in the results. + needsRefill = YES; + } + } + }]; + if (self.query.limit) { + // TODO(klimt): Make DocumentSet size be constant time. + while (newDocumentSet.count > self.query.limit) { + FSTDocument *oldDoc = [newDocumentSet lastDocument]; + newDocumentSet = [newDocumentSet documentSetByRemovingKey:oldDoc.key]; + [changeSet + addChange:[FSTDocumentViewChange changeWithDocument:oldDoc + type:FSTDocumentViewChangeTypeRemoved]]; + } + } + + FSTAssert(!needsRefill || !previousChanges, + @"View was refilled using docs that themselves needed refilling."); + + return [[FSTViewDocumentChanges alloc] initWithDocumentSet:newDocumentSet + changeSet:changeSet + needsRefill:needsRefill + mutatedKeys:newMutatedKeys]; +} + +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges { + return [self applyChangesToDocuments:docChanges targetChange:nil]; +} + +- (FSTViewChange *)applyChangesToDocuments:(FSTViewDocumentChanges *)docChanges + targetChange:(nullable FSTTargetChange *)targetChange { + FSTAssert(!docChanges.needsRefill, @"Cannot apply changes that need a refill"); + + FSTDocumentSet *oldDocuments = self.documentSet; + self.documentSet = docChanges.documentSet; + self.mutatedKeys = docChanges.mutatedKeys; + + // Sort changes based on type and query comparator. + NSArray<FSTDocumentViewChange *> *changes = [docChanges.changeSet changes]; + changes = [changes sortedArrayUsingComparator:^NSComparisonResult(FSTDocumentViewChange *c1, + FSTDocumentViewChange *c2) { + NSComparisonResult typeComparison = FSTCompareDocumentViewChangeTypes(c1.type, c2.type); + if (typeComparison != NSOrderedSame) { + return typeComparison; + } + return self.query.comparator(c1.document, c2.document); + }]; + + NSArray<FSTLimboDocumentChange *> *limboChanges = [self applyTargetChange:targetChange]; + BOOL synced = self.limboDocuments.count == 0 && self.isCurrent; + FSTSyncState newSyncState = synced ? FSTSyncStateSynced : FSTSyncStateLocal; + BOOL syncStateChanged = newSyncState != self.syncState; + self.syncState = newSyncState; + + if (changes.count == 0 && !syncStateChanged) { + // No changes. + return [FSTViewChange changeWithSnapshot:nil limboChanges:limboChanges]; + } else { + FSTViewSnapshot *snapshot = + [[FSTViewSnapshot alloc] initWithQuery:self.query + documents:docChanges.documentSet + oldDocuments:oldDocuments + documentChanges:changes + fromCache:newSyncState == FSTSyncStateLocal + hasPendingWrites:!docChanges.mutatedKeys.isEmpty + syncStateChanged:syncStateChanged]; + + return [FSTViewChange changeWithSnapshot:snapshot limboChanges:limboChanges]; + } +} + +#pragma mark - Private methods + +/** Returns whether the doc for the given key should be in limbo. */ +- (BOOL)shouldBeLimboDocumentKey:(FSTDocumentKey *)key { + // If the remote end says it's part of this query, it's not in limbo. + if ([self.syncedDocuments containsObject:key]) { + return NO; + } + // The local store doesn't think it's a result, so it shouldn't be in limbo. + if (![self.documentSet containsKey:key]) { + return NO; + } + // If there are local changes to the doc, they might explain why the server doesn't know that it's + // part of the query. So don't put it in limbo. + // TODO(klimt): Ideally, we would only consider changes that might actually affect this specific + // query. + if ([self.documentSet documentForKey:key].hasLocalMutations) { + return NO; + } + // Everything else is in limbo. + return YES; +} + +/** + * Updates syncedDocuments, isAcked, and limbo docs based on the given change. + * @return the list of changes to which docs are in limbo. + */ +- (NSArray<FSTLimboDocumentChange *> *)applyTargetChange:(nullable FSTTargetChange *)targetChange { + if (targetChange) { + FSTTargetMapping *targetMapping = targetChange.mapping; + if ([targetMapping isKindOfClass:[FSTResetMapping class]]) { + self.syncedDocuments = ((FSTResetMapping *)targetMapping).documents; + } else if ([targetMapping isKindOfClass:[FSTUpdateMapping class]]) { + [((FSTUpdateMapping *)targetMapping).addedDocuments + enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + self.syncedDocuments = [self.syncedDocuments setByAddingObject:key]; + }]; + [((FSTUpdateMapping *)targetMapping).removedDocuments + enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + self.syncedDocuments = [self.syncedDocuments setByRemovingObject:key]; + }]; + } + + switch (targetChange.currentStatusUpdate) { + case FSTCurrentStatusUpdateMarkCurrent: + self.current = YES; + break; + case FSTCurrentStatusUpdateMarkNotCurrent: + self.current = NO; + break; + case FSTCurrentStatusUpdateNone: + break; + } + } + + // Recompute the set of limbo docs. + // TODO(klimt): Do this incrementally so that it's not quadratic when updating many documents. + FSTDocumentKeySet *oldLimboDocuments = self.limboDocuments; + self.limboDocuments = [FSTDocumentKeySet keySet]; + if (self.isCurrent) { + for (FSTDocument *doc in self.documentSet.documentEnumerator) { + if ([self shouldBeLimboDocumentKey:doc.key]) { + self.limboDocuments = [self.limboDocuments setByAddingObject:doc.key]; + } + } + } + + // Diff the new limbo docs with the old limbo docs. + NSMutableArray<FSTLimboDocumentChange *> *changes = + [NSMutableArray arrayWithCapacity:(oldLimboDocuments.count + self.limboDocuments.count)]; + [oldLimboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + if (![self.limboDocuments containsObject:key]) { + [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeRemoved + key:key]]; + } + }]; + [self.limboDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + if (![oldLimboDocuments containsObject:key]) { + [changes addObject:[FSTLimboDocumentChange changeWithType:FSTLimboDocumentChangeTypeAdded + key:key]]; + } + }]; + return changes; +} + +@end + +static inline int DocumentViewChangeTypePosition(FSTDocumentViewChangeType changeType) { + switch (changeType) { + case FSTDocumentViewChangeTypeRemoved: + return 0; + case FSTDocumentViewChangeTypeAdded: + return 1; + case FSTDocumentViewChangeTypeModified: + return 2; + case FSTDocumentViewChangeTypeMetadata: + // A metadata change is converted to a modified change at the public API layer. Since we sort + // by document key and then change type, metadata and modified changes must be sorted + // equivalently. + return 2; + default: + FSTCFail(@"Unknown FSTDocumentViewChangeType %lu", (unsigned long)changeType); + } +} + +static NSComparisonResult FSTCompareDocumentViewChangeTypes(FSTDocumentViewChangeType c1, + FSTDocumentViewChangeType c2) { + int pos1 = DocumentViewChangeTypePosition(c1); + int pos2 = DocumentViewChangeTypePosition(c2); + if (pos1 == pos2) { + return NSOrderedSame; + } else if (pos1 < pos2) { + return NSOrderedAscending; + } else { + return NSOrderedDescending; + } +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTViewSnapshot.h b/Firestore/Source/Core/FSTViewSnapshot.h new file mode 100644 index 0000000..3db6108 --- /dev/null +++ b/Firestore/Source/Core/FSTViewSnapshot.h @@ -0,0 +1,117 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTDocument; +@class FSTQuery; +@class FSTDocumentSet; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTDocumentViewChange + +/** + * The types of changes that can happen to a document with respect to a view. + * NOTE: We sort document changes by their type, so the ordering of this enum is significant. + */ +typedef NS_ENUM(NSInteger, FSTDocumentViewChangeType) { + FSTDocumentViewChangeTypeRemoved = 0, + FSTDocumentViewChangeTypeAdded, + FSTDocumentViewChangeTypeModified, + FSTDocumentViewChangeTypeMetadata, +}; + +/** A change to a single document's state within a view. */ +@interface FSTDocumentViewChange : NSObject + +- (id)init __attribute__((unavailable("Use a static constructor method."))); + ++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type; + +/** The type of change for the document. */ +@property(nonatomic, assign, readonly) FSTDocumentViewChangeType type; +/** The document whose status changed. */ +@property(nonatomic, strong, readonly) FSTDocument *document; + +@end + +#pragma mark - FSTDocumentChangeSet + +/** The possibly states a document can be in w.r.t syncing from local storage to the backend. */ +typedef NS_ENUM(NSInteger, FSTSyncState) { + FSTSyncStateNone = 0, + FSTSyncStateLocal, + FSTSyncStateSynced, +}; + +/** A set of changes to documents with respect to a view. This set is mutable. */ +@interface FSTDocumentViewChangeSet : NSObject + +/** Returns a new empty change set. */ ++ (instancetype)changeSet; + +/** Takes a new change and applies it to the set. */ +- (void)addChange:(FSTDocumentViewChange *)change; + +/** Returns the set of all changes tracked in this set. */ +- (NSArray<FSTDocumentViewChange *> *)changes; + +@end + +#pragma mark - FSTViewSnapshot + +typedef void (^FSTViewSnapshotHandler)(FSTViewSnapshot *_Nullable snapshot, + NSError *_Nullable error); + +/** A view snapshot is an immutable capture of the results of a query and the changes to them. */ +@interface FSTViewSnapshot : NSObject + +- (instancetype)initWithQuery:(FSTQuery *)query + documents:(FSTDocumentSet *)documents + oldDocuments:(FSTDocumentSet *)oldDocuments + documentChanges:(NSArray<FSTDocumentViewChange *> *)documentChanges + fromCache:(BOOL)fromCache + hasPendingWrites:(BOOL)hasPendingWrites + syncStateChanged:(BOOL)syncStateChanged NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The query this view is tracking the results for. */ +@property(nonatomic, strong, readonly) FSTQuery *query; + +/** The documents currently known to be results of the query. */ +@property(nonatomic, strong, readonly) FSTDocumentSet *documents; + +/** The documents of the last snapshot. */ +@property(nonatomic, strong, readonly) FSTDocumentSet *oldDocuments; + +/** The set of changes that have been applied to the documents. */ +@property(nonatomic, strong, readonly) NSArray<FSTDocumentViewChange *> *documentChanges; + +/** Whether any document in the snapshot was served from the local cache. */ +@property(nonatomic, assign, readonly, getter=isFromCache) BOOL fromCache; + +/** Whether any document in the snapshot has pending local writes. */ +@property(nonatomic, assign, readonly) BOOL hasPendingWrites; + +/** Whether the sync state changed as part of this snapshot. */ +@property(nonatomic, assign, readonly) BOOL syncStateChanged; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Core/FSTViewSnapshot.m b/Firestore/Source/Core/FSTViewSnapshot.m new file mode 100644 index 0000000..016f890 --- /dev/null +++ b/Firestore/Source/Core/FSTViewSnapshot.m @@ -0,0 +1,231 @@ +/* + * 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 "FSTViewSnapshot.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTImmutableSortedDictionary.h" +#import "FSTQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTDocumentViewChange + +@interface FSTDocumentViewChange () + ++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type; + +- (instancetype)initWithDocument:(FSTDocument *)document + type:(FSTDocumentViewChangeType)type NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTDocumentViewChange + ++ (instancetype)changeWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type { + return [[FSTDocumentViewChange alloc] initWithDocument:document type:type]; +} + +- (instancetype)initWithDocument:(FSTDocument *)document type:(FSTDocumentViewChangeType)type { + self = [super init]; + if (self) { + _document = document; + _type = type; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + if (![other isKindOfClass:[FSTDocumentViewChange class]]) { + return NO; + } + FSTDocumentViewChange *otherChange = (FSTDocumentViewChange *)other; + return [self.document isEqual:otherChange.document] && self.type == otherChange.type; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"<FSTDocumentViewChange type:%ld doc:%@>", (long)self.type, self.document]; +} + +@end + +#pragma mark - FSTDocumentViewChangeSet + +@interface FSTDocumentViewChangeSet () + +/** The set of all changes tracked so far, with redundant changes merged. */ +@property(nonatomic, strong) + FSTImmutableSortedDictionary<FSTDocumentKey *, FSTDocumentViewChange *> *changeMap; + +@end + +@implementation FSTDocumentViewChangeSet + ++ (instancetype)changeSet { + return [[FSTDocumentViewChangeSet alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _changeMap = [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + } + return self; +} + +- (NSString *)description { + return [self.changeMap description]; +} + +- (void)addChange:(FSTDocumentViewChange *)change { + FSTDocumentKey *key = change.document.key; + FSTDocumentViewChange *oldChange = [self.changeMap objectForKey:key]; + if (!oldChange) { + self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key]; + return; + } + + // Merge the new change with the existing change. + if (change.type != FSTDocumentViewChangeTypeAdded && + oldChange.type == FSTDocumentViewChangeTypeMetadata) { + self.changeMap = [self.changeMap dictionaryBySettingObject:change forKey:key]; + + } else if (change.type == FSTDocumentViewChangeTypeMetadata && + oldChange.type != FSTDocumentViewChangeTypeRemoved) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document type:oldChange.type]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + + } else if (change.type == FSTDocumentViewChangeTypeModified && + oldChange.type == FSTDocumentViewChangeTypeModified) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeModified]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeModified && + oldChange.type == FSTDocumentViewChangeTypeAdded) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeAdded]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeRemoved && + oldChange.type == FSTDocumentViewChangeTypeAdded) { + self.changeMap = [self.changeMap dictionaryByRemovingObjectForKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeRemoved && + oldChange.type == FSTDocumentViewChangeTypeModified) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:oldChange.document + type:FSTDocumentViewChangeTypeRemoved]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else if (change.type == FSTDocumentViewChangeTypeAdded && + oldChange.type == FSTDocumentViewChangeTypeRemoved) { + FSTDocumentViewChange *newChange = + [FSTDocumentViewChange changeWithDocument:change.document + type:FSTDocumentViewChangeTypeModified]; + self.changeMap = [self.changeMap dictionaryBySettingObject:newChange forKey:key]; + } else { + // This includes these cases, which don't make sense: + // Added -> Added + // Removed -> Removed + // Modified -> Added + // Removed -> Modified + // Metadata -> Added + // Removed -> Metadata + FSTFail(@"Unsupported combination of changes: %ld after %ld", (long)change.type, + (long)oldChange.type); + } +} + +- (NSArray<FSTDocumentViewChange *> *)changes { + NSMutableArray<FSTDocumentViewChange *> *changes = [NSMutableArray array]; + [self.changeMap enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, + FSTDocumentViewChange *change, BOOL *stop) { + [changes addObject:change]; + }]; + return changes; +} + +@end + +#pragma mark - FSTViewSnapshot + +@implementation FSTViewSnapshot + +- (instancetype)initWithQuery:(FSTQuery *)query + documents:(FSTDocumentSet *)documents + oldDocuments:(FSTDocumentSet *)oldDocuments + documentChanges:(NSArray<FSTDocumentViewChange *> *)documentChanges + fromCache:(BOOL)fromCache + hasPendingWrites:(BOOL)hasPendingWrites + syncStateChanged:(BOOL)syncStateChanged { + self = [super init]; + if (self) { + _query = query; + _documents = documents; + _oldDocuments = oldDocuments; + _documentChanges = documentChanges; + _fromCache = fromCache; + _hasPendingWrites = hasPendingWrites; + _syncStateChanged = syncStateChanged; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat: + @"<FSTViewSnapshot query:%@ documents:%@ oldDocument:%@ changes:%@ " + "fromCache:%@ hasPendingWrites:%@ syncStateChanged:%@>", + self.query, self.documents, self.oldDocuments, self.documentChanges, + (self.fromCache ? @"YES" : @"NO"), (self.hasPendingWrites ? @"YES" : @"NO"), + (self.syncStateChanged ? @"YES" : @"NO")]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } else if (![object isKindOfClass:[FSTViewSnapshot class]]) { + return NO; + } + + FSTViewSnapshot *other = object; + return [self.query isEqual:other.query] && [self.documents isEqual:other.documents] && + [self.oldDocuments isEqual:other.oldDocuments] && + [self.documentChanges isEqualToArray:other.documentChanges] && + self.fromCache == other.fromCache && self.hasPendingWrites == other.hasPendingWrites && + self.syncStateChanged == other.syncStateChanged; +} + +- (NSUInteger)hash { + NSUInteger result = [self.query hash]; + result = 31 * result + [self.documents hash]; + result = 31 * result + [self.oldDocuments hash]; + result = 31 * result + [self.documentChanges hash]; + result = 31 * result + (self.fromCache ? 1231 : 1237); + result = 31 * result + (self.hasPendingWrites ? 1231 : 1237); + result = 31 * result + (self.syncStateChanged ? 1231 : 1237); + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTDocumentReference.h b/Firestore/Source/Local/FSTDocumentReference.h new file mode 100644 index 0000000..eff60e4 --- /dev/null +++ b/Firestore/Source/Local/FSTDocumentReference.h @@ -0,0 +1,61 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * An immutable value used to keep track of an association between some referencing target or batch + * and a document key that the target or batch references. + * + * A reference can be from either listen targets (identified by their FSTTargetID) or mutation + * batches (identified by their FSTBatchID). See FSTGarbageCollector for more details. + * + * Not to be confused with FIRDocumentReference. + */ +@interface FSTDocumentReference : NSObject <NSCopying> + +/** Initializes the document reference with the given key and ID. */ +- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int)ID NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The document key that's the target of this reference. */ +@property(nonatomic, strong, readonly) FSTDocumentKey *key; + +/** + * The targetID of a referring target or the batchID of a referring mutation batch. (Which this + * is depends upon which FSTReferenceSet this reference is a part of.) + */ +@property(nonatomic, assign, readonly) int ID; + +@end + +#pragma mark Comparators + +/** Sorts document references by key then ID. */ +extern const NSComparator FSTDocumentReferenceComparatorByKey; + +/** Sorts document references by ID then key. */ +extern const NSComparator FSTDocumentReferenceComparatorByID; + +/** A callback for use when enumerating an FSTImmutableSortedSet of FSTDocumentReferences. */ +typedef void (^FSTDocumentReferenceBlock)(FSTDocumentReference *reference, BOOL *stop); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTDocumentReference.m b/Firestore/Source/Local/FSTDocumentReference.m new file mode 100644 index 0000000..7d9e3db --- /dev/null +++ b/Firestore/Source/Local/FSTDocumentReference.m @@ -0,0 +1,83 @@ +/* + * 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 "FSTDocumentReference.h" + +#import "FSTComparison.h" +#import "FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTDocumentReference + +- (instancetype)initWithKey:(FSTDocumentKey *)key ID:(int)ID { + self = [super init]; + if (self) { + _key = key; + _ID = ID; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + + FSTDocumentReference *reference = (FSTDocumentReference *)other; + + return [self.key isEqualToKey:reference.key] && self.ID == reference.ID; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31u + self.ID; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTDocumentReference: key=%@, ID=%d>", self.key, self.ID]; +} + +- (id)copyWithZone:(nullable NSZone *)zone { + // FSTDocumentReference is immutable + return self; +} + +@end + +#pragma mark Comparators + +/** Sorts document references by key then ID. */ +const NSComparator FSTDocumentReferenceComparatorByKey = + ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) { + NSComparisonResult result = FSTDocumentKeyComparator(left.key, right.key); + if (result != NSOrderedSame) { + return result; + } + return FSTCompareInts(left.ID, right.ID); + }; + +/** Sorts document references by ID then key. */ +const NSComparator FSTDocumentReferenceComparatorByID = + ^NSComparisonResult(FSTDocumentReference *left, FSTDocumentReference *right) { + NSComparisonResult result = FSTCompareInts(left.ID, right.ID); + if (result != NSOrderedSame) { + return result; + } + return FSTDocumentKeyComparator(left.key, right.key); + }; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTEagerGarbageCollector.h b/Firestore/Source/Local/FSTEagerGarbageCollector.h new file mode 100644 index 0000000..f2f373c --- /dev/null +++ b/Firestore/Source/Local/FSTEagerGarbageCollector.h @@ -0,0 +1,36 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTGarbageCollector.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A garbage collector implementation that eagerly collects documents as soon as they're no longer + * referenced in any of its registered FSTGarbageSources. + * + * This implementation keeps track of a set of keys that are potentially garbage without keeping + * an exact reference count. During -collectGarbage, the collector verifies that all potential + * garbage keys actually have no references by consulting its list of garbage sources. + */ +@interface FSTEagerGarbageCollector : NSObject <FSTGarbageCollector> +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTEagerGarbageCollector.m b/Firestore/Source/Local/FSTEagerGarbageCollector.m new file mode 100644 index 0000000..971f368 --- /dev/null +++ b/Firestore/Source/Local/FSTEagerGarbageCollector.m @@ -0,0 +1,89 @@ +/* + * 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 "FSTEagerGarbageCollector.h" + +#import "FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTMultiReferenceSet + +@interface FSTEagerGarbageCollector () + +/** The garbage collectible sources to double-check during garbage collection. */ +@property(nonatomic, strong, readonly) NSMutableArray<id<FSTGarbageSource>> *sources; + +/** A set of potentially garbage keys. */ +@property(nonatomic, strong, readonly) NSMutableSet<FSTDocumentKey *> *potentialGarbage; + +@end + +@implementation FSTEagerGarbageCollector + +- (instancetype)init { + self = [super init]; + if (self) { + _sources = [NSMutableArray array]; + _potentialGarbage = [[NSMutableSet alloc] init]; + } + return self; +} + +- (BOOL)isEager { + return YES; +} + +- (void)addGarbageSource:(id<FSTGarbageSource>)garbageSource { + [self.sources addObject:garbageSource]; + garbageSource.garbageCollector = self; +} + +- (void)removeGarbageSource:(id<FSTGarbageSource>)garbageSource { + [self.sources removeObject:garbageSource]; + garbageSource.garbageCollector = nil; +} + +- (void)addPotentialGarbageKey:(FSTDocumentKey *)key { + [self.potentialGarbage addObject:key]; +} + +- (NSMutableSet<FSTDocumentKey *> *)collectGarbage { + NSMutableArray<id<FSTGarbageSource>> *sources = self.sources; + + NSMutableSet<FSTDocumentKey *> *actualGarbage = [NSMutableSet set]; + for (FSTDocumentKey *key in self.potentialGarbage) { + BOOL isGarbage = YES; + for (id<FSTGarbageSource> source in sources) { + if ([source containsKey:key]) { + isGarbage = NO; + break; + } + } + + if (isGarbage) { + [actualGarbage addObject:key]; + } + } + + // Clear locally retained potential keys and returned confirmed garbage. + [self.potentialGarbage removeAllObjects]; + return actualGarbage; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTGarbageCollector.h b/Firestore/Source/Local/FSTGarbageCollector.h new file mode 100644 index 0000000..c999f66 --- /dev/null +++ b/Firestore/Source/Local/FSTGarbageCollector.h @@ -0,0 +1,95 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTTypes.h" + +@class FSTDocumentKey; +@class FSTDocumentReference; +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A pseudo-collection that maintains references to documents. FSTGarbageSource collections + * notify the FSTGarbageCollector when references to documents change through the + * -addPotentialGarbageKey: message. + */ +@protocol FSTGarbageSource + +/** + * The garbage collector to which this collection should send -addPotentialGarbageKey: messages. + */ +@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector; + +/** + * Checks to see if there are any references to a document with the given key. This can be used by + * garbage collectors to double-check if a key exists in this collection when it was released + * elsewhere. + */ +- (BOOL)containsKey:(FSTDocumentKey *)key; + +@end + +/** + * Tracks different kinds of references to a document, for all the different ways the client + * needs to retain a document. + * + * Usually the local store this means tracking of three different types of references to a + * document: + * 1. RemoteTarget reference identified by a target ID. + * 2. LocalView reference identified also by a target ID. + * 3. Local mutation reference identified by a batch ID. + * + * The idea is that we want to keep a document around at least as long as any remote target or + * local (latency compensated) view is referencing it, or there's an outstanding local mutation to + * that document. + */ +@protocol FSTGarbageCollector + +/** + * A property that describes whether or not the collector wants to eagerly collect keys. + * + * TODO(b/33384523) Delegate deleting released queries to the GC. + * This flag is a temporary workaround for dealing with a persistent query cache. The collector + * really should have an API for releasing queries that does the right thing for its policy. + */ +@property(nonatomic, assign, readonly, getter=isEager) BOOL eager; + +/** Adds a garbage source to the collector. */ +- (void)addGarbageSource:(id<FSTGarbageSource>)garbageSource; + +/** Removes a garbage source from the collector. */ +- (void)removeGarbageSource:(id<FSTGarbageSource>)garbageSource; + +/** + * Notifies the garbage collector that a document with the given key may have become garbage. + * + * This is useful in both when a document has definitely been released (for example when removed + * from a garbage source) but also when a document has been updated. Documents should be marked in + * this way because the client accepts updates for documents even after the document no longer + * matches any active targets. This behavior allows the client to avoid re-showing an old document + * in the next latency-compensated view. + */ +- (void)addPotentialGarbageKey:(FSTDocumentKey *)key; + +/** Returns the contents of the garbage bin and clears it. */ +- (NSSet<FSTDocumentKey *> *)collectGarbage; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDB.h b/Firestore/Source/Local/FSTLevelDB.h new file mode 100644 index 0000000..a2c838d --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDB.h @@ -0,0 +1,105 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTPersistence.h" + +#ifdef __cplusplus +#include <memory> + +namespace leveldb { +class DB; +class Status; +} +#endif + +@class FSTDatabaseInfo; +@class FSTLocalSerializer; + +NS_ASSUME_NONNULL_BEGIN + +/** A LevelDB-backed instance of FSTPersistence. */ +// TODO(mikelehen): Rename to FSTLevelDBPersistence. +@interface FSTLevelDB : NSObject <FSTPersistence> + +/** + * Initializes the LevelDB in the given directory. Note that all expensive startup work including + * opening any database files is deferred until -[FSTPersistence start] is called. + */ +- (instancetype)initWithDirectory:(NSString *)directory + serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __attribute__((unavailable("Use -initWithDirectory: instead."))); + +/** Finds a suitable directory to serve as the root of all Firestore local storage. */ ++ (NSString *)documentsDirectory; + +/** + * Computes a unique storage directory for the given identifying components of local storage. + * + * @param databaseInfo The identifying information for the local storage instance. + * @param documentsDirectory The root document directory relative to which the storage directory + * will be created. Usually just +[FSTLevelDB documentsDir]. + * @return A storage directory unique to the instance identified by databaseInfo. + */ ++ (NSString *)storageDirectoryForDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + documentsDirectory:(NSString *)documentsDirectory; + +/** + * Starts LevelDB-backed persistent storage by opening the database files, creating the DB if it + * does not exist. + * + * The leveldb directory is created relative to the appropriate document storage directory for the + * platform: NSDocumentDirectory on iOS or $HOME/.firestore on macOS. + */ +- (BOOL)start:(NSError **)error; + +#ifdef __cplusplus +// What follows is the Objective-C++ extension to the API. + +/** + * Creates an NSError based on the given status if the status is not ok. + * + * @param status The status of the preceding LevelDB operation. + * @param description A printf-style format string describing what kind of failure happened if + * @a status is not ok. Additional parameters are substituted into the placeholders in this + * format string. + * + * @return An NSError with its localizedDescription composed from the description format and its + * localizedFailureReason composed from any error message embedded in @a status. + */ ++ (nullable NSError *)errorWithStatus:(leveldb::Status)status + description:(NSString *)description, ... NS_FORMAT_FUNCTION(2, 3); + +/** + * Converts the given @a status to an NSString describing the status condition, suitable for + * logging or inclusion in an NSError. + * + * @param status The status of the preceding LevelDB operation. + * + * @return An NSString describing the status (even if the status was ok). + */ ++ (NSString *)descriptionOfStatus:(leveldb::Status)status; + +/** The native db pointer, allocated during start. */ +@property(nonatomic, assign, readonly) std::shared_ptr<leveldb::DB> ptr; + +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDB.mm b/Firestore/Source/Local/FSTLevelDB.mm new file mode 100644 index 0000000..81e1064 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDB.mm @@ -0,0 +1,246 @@ +/* + * 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 "FSTLevelDB.h" + +#include <leveldb/db.h> + +#import "FIRFirestoreErrors.h" +#import "FSTAssert.h" +#import "FSTDatabaseID.h" +#import "FSTDatabaseInfo.h" +#import "FSTLevelDBMutationQueue.h" +#import "FSTLevelDBQueryCache.h" +#import "FSTLevelDBRemoteDocumentCache.h" +#import "FSTLogger.h" +#import "FSTSerializerBeta.h" +#import "FSTWriteGroup.h" +#import "FSTWriteGroupTracker.h" + +NS_ASSUME_NONNULL_BEGIN + +static NSString *const kReservedPathComponent = @"firestore"; + +using leveldb::DB; +using leveldb::Options; +using leveldb::Status; +using leveldb::WriteOptions; + +@interface FSTLevelDB () + +@property(nonatomic, copy) NSString *directory; +@property(nonatomic, strong) FSTWriteGroupTracker *writeGroupTracker; +@property(nonatomic, assign, getter=isStarted) BOOL started; +@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + +@end + +@implementation FSTLevelDB + +- (instancetype)initWithDirectory:(NSString *)directory + serializer:(FSTLocalSerializer *)serializer { + if (self = [super init]) { + _directory = [directory copy]; + _writeGroupTracker = [FSTWriteGroupTracker tracker]; + _serializer = serializer; + } + return self; +} + ++ (NSString *)documentsDirectory { +#if TARGET_OS_IPHONE + NSArray<NSString *> *directories = + NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); + return [directories[0] stringByAppendingPathComponent:kReservedPathComponent]; + +#elif TARGET_OS_MAC + NSString *dotPrefixed = [@"." stringByAppendingString:kReservedPathComponent]; + return [NSHomeDirectory() stringByAppendingPathComponent:dotPrefixed]; + +#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/ + +#endif +} + ++ (NSString *)storageDirectoryForDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + documentsDirectory:(NSString *)documentsDirectory { + // Use two different path formats: + // + // * persistenceKey / projectID . databaseID / name + // * persistenceKey / projectID / name + // + // projectIDs are DNS-compatible names and cannot contain dots so there's + // no danger of collisions. + NSString *directory = documentsDirectory; + directory = [directory stringByAppendingPathComponent:databaseInfo.persistenceKey]; + + NSString *segment = databaseInfo.databaseID.projectID; + if (![databaseInfo.databaseID isDefaultDatabase]) { + segment = [NSString stringWithFormat:@"%@.%@", segment, databaseInfo.databaseID.databaseID]; + } + directory = [directory stringByAppendingPathComponent:segment]; + + // Reserve one additional path component to allow multiple physical databases + directory = [directory stringByAppendingPathComponent:@"main"]; + return directory; +} + +#pragma mark - Startup + +- (BOOL)start:(NSError **)error { + FSTAssert(!self.isStarted, @"FSTLevelDB double-started!"); + self.started = YES; + NSString *directory = self.directory; + if (![self ensureDirectory:directory error:error]) { + return NO; + } + + DB *database = [self createDBWithDirectory:directory error:error]; + if (!database) { + return NO; + } + + _ptr.reset(database); + return YES; +} + +/** Creates the directory at @a directory and marks it as excluded from iCloud backup. */ +- (BOOL)ensureDirectory:(NSString *)directory error:(NSError **)error { + NSError *localError; + NSFileManager *files = [NSFileManager defaultManager]; + + BOOL success = [files createDirectoryAtPath:directory + withIntermediateDirectories:YES + attributes:nil + error:&localError]; + if (!success) { + *error = + [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:@{ + NSLocalizedDescriptionKey : @"Failed to create persistence directory", + NSUnderlyingErrorKey : localError + }]; + return NO; + } + + NSURL *dirURL = [NSURL fileURLWithPath:directory]; + success = [dirURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&localError]; + if (!success) { + *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:@{ + NSLocalizedDescriptionKey : + @"Failed mark persistence directory as excluded from backups", + NSUnderlyingErrorKey : localError + }]; + return NO; + } + + return YES; +} + +/** Opens the database within the given directory. */ +- (nullable DB *)createDBWithDirectory:(NSString *)directory error:(NSError **)error { + Options options; + options.create_if_missing = true; + + DB *database; + Status status = DB::Open(options, [directory UTF8String], &database); + if (!status.ok()) { + if (error) { + NSString *name = [directory lastPathComponent]; + *error = + [FSTLevelDB errorWithStatus:status + description:@"Failed to create database %@ at path %@", name, directory]; + } + return nullptr; + } + + return database; +} + +#pragma mark - Persistence Factory methods + +- (id<FSTMutationQueue>)mutationQueueForUser:(FSTUser *)user { + return [FSTLevelDBMutationQueue mutationQueueWithUser:user db:_ptr serializer:self.serializer]; +} + +- (id<FSTQueryCache>)queryCache { + return [[FSTLevelDBQueryCache alloc] initWithDB:_ptr serializer:self.serializer]; +} + +- (id<FSTRemoteDocumentCache>)remoteDocumentCache { + return [[FSTLevelDBRemoteDocumentCache alloc] initWithDB:_ptr serializer:self.serializer]; +} + +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { + return [self.writeGroupTracker startGroupWithAction:action]; +} + +- (void)commitGroup:(FSTWriteGroup *)group { + [self.writeGroupTracker endGroup:group]; + + NSString *description = [group description]; + FSTLog(@"Committing %@", description); + + Status status = [group writeToDB:_ptr]; + if (!status.ok()) { + FSTFail(@"%@ failed with status: %s, description: %@", group.action, status.ToString().c_str(), + description); + } +} + +- (void)shutdown { + FSTAssert(self.isStarted, @"FSTLevelDB shutdown without start!"); + self.started = NO; + _ptr.reset(); +} + +#pragma mark - Error and Status + ++ (nullable NSError *)errorWithStatus:(Status)status description:(NSString *)description, ... { + if (status.ok()) { + return nil; + } + + va_list args; + va_start(args, description); + + NSString *message = [[NSString alloc] initWithFormat:description arguments:args]; + NSString *reason = [self descriptionOfStatus:status]; + NSError *result = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:@{ + NSLocalizedDescriptionKey : message, + NSLocalizedFailureReasonErrorKey : reason + }]; + + va_end(args); + + return result; +} + ++ (NSString *)descriptionOfStatus:(Status)status { + return [NSString stringWithCString:status.ToString().c_str() encoding:NSUTF8StringEncoding]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBKey.h b/Firestore/Source/Local/FSTLevelDBKey.h new file mode 100644 index 0000000..bad7829 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBKey.h @@ -0,0 +1,344 @@ +/* + * 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. + */ + +#ifndef __cplusplus +#error "FSTLevelDBKey is Objective-C++ and can only be included from .mm files" +#endif + +#import <Foundation/Foundation.h> + +#import "FSTTypes.h" + +#import "StringView.h" + +@class FSTDocumentKey; +@class FSTResourcePath; + +NS_ASSUME_NONNULL_BEGIN + +// All leveldb logical tables should have their keys structures described in this file. +// +// mutations: +// - tableName: string = "mutation" +// - userID: string +// - batchID: FSTBatchID +// +// document_mutations: +// - tableName: string = "document_mutation" +// - userID: string +// - path: FSTResourcePath +// - batchID: FSTBatchID +// +// mutation_queues: +// - tableName: string = "mutation_queue" +// - userID: string +// +// targets: +// - tableName: string = "target" +// - targetId: FSTTargetID +// +// target_globals: +// - tableName: string = "target_global" +// +// query_targets: +// - tableName: string = "query_target" +// - canonicalID: string +// - targetId: FSTTargetID +// +// target_documents: +// - tableName: string = "target_document" +// - targetID: FSTTargetID +// - path: FSTResourcePath +// +// document_targets: +// - tableName: string = "document_target" +// - path: FSTResourcePath +// - targetID: FSTTargetID +// +// remote_documents: +// - tableName: string = "remote_document" +// - path: FSTResourcePath + +/** Helpers for any LevelDB key. */ +@interface FSTLevelDBKey : NSObject + +/** + * Parses the given key and returns a human readable description of its contents, suitable for + * error messages and logging. + */ ++ (NSString *)descriptionForKey:(Firestore::StringView)key; + +@end + +/** A key in the mutations table. */ +@interface FSTLevelDBMutationKey : NSObject + +/** Creates a key prefix that points just before the first key in the table. */ ++ (std::string)keyPrefix; + +/** Creates a key prefix that points just before the first key for the given userID. */ ++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID; + +/** Creates a complete key that points to a specific userID and batchID. */ ++ (std::string)keyWithUserID:(Firestore::StringView)userID batchID:(FSTBatchID)batchID; + +/** + * Decodes the given complete key, storing the decoded values as properties of the receiver. + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The user that owns the mutation batches. */ +@property(nonatomic, assign, readonly) const std::string &userID; + +/** The batchID of the batch. */ +@property(nonatomic, assign, readonly) FSTBatchID batchID; + +@end + +/** + * A key in the document mutations index, which stores the batches in which documents are mutated. + */ +@interface FSTLevelDBDocumentMutationKey : NSObject + +/** Creates a key prefix that points just before the first key in the table. */ ++ (std::string)keyPrefix; + +/** Creates a key prefix that points just before the first key for the given userID. */ ++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID; + +/** + * Creates a key prefix that points just before the first key for the userID and resource path. + * + * Note that this uses an FSTResourcePath rather than an FSTDocumentKey in order to allow prefix + * scans over a collection. However a naive scan over those results isn't useful since it would + * match both immediate children of the collection and any subcollections. + */ ++ (std::string)keyPrefixWithUserID:(Firestore::StringView)userID + resourcePath:(FSTResourcePath *)resourcePath; + +/** Creates a complete key that points to a specific userID, document key, and batchID. */ ++ (std::string)keyWithUserID:(Firestore::StringView)userID + documentKey:(FSTDocumentKey *)documentKey + batchID:(FSTBatchID)batchID; + +/** + * Decodes the given complete key, storing the decoded values as properties of the receiver. + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The user that owns the mutation batches. */ +@property(nonatomic, assign, readonly) const std::string &userID; + +/** The path to the document, as encoded in the key. */ +@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey; + +/** The batchID in which the document participates. */ +@property(nonatomic, assign, readonly) FSTBatchID batchID; + +@end + +/** + * A key in the mutation_queues table. + * + * Note that where mutation_queues contains one row about each queue, mutations contains the actual + * mutation batches themselves. + */ +@interface FSTLevelDBMutationQueueKey : NSObject + +/** Creates a key prefix that points just before the first key in the table. */ ++ (std::string)keyPrefix; + +/** Creates a complete key that points to a specific mutation queue entry for the given userID. */ ++ (std::string)keyWithUserID:(Firestore::StringView)userID; + +/** + * Decodes the given complete key, storing the decoded values as properties of the receiver. + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +@property(nonatomic, assign, readonly) const std::string &userID; + +@end + +/** A key in the target globals table, a record of global values across all targets. */ +@interface FSTLevelDBTargetGlobalKey : NSObject + +/** Creates a key that points to the single target global row. */ ++ (std::string)key; + +/** + * Decodes the contents of a target global key, essentially just verifying that the key has the + * correct table name. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +@end + +/** A key in the targets table. */ +@interface FSTLevelDBTargetKey : NSObject + +/** Creates a key prefix that points just before the first key in the table. */ ++ (std::string)keyPrefix; + +/** Creates a complete key that points to a specific target, by targetID. */ ++ (std::string)keyWithTargetID:(FSTTargetID)targetID; + +/** + * Decodes the contents of a target key into properties on this instance. + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The targetID identifying a target. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +@end + +/** + * A key in the query targets table, an index of canonicalIDs to the targets they may match. This + * is not a unique mapping because canonicalID does not promise a unique name for all possible + * queries. + */ +@interface FSTLevelDBQueryTargetKey : NSObject + +/** + * Creates a key that contains just the query targets table prefix and points just before the + * first key. + */ ++ (std::string)keyPrefix; + +/** Creates a key that points to the first query-target association for a canonicalID. */ ++ (std::string)keyPrefixWithCanonicalID:(Firestore::StringView)canonicalID; + +/** Creates a key that points to a specific query-target entry. */ ++ (std::string)keyWithCanonicalID:(Firestore::StringView)canonicalID targetID:(FSTTargetID)targetID; + +/** Decodes the contents of a query target key into properties on this instance. */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The canonicalID derived from the query. */ +@property(nonatomic, assign, readonly) const std::string &canonicalID; + +/** The targetID identifying a target. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +@end + +/** + * A key in the target documents table, an index of targetIDs to the documents they contain. + */ +@interface FSTLevelDBTargetDocumentKey : NSObject + +/** + * Creates a key that contains just the target documents table prefix and points just before the + * first key. + */ ++ (std::string)keyPrefix; + +/** Creates a key that points to the first target-document association for a targetID. */ ++ (std::string)keyPrefixWithTargetID:(FSTTargetID)targetID; + +/** Creates a key that points to a specific target-document entry. */ ++ (std::string)keyWithTargetID:(FSTTargetID)targetID documentKey:(FSTDocumentKey *)documentKey; + +/** Decodes the contents of a target document key into properties on this instance. */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The targetID identifying a target. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +/** The path to the document, as encoded in the key. */ +@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey; + +@end + +/** + * A key in the document targets table, an index from documents to the targets that contain them. + */ +@interface FSTLevelDBDocumentTargetKey : NSObject + +/** + * Creates a key that contains just the document targets table prefix and points just before the + * first key. + */ ++ (std::string)keyPrefix; + +/** Creates a key that points to the first document-target association for document. */ ++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath; + +/** Creates a key that points to a specific document-target entry. */ ++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)documentKey targetID:(FSTTargetID)targetID; + +/** Decodes the contents of a document target key into properties on this instance. */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The targetID identifying a target. */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +/** The path to the document, as encoded in the key. */ +@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey; + +@end + +/** A key in the remote documents table. */ +@interface FSTLevelDBRemoteDocumentKey : NSObject + +/** + * Creates a key that contains just the remote documents table prefix and points just before the + * first remote document key. + */ ++ (std::string)keyPrefix; + +/** + * Creates a complete key that points to a specific document. The documentKey must have an even + * number of path segments. + */ ++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)key; + +/** + * Creates a key prefix that contains a part of a document path. Odd numbers of segments create a + * collection key prefix, while an even number of segments create a document key prefix. Note that + * a document key prefix will match the document itself and any documents that exist in its + * subcollections. + */ ++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath; + +/** + * Decodes the contents of a remote document key into properties on this instance. This can only + * decode complete document paths (i.e. the result of +keyWithDocumentKey:). + * + * @return YES if the key successfully decoded, NO otherwise. If NO is returned, the properties of + * the receiver are in an undefined state until the next call to -decodeKey:. + */ +- (BOOL)decodeKey:(Firestore::StringView)key; + +/** The path to the document, as encoded in the key. */ +@property(nonatomic, strong, readonly, nullable) FSTDocumentKey *documentKey; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBKey.mm b/Firestore/Source/Local/FSTLevelDBKey.mm new file mode 100644 index 0000000..ee3e270 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBKey.mm @@ -0,0 +1,757 @@ +/* + * 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 "FSTLevelDBKey.h" + +#include <string> + +#include "ordered_code.h" +#include "string_util.h" +#import "FSTDocumentKey.h" +#import "FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +using Firestore::OrderedCode; +using Firestore::PrefixSuccessor; +using Firestore::StringView; +using leveldb::Slice; + +static const char *kMutationsTable = "mutation"; +static const char *kDocumentMutationsTable = "document_mutation"; +static const char *kMutationQueuesTable = "mutation_queue"; +static const char *kTargetGlobalTable = "target_global"; +static const char *kTargetsTable = "target"; +static const char *kQueryTargetsTable = "query_target"; +static const char *kTargetDocumentsTable = "target_document"; +static const char *kDocumentTargetsTable = "document_target"; +static const char *kRemoteDocumentsTable = "remote_document"; + +/** + * Labels for the components of keys. These serve to make keys self-describing. + * + * These are intended to sort similarly to keys in the server storage format. + * + * Note that the server writes component labels using the equivalent to + * OrderedCode::WriteSignedNumDecreasing. This means that despite the higher numeric value, a + * terminator sorts before a path segment. In order to avoid needing the WriteSignedNumDecreasing + * code just for these values, this enum's values are in the reverse order to the server side. + * + * Most server-side values don't apply here. For example, the server embeds projects, databases, + * namespaces and similar values in its entity keys where the clients just open a different + * leveldb. Similarly, many of these values don't apply to the server since the server is backed + * by spanner which natively has concepts of tables and indexes. Where there's overlap, a comment + * denotes the server value from the storage_format_internal.proto. + */ +typedef NS_ENUM(int64_t, FSTComponentLabel) { + /** + * A terminator is the final component of a key. All complete keys have a terminator and a key + * is known to be a key prefix if it doesn't have a terminator. + */ + FSTComponentLabelTerminator = 0, // TERMINATOR_COMPONENT = 63, server-side + + /** A table name component names the logical table to which the key belongs. */ + FSTComponentLabelTableName = 5, + + /** A component containing the batch ID of a mutation. */ + FSTComponentLabelBatchID = 10, + + /** A component containing the canonical ID of a query. */ + FSTComponentLabelCanonicalID = 11, + + /** A component containing the target ID of a query. */ + FSTComponentLabelTargetID = 12, + + /** A component containing a user ID. */ + FSTComponentLabelUserID = 13, + + /** + * A path segment describes just a single segment in a resource path. Path segments that occur + * sequentially in a key represent successive segments in a single path. + * + * This value must be greater than FSTComponentLabelTerminator to ensure that longer paths sort + * after paths that are prefixes of them. + * + * This value must also be larger than other separators so that path suffixes sort after other + * key components. + */ + FSTComponentLabelPathSegment = 62, // PATH = 60, server-side + + /** The maximum value that can be encoded by WriteSignedNumIncreasing in a single byte. */ + FSTComponentLabelUnknown = 63, +}; + +namespace { + +/** Writes a component label to the given key destination. */ +void WriteComponentLabel(std::string *dest, FSTComponentLabel label) { + OrderedCode::WriteSignedNumIncreasing(dest, label); +} + +/** + * Reads a component label from the given key contents. + * + * If the read is unsuccessful, returns NO, and changes none of its arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * label will be set to the decoded label value. + */ +BOOL ReadComponentLabel(leveldb::Slice *contents, FSTComponentLabel *label) { + int64_t rawResult = 0; + Slice tmp = *contents; + if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) { + if (rawResult >= FSTComponentLabelTerminator && rawResult <= FSTComponentLabelUnknown) { + *label = static_cast<FSTComponentLabel>(rawResult); + *contents = tmp; + return YES; + } + } + return NO; +} + +/** + * Reads a component label from the given key contents. + * + * If the read is unsuccessful or if the read was successful but the label that was read did not + * match the expectedLabel returns NO and changes none of its arguments. + * + * If the read is successful, returns YES and contents will be updated to the next unread byte. + */ +BOOL ReadComponentLabelMatching(Slice *contents, FSTComponentLabel expectedLabel) { + int64_t rawResult = 0; + Slice tmp = *contents; + if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) { + if (rawResult == expectedLabel) { + *contents = tmp; + return YES; + } + } + return NO; +} + +/** + * Reads a signed number from the given key contents and verifies that the value fits in a 32-bit + * integer. + * + * If the read is unsuccessful or the number that was read was out of bounds for an int32_t, + * returns NO, and changes none of its arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * result will be set to the decoded integer value. + */ +BOOL ReadInt32(Slice *contents, int32_t *result) { + int64_t rawResult = 0; + Slice tmp = *contents; + if (OrderedCode::ReadSignedNumIncreasing(&tmp, &rawResult)) { + if (rawResult >= INT32_MIN && rawResult <= INT32_MAX) { + *contents = tmp; + *result = static_cast<int32_t>(rawResult); + return YES; + } + } + return NO; +} + +/** Writes a component label and a signed integer to the given key destination. */ +void WriteLabeledInt32(std::string *dest, FSTComponentLabel label, int32_t value) { + WriteComponentLabel(dest, label); + OrderedCode::WriteSignedNumIncreasing(dest, value); +} + +/** + * Reads a component label and signed number from the given key contents and verifies that the + * label matches the expectedLabel and the value fits in a 32-bit integer. + * + * If the read is unsuccessful, the label didn't match, or the number that was read was out of + * bounds for an int32_t, returns NO, and changes none of its arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * value will be set to the decoded integer value. + */ +BOOL ReadLabeledInt32(Slice *contents, FSTComponentLabel expectedLabel, int32_t *value) { + Slice tmp = *contents; + if (ReadComponentLabelMatching(&tmp, expectedLabel)) { + if (ReadInt32(&tmp, value)) { + *contents = tmp; + return YES; + } + } + return NO; +} + +/** Writes a component label and an encoded string to the given key destination. */ +void WriteLabeledString(std::string *dest, FSTComponentLabel label, StringView value) { + WriteComponentLabel(dest, label); + OrderedCode::WriteString(dest, value); +} + +/** + * Reads a component label and a string from the given key contents and verifies that the label + * matches the expectedLabel. + * + * If the read is unsuccessful or the label didn't match, returns NO, and changes none of its + * arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * value will be set to the decoded string value. + */ +BOOL ReadLabeledString(Slice *contents, FSTComponentLabel expectedLabel, std::string *value) { + Slice tmp = *contents; + if (ReadComponentLabelMatching(&tmp, expectedLabel)) { + if (OrderedCode::ReadString(&tmp, value)) { + *contents = tmp; + return YES; + } + } + + return NO; +} + +/** + * Reads a component label and a string from the given key contents and verifies that the label + * matches the expectedLabel and the string matches the expectedValue. + * + * If the read is unsuccessful, the label or didn't match, or the string value didn't match, + * returns NO, and changes none of its arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte. + */ +BOOL ReadLabeledStringMatching(Slice *contents, + FSTComponentLabel expectedLabel, + const char *expectedValue) { + std::string value; + Slice tmp = *contents; + if (ReadLabeledString(&tmp, expectedLabel, &value)) { + if (value == expectedValue) { + *contents = tmp; + return YES; + } + } + + return NO; +} + +/** + * For each segment in the given resource path writes an FSTComponentLabelPathSegment component + * label and a string containing the path segment. + */ +void WriteResourcePath(std::string *dest, FSTResourcePath *path) { + for (int i = 0; i < path.length; i++) { + WriteComponentLabel(dest, FSTComponentLabelPathSegment); + OrderedCode::WriteString(dest, StringView([path segmentAtIndex:i])); + } +} + +/** + * Reads component labels and strings from the given key contents until it finds a component label + * other that FSTComponentLabelPathSegment. All matched path segments are assembled into a resource + * path and wrapped in an FSTDocumentKey. + * + * If the read is unsuccessful or the document key is invalid, returns NO, and changes none of its + * arguments. + * + * If the read is successful, returns YES, contents will be updated to the next unread byte, and + * value will be set to the decoded document key. + */ +BOOL ReadDocumentKey(Slice *contents, FSTDocumentKey *__strong *result) { + Slice completeSegments = *contents; + + std::string segment; + NSMutableArray<NSString *> *pathSegments = [NSMutableArray array]; + for (;;) { + // Advance a temporary slice to avoid advancing contents into the next key component which may + // not be a path segment. + Slice readPosition = completeSegments; + if (!ReadComponentLabelMatching(&readPosition, FSTComponentLabelPathSegment)) { + break; + } + if (!OrderedCode::ReadString(&readPosition, &segment)) { + return NO; + } + + NSString *pathSegment = [[NSString alloc] initWithUTF8String:segment.c_str()]; + [pathSegments addObject:pathSegment]; + segment.clear(); + + completeSegments = readPosition; + } + + FSTResourcePath *path = [FSTResourcePath pathWithSegments:pathSegments]; + if (path.length > 0 && [FSTDocumentKey isDocumentKey:path]) { + *contents = completeSegments; + *result = [FSTDocumentKey keyWithPath:path]; + return YES; + } + + return NO; +} + +// Trivial shortcuts that make reading and writing components type-safe. + +inline void WriteTerminator(std::string *dest) { + OrderedCode::WriteSignedNumIncreasing(dest, FSTComponentLabelTerminator); +} + +inline BOOL ReadTerminator(Slice *contents) { + return ReadComponentLabelMatching(contents, FSTComponentLabelTerminator); +} + +inline void WriteTableName(std::string *dest, const char *tableName) { + WriteLabeledString(dest, FSTComponentLabelTableName, tableName); +} + +inline BOOL ReadTableNameMatching(Slice *contents, const char *expectedTableName) { + return ReadLabeledStringMatching(contents, FSTComponentLabelTableName, expectedTableName); +} + +inline void WriteBatchID(std::string *dest, FSTBatchID batchID) { + WriteLabeledInt32(dest, FSTComponentLabelBatchID, batchID); +} + +inline BOOL ReadBatchID(Slice *contents, FSTBatchID *batchID) { + return ReadLabeledInt32(contents, FSTComponentLabelBatchID, batchID); +} + +inline void WriteCanonicalID(std::string *dest, StringView canonicalID) { + WriteLabeledString(dest, FSTComponentLabelCanonicalID, canonicalID); +} + +inline BOOL ReadCanonicalID(Slice *contents, std::string *canonicalID) { + return ReadLabeledString(contents, FSTComponentLabelCanonicalID, canonicalID); +} + +inline void WriteTargetID(std::string *dest, FSTTargetID targetID) { + WriteLabeledInt32(dest, FSTComponentLabelTargetID, targetID); +} + +inline BOOL ReadTargetID(Slice *contents, FSTTargetID *targetID) { + return ReadLabeledInt32(contents, FSTComponentLabelTargetID, targetID); +} + +inline void WriteUserID(std::string *dest, StringView userID) { + WriteLabeledString(dest, FSTComponentLabelUserID, userID); +} + +inline BOOL ReadUserID(Slice *contents, std::string *userID) { + return ReadLabeledString(contents, FSTComponentLabelUserID, userID); +} + +/** Returns a base64-encoded string for an invalid key, used for debug-friendly description text. */ +NSString *InvalidKey(const Slice &key) { + NSData *keyData = + [[NSData alloc] initWithBytesNoCopy:(void *)key.data() length:key.size() freeWhenDone:NO]; + return [keyData base64EncodedStringWithOptions:0]; +} + +} // namespace + +@implementation FSTLevelDBKey + ++ (NSString *)descriptionForKey:(StringView)key { + Slice contents = key; + BOOL isTerminated = NO; + + NSMutableString *description = [NSMutableString string]; + [description appendString:@"["]; + while (contents.size() > 0) { + Slice tmp = contents; + FSTComponentLabel label = FSTComponentLabelUnknown; + if (!ReadComponentLabel(&tmp, &label)) { + break; + } + + if (label == FSTComponentLabelTerminator) { + isTerminated = YES; + contents = tmp; + break; + } + + // Reset tmp since all the different read routines expect to see the separator first + tmp = contents; + + if (label == FSTComponentLabelPathSegment) { + FSTDocumentKey *documentKey = nil; + if (!ReadDocumentKey(&tmp, &documentKey)) { + break; + } + [description appendFormat:@" key=%@", [documentKey.path description]]; + + } else if (label == FSTComponentLabelTableName) { + std::string table; + if (!ReadLabeledString(&tmp, FSTComponentLabelTableName, &table)) { + break; + } + [description appendFormat:@"%s:", table.c_str()]; + + } else if (label == FSTComponentLabelBatchID) { + FSTBatchID batchID; + if (!ReadBatchID(&tmp, &batchID)) { + break; + } + [description appendFormat:@" batchID=%d", batchID]; + + } else if (label == FSTComponentLabelCanonicalID) { + std::string canonicalID; + if (!ReadCanonicalID(&tmp, &canonicalID)) { + break; + } + [description appendFormat:@" canonicalID=%s", canonicalID.c_str()]; + + } else if (label == FSTComponentLabelTargetID) { + FSTTargetID targetID; + if (!ReadTargetID(&tmp, &targetID)) { + break; + } + [description appendFormat:@" targetID=%d", targetID]; + + } else if (label == FSTComponentLabelUserID) { + std::string userID; + if (!ReadUserID(&tmp, &userID)) { + break; + } + [description appendFormat:@" userID=%s", userID.c_str()]; + + } else { + [description appendFormat:@" unknown label=%d", (int)label]; + break; + } + + contents = tmp; + } + + if (contents.size() > 0) { + [description appendFormat:@" invalid key=<%@>", InvalidKey(key)]; + + } else if (!isTerminated) { + [description appendFormat:@" incomplete key"]; + } + + [description appendString:@"]"]; + return description; +} + +@end + +@implementation FSTLevelDBMutationKey { + std::string _userID; +} + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kMutationsTable); + return result; +} + ++ (std::string)keyPrefixWithUserID:(StringView)userID { + std::string result; + WriteTableName(&result, kMutationsTable); + WriteUserID(&result, userID); + return result; +} + ++ (std::string)keyWithUserID:(StringView)userID batchID:(FSTBatchID)batchID { + std::string result; + WriteTableName(&result, kMutationsTable); + WriteUserID(&result, userID); + WriteBatchID(&result, batchID); + WriteTerminator(&result); + return result; +} + +- (const std::string &)userID { + return _userID; +} + +- (BOOL)decodeKey:(StringView)key { + _userID.clear(); + + Slice contents = key; + return ReadTableNameMatching(&contents, kMutationsTable) && ReadUserID(&contents, &_userID) && + ReadBatchID(&contents, &_batchID) && ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBDocumentMutationKey { + std::string _userID; +} + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kDocumentMutationsTable); + return result; +} + ++ (std::string)keyPrefixWithUserID:(StringView)userID { + std::string result; + WriteTableName(&result, kDocumentMutationsTable); + WriteUserID(&result, userID); + return result; +} + ++ (std::string)keyPrefixWithUserID:(StringView)userID resourcePath:(FSTResourcePath *)resourcePath { + std::string result; + WriteTableName(&result, kDocumentMutationsTable); + WriteUserID(&result, userID); + WriteResourcePath(&result, resourcePath); + return result; +} + ++ (std::string)keyWithUserID:(StringView)userID + documentKey:(FSTDocumentKey *)documentKey + batchID:(FSTBatchID)batchID { + std::string result; + WriteTableName(&result, kDocumentMutationsTable); + WriteUserID(&result, userID); + WriteResourcePath(&result, documentKey.path); + WriteBatchID(&result, batchID); + WriteTerminator(&result); + return result; +} + +- (const std::string &)userID { + return _userID; +} + +- (BOOL)decodeKey:(StringView)key { + _userID.clear(); + _documentKey = nil; + + Slice contents = key; + return ReadTableNameMatching(&contents, kDocumentMutationsTable) && + ReadUserID(&contents, &_userID) && ReadDocumentKey(&contents, &_documentKey) && + ReadBatchID(&contents, &_batchID) && ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBMutationQueueKey { + std::string _userID; +} + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kMutationQueuesTable); + return result; +} + ++ (std::string)keyWithUserID:(StringView)userID { + std::string result; + WriteTableName(&result, kMutationQueuesTable); + WriteUserID(&result, userID); + WriteTerminator(&result); + return result; +} + +- (const std::string &)userID { + return _userID; +} + +- (BOOL)decodeKey:(StringView)key { + _userID.clear(); + + Slice contents = key; + return ReadTableNameMatching(&contents, kMutationQueuesTable) && + ReadUserID(&contents, &_userID) && ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBTargetGlobalKey + ++ (std::string)key { + std::string result; + WriteTableName(&result, kTargetGlobalTable); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(StringView)key { + Slice contents = key; + return ReadTableNameMatching(&contents, kTargetGlobalTable) && ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBTargetKey + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kTargetsTable); + return result; +} + ++ (std::string)keyWithTargetID:(FSTTargetID)targetID { + std::string result; + WriteTableName(&result, kTargetsTable); + WriteTargetID(&result, targetID); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(StringView)key { + Slice contents = key; + return ReadTableNameMatching(&contents, kTargetsTable) && ReadTargetID(&contents, &_targetID) && + ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBQueryTargetKey { + std::string _canonicalID; +} + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kQueryTargetsTable); + return result; +} + ++ (std::string)keyPrefixWithCanonicalID:(StringView)canonicalID { + std::string result; + WriteTableName(&result, kQueryTargetsTable); + WriteCanonicalID(&result, canonicalID); + return result; +} + ++ (std::string)keyWithCanonicalID:(StringView)canonicalID targetID:(FSTTargetID)targetID { + std::string result; + WriteTableName(&result, kQueryTargetsTable); + WriteCanonicalID(&result, canonicalID); + WriteTargetID(&result, targetID); + WriteTerminator(&result); + return result; +} + +- (const std::string &)canonicalID { + return _canonicalID; +} + +- (BOOL)decodeKey:(StringView)key { + _canonicalID.clear(); + + Slice contents = key; + return ReadTableNameMatching(&contents, kQueryTargetsTable) && + ReadCanonicalID(&contents, &_canonicalID) && ReadTargetID(&contents, &_targetID) && + ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBTargetDocumentKey + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kTargetDocumentsTable); + return result; +} + ++ (std::string)keyPrefixWithTargetID:(FSTTargetID)targetID { + std::string result; + WriteTableName(&result, kTargetDocumentsTable); + WriteTargetID(&result, targetID); + return result; +} + ++ (std::string)keyWithTargetID:(FSTTargetID)targetID documentKey:(FSTDocumentKey *)documentKey { + std::string result; + WriteTableName(&result, kTargetDocumentsTable); + WriteTargetID(&result, targetID); + WriteResourcePath(&result, documentKey.path); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(Firestore::StringView)key { + _documentKey = nil; + + leveldb::Slice contents = key; + return ReadTableNameMatching(&contents, kTargetDocumentsTable) && + ReadTargetID(&contents, &_targetID) && ReadDocumentKey(&contents, &_documentKey) && + ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBDocumentTargetKey + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kDocumentTargetsTable); + return result; +} + ++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)resourcePath { + std::string result; + WriteTableName(&result, kDocumentTargetsTable); + WriteResourcePath(&result, resourcePath); + return result; +} + ++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)documentKey targetID:(FSTTargetID)targetID { + std::string result; + WriteTableName(&result, kDocumentTargetsTable); + WriteResourcePath(&result, documentKey.path); + WriteTargetID(&result, targetID); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(Firestore::StringView)key { + _documentKey = nil; + + leveldb::Slice contents = key; + return ReadTableNameMatching(&contents, kDocumentTargetsTable) && + ReadDocumentKey(&contents, &_documentKey) && ReadTargetID(&contents, &_targetID) && + ReadTerminator(&contents); +} + +@end + +@implementation FSTLevelDBRemoteDocumentKey + ++ (std::string)keyPrefix { + std::string result; + WriteTableName(&result, kRemoteDocumentsTable); + return result; +} + ++ (std::string)keyPrefixWithResourcePath:(FSTResourcePath *)path { + std::string result; + WriteTableName(&result, kRemoteDocumentsTable); + WriteResourcePath(&result, path); + return result; +} + ++ (std::string)keyWithDocumentKey:(FSTDocumentKey *)key { + std::string result; + WriteTableName(&result, kRemoteDocumentsTable); + WriteResourcePath(&result, key.path); + WriteTerminator(&result); + return result; +} + +- (BOOL)decodeKey:(StringView)key { + _documentKey = nil; + + Slice contents = key; + return ReadTableNameMatching(&contents, kRemoteDocumentsTable) && + ReadDocumentKey(&contents, &_documentKey) && ReadTerminator(&contents); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.h b/Firestore/Source/Local/FSTLevelDBMutationQueue.h new file mode 100644 index 0000000..c9b5166 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.h @@ -0,0 +1,64 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTMutationQueue.h" + +#ifdef __cplusplus +#include <memory> + +namespace leveldb { +class DB; +} +#endif + +@class FSTLevelDB; +@class FSTLocalSerializer; +@class FSTUser; +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +/** A mutation queue for a specific user, backed by LevelDB. */ +@interface FSTLevelDBMutationQueue : NSObject <FSTMutationQueue> + +- (instancetype)init __attribute__((unavailable("Use a static constructor"))); + +/** The garbage collector to notify about potential garbage keys. */ +@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector; + +#ifdef __cplusplus +/** + * Creates a new mutation queue for the given user, in the given LevelDB. + * + * @param user The user for which to create a mutation queue. + * @param db The LevelDB in which to create the queue. + */ ++ (instancetype)mutationQueueWithUser:(FSTUser *)user + db:(std::shared_ptr<leveldb::DB>)db + serializer:(FSTLocalSerializer *)serializer; + +/** + * Returns one larger than the largest batch ID that has been stored. If there are no mutations + * returns 0. Note that batch IDs are global. + */ ++ (FSTBatchID)loadNextBatchIDFromDB:(std::shared_ptr<leveldb::DB>)db; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBMutationQueue.mm b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm new file mode 100644 index 0000000..d57a15d --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBMutationQueue.mm @@ -0,0 +1,637 @@ +/* + * 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 "FSTLevelDBMutationQueue.h" + +#include <leveldb/db.h> +#include <leveldb/write_batch.h> +#include <set> +#include <string> + +#import "Mutation.pbobjc.h" +#import "FSTUser.h" +#import "FSTQuery.h" +#import "FSTLevelDB.h" +#import "FSTLevelDBKey.h" +#import "FSTLocalSerializer.h" +#import "FSTWriteGroup.h" +#import "FSTDocumentKey.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTPath.h" +#import "FSTAssert.h" + +#include "ordered_code.h" +#include "string_util.h" + +NS_ASSUME_NONNULL_BEGIN + +using Firestore::OrderedCode; +using Firestore::StringView; +using leveldb::DB; +using leveldb::Iterator; +using leveldb::ReadOptions; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteBatch; +using leveldb::WriteOptions; + +@interface FSTLevelDBMutationQueue () + +- (instancetype)initWithUserID:(NSString *)userID + db:(std::shared_ptr<DB>)db + serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; + +/** The normalized userID (e.g. nil UID => @"" userID) used in our LevelDB keys. */ +@property(nonatomic, strong, readonly) NSString *userID; + +/** + * Next value to use when assigning sequential IDs to each mutation batch. + * + * NOTE: There can only be one FSTLevelDBMutationQueue for a given db at a time, hence it is safe + * to track nextBatchID as an instance-level property. Should we ever relax this constraint we'll + * need to revisit this. + */ +@property(nonatomic, assign) FSTBatchID nextBatchID; + +/** A write-through cache copy of the metadata describing the current queue. */ +@property(nonatomic, strong, nullable) FSTPBMutationQueue *metadata; + +@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + +@end + +/** + * Returns a standard set of read options. + * + * For now this is paranoid, but perhaps disable that in production builds. + */ +static ReadOptions StandardReadOptions() { + ReadOptions options; + options.verify_checksums = true; + return options; +} + +@implementation FSTLevelDBMutationQueue { + // The DB pointer is shared with all cooperating LevelDB-related objects. + std::shared_ptr<DB> _db; +} + ++ (instancetype)mutationQueueWithUser:(FSTUser *)user + db:(std::shared_ptr<DB>)db + serializer:(FSTLocalSerializer *)serializer { + FSTAssert(![user.UID isEqual:@""], @"UserID must not be an empty string."); + NSString *userID = user.isUnauthenticated ? @"" : user.UID; + + return [[FSTLevelDBMutationQueue alloc] initWithUserID:userID db:db serializer:serializer]; +} + +- (instancetype)initWithUserID:(NSString *)userID + db:(std::shared_ptr<DB>)db + serializer:(FSTLocalSerializer *)serializer { + if (self = [super init]) { + _userID = userID; + _db = db; + _serializer = serializer; + } + return self; +} + +- (void)startWithGroup:(FSTWriteGroup *)group { + FSTBatchID nextBatchID = [FSTLevelDBMutationQueue loadNextBatchIDFromDB:_db]; + + // On restart, nextBatchId may end up lower than lastAcknowledgedBatchId since it's computed from + // the queue contents, and there may be no mutations in the queue. In this case, we need to reset + // lastAcknowledgedBatchId (which is safe since the queue must be empty). + std::string key = [self keyForCurrentMutationQueue]; + FSTPBMutationQueue *metadata = [self metadataForKey:key]; + if (!metadata) { + metadata = [FSTPBMutationQueue message]; + + // proto3's default value for lastAcknowledgedBatchId is zero, but that would consider the first + // entry in the queue to be acknowledged without that acknowledgement actually happening. + metadata.lastAcknowledgedBatchId = kFSTBatchIDUnknown; + } else { + FSTBatchID lastAcked = metadata.lastAcknowledgedBatchId; + if (lastAcked >= nextBatchID) { + FSTAssert([self isEmpty], @"Reset nextBatchID is only possible when the queue is empty"); + lastAcked = kFSTBatchIDUnknown; + + metadata.lastAcknowledgedBatchId = lastAcked; + [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]]; + } + } + + self.nextBatchID = nextBatchID; + self.metadata = metadata; +} + +- (void)shutdown { + _db.reset(); +} + ++ (FSTBatchID)loadNextBatchIDFromDB:(std::shared_ptr<DB>)db { + std::unique_ptr<Iterator> it(db->NewIterator(StandardReadOptions())); + + auto tableKey = [FSTLevelDBMutationKey keyPrefix]; + + FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init]; + FSTBatchID maxBatchID = kFSTBatchIDUnknown; + + BOOL moreUserIDs = NO; + std::string nextUserID; + + it->Seek(tableKey); + if (it->Valid() && [rowKey decodeKey:it->key()]) { + moreUserIDs = YES; + nextUserID = rowKey.userID; + } + + // This loop assumes that nextUserId contains the next username at the start of the iteration. + while (moreUserIDs) { + // Compute the first key after the last mutation for nextUserID. + auto userEnd = [FSTLevelDBMutationKey keyPrefixWithUserID:nextUserID]; + userEnd = Firestore::PrefixSuccessor(userEnd); + + // Seek to that key with the intent of finding the boundary between nextUserID's mutations + // and the one after that (if any). + it->Seek(userEnd); + + // At this point there are three possible cases to handle differently. Each case must prepare + // the next iteration (by assigning to nextUserID or setting moreUserIDs = NO) and seek the + // iterator to the last row in the current user's mutation sequence. + if (!it->Valid()) { + // The iterator is past the last row altogether (there are no additional userIDs and now + // rows in any table after mutations). The last row will have the highest batchID. + moreUserIDs = NO; + it->SeekToLast(); + + } else if ([rowKey decodeKey:it->key()]) { + // The iterator is valid and the key decoded successfully so the next user was just decoded. + nextUserID = rowKey.userID; + it->Prev(); + + } else { + // The iterator is past the end of the mutations table but there are other rows. + moreUserIDs = NO; + it->Prev(); + } + + // In all the cases above there was at least one row for the current user and each case has + // set things up such that iterator points to it. + if (![rowKey decodeKey:it->key()]) { + FSTFail(@"There should have been a key previous to %s", userEnd.c_str()); + } + + if (rowKey.batchID > maxBatchID) { + maxBatchID = rowKey.batchID; + } + } + + return maxBatchID + 1; +} + +- (BOOL)isEmpty { + std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID]; + + std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions())); + it->Seek(userKey); + + BOOL empty = YES; + if (it->Valid() && it->key().starts_with(userKey)) { + empty = NO; + } + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"isEmpty failed with status: %s", status.ToString().c_str()); + } + + return empty; +} + +- (FSTBatchID)highestAcknowledgedBatchID { + return self.metadata.lastAcknowledgedBatchId; +} + +- (void)acknowledgeBatch:(FSTMutationBatch *)batch + streamToken:(nullable NSData *)streamToken + group:(FSTWriteGroup *)group { + FSTBatchID batchID = batch.batchID; + FSTAssert(batchID > self.highestAcknowledgedBatchID, + @"Mutation batchIDs must be acknowledged in order"); + + FSTPBMutationQueue *metadata = self.metadata; + metadata.lastAcknowledgedBatchId = batchID; + metadata.lastStreamToken = streamToken; + + [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]]; +} + +- (nullable NSData *)lastStreamToken { + return self.metadata.lastStreamToken; +} + +- (void)setLastStreamToken:(nullable NSData *)streamToken group:(FSTWriteGroup *)group { + FSTPBMutationQueue *metadata = self.metadata; + metadata.lastStreamToken = streamToken; + + [group setMessage:metadata forKey:[self keyForCurrentMutationQueue]]; +} + +- (std::string)keyForCurrentMutationQueue { + return [FSTLevelDBMutationQueueKey keyWithUserID:self.userID]; +} + +- (nullable FSTPBMutationQueue *)metadataForKey:(const std::string &)key { + std::string value; + Status status = _db->Get(StandardReadOptions(), key, &value); + if (status.ok()) { + return [self parsedMetadata:value]; + } else if (status.IsNotFound()) { + return nil; + } else { + FSTFail(@"metadataForKey: failed loading key %s with status: %s", key.c_str(), + status.ToString().c_str()); + } +} + +- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray<FSTMutation *> *)mutations + group:(FSTWriteGroup *)group { + FSTBatchID batchID = self.nextBatchID; + self.nextBatchID += 1; + + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID + localWriteTime:localWriteTime + mutations:mutations]; + std::string key = [self mutationKeyForBatch:batch]; + [group setMessage:[self.serializer encodedMutationBatch:batch] forKey:key]; + + NSString *userID = self.userID; + + // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the + // future if we wanted to store some other kind of value here, we can parse these empty values as + // with some other protocol buffer (and the parser will see all default values). + std::string emptyBuffer; + + for (FSTMutation *mutation in mutations) { + key = [FSTLevelDBDocumentMutationKey keyWithUserID:userID + documentKey:mutation.key + batchID:batchID]; + [group setData:emptyBuffer forKey:key]; + } + + return batch; +} + +- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID { + std::string key = [self mutationKeyForBatchID:batchID]; + + std::string value; + Status status = _db->Get(StandardReadOptions(), key, &value); + if (!status.ok()) { + if (status.IsNotFound()) { + return nil; + } + FSTFail(@"Lookup mutation batch (%@, %d) failed with status: %s", self.userID, batchID, + status.ToString().c_str()); + } + + return [self decodedMutationBatch:value]; +} + +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID { + std::string key = [self mutationKeyForBatchID:batchID + 1]; + std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions())); + it->Seek(key); + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"Seek to mutation batch (%@, %d) failed with status: %s", self.userID, batchID, + status.ToString().c_str()); + } + + FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init]; + if (!it->Valid() || ![rowKey decodeKey:it->key()]) { + // Past the last row in the DB or out of the mutations table + return nil; + } + + if (rowKey.userID != [self.userID UTF8String]) { + // Jumped past the last mutation for this user + return nil; + } + + FSTAssert(rowKey.batchID > batchID, @"Should have found mutation after %d", batchID); + return [self decodedMutationBatch:it->value()]; +} + +- (NSArray<FSTMutationBatch *> *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID { + std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID]; + const char *userID = [self.userID UTF8String]; + + std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions())); + it->Seek(userKey); + + NSMutableArray *result = [NSMutableArray array]; + FSTLevelDBMutationKey *rowKey = [[FSTLevelDBMutationKey alloc] init]; + for (; it->Valid() && [rowKey decodeKey:it->key()]; it->Next()) { + if (rowKey.userID != userID) { + // End of this user's mutations + break; + } else if (rowKey.batchID > batchID) { + // This mutation is past what we're looking for + break; + } + + [result addObject:[self decodedMutationBatch:it->value()]]; + } + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"Find all mutations through mutation batch (%@, %d) failed with status: %s", + self.userID, batchID, status.ToString().c_str()); + } + + return result; +} + +- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKey: + (FSTDocumentKey *)documentKey { + NSString *userID = self.userID; + + // Scan the document-mutation index starting with a prefix starting with the given documentKey. + std::string indexPrefix = + [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:documentKey.path]; + std::unique_ptr<Iterator> indexIterator(_db->NewIterator(StandardReadOptions())); + indexIterator->Seek(indexPrefix); + + // Simultaneously scan the mutation queue. This works because each (key, batchID) pair is unique + // and ordered, so when scanning a table prefixed by exactly key, all the batchIDs encountered + // will be unique and in order. + std::string mutationsPrefix = [FSTLevelDBMutationKey keyPrefixWithUserID:userID]; + std::unique_ptr<Iterator> mutationIterator(_db->NewIterator(StandardReadOptions())); + + NSMutableArray *result = [NSMutableArray array]; + FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init]; + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + // Only consider rows matching exactly the specific key of interest. Note that because we order + // by path first, and we order terminators before path separators, we'll encounter all the + // index rows for documentKey contiguously. In particular, all the rows for documentKey will + // occur before any rows for documents nested in a subcollection beneath documentKey so we can + // stop as soon as we hit any such row. + if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey] || + ![rowKey.documentKey isEqualToKey:documentKey]) { + break; + } + + // Each row is a unique combination of key and batchID, so this foreign key reference can + // only occur once. + std::string mutationKey = [FSTLevelDBMutationKey keyWithUserID:userID batchID:rowKey.batchID]; + mutationIterator->Seek(mutationKey); + if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) { + NSString *foundKeyDescription = @"the end of the table"; + if (mutationIterator->Valid()) { + foundKeyDescription = [FSTLevelDBKey descriptionForKey:mutationIterator->key()]; + } + FSTFail(@"Dangling document-mutation reference found: " + @"%@ points to %@; seeking there found %@", + [FSTLevelDBKey descriptionForKey:indexKey], + [FSTLevelDBKey descriptionForKey:mutationKey], foundKeyDescription); + } + + [result addObject:[self decodedMutationBatch:mutationIterator->value()]]; + } + return result; +} + +- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingQuery:(FSTQuery *)query { + FSTAssert(![query isDocumentQuery], @"Document queries shouldn't go down this path"); + NSString *userID = self.userID; + + FSTResourcePath *queryPath = query.path; + int immediateChildrenPathLength = queryPath.length + 1; + + // TODO(mcg): Actually implement a single-collection query + // + // This is actually executing an ancestor query, traversing the whole subtree below the + // collection which can be horrifically inefficient for some structures. The right way to + // solve this is to implement the full value index, but that's not in the cards in the near + // future so this is the best we can do for the moment. + // + // Since we don't yet index the actual properties in the mutations, our current approach is to + // just return all mutation batches that affect documents in the collection being queried. + // + // Unlike allMutationBatchesAffectingDocumentKey, this iteration will scan the document-mutation + // index for more than a single document so the associated batchIDs will be neither necessarily + // unique nor in order. This means an efficient simultaneous scan isn't possible. + std::string indexPrefix = + [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:queryPath]; + std::unique_ptr<Iterator> indexIterator(_db->NewIterator(StandardReadOptions())); + indexIterator->Seek(indexPrefix); + + NSMutableArray *result = [NSMutableArray array]; + FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init]; + + // Collect up unique batchIDs encountered during a scan of the index. Use a set<FSTBatchID> to + // accumulate batch IDs so they can be traversed in order in a scan of the main table. + // + // This method is faster than performing lookups of the keys with _db->Get and keeping a hash of + // batchIDs that have already been looked up. The performance difference is minor for small + // numbers of keys but > 30% faster for larger numbers of keys. + std::set<FSTBatchID> uniqueBatchIds; + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey]) { + break; + } + + // Rows with document keys more than one segment longer than the query path can't be matches. + // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx. + // TODO(mcg): we'll need a different scanner when we implement ancestor queries. + if (rowKey.documentKey.path.length != immediateChildrenPathLength) { + continue; + } + + uniqueBatchIds.insert(rowKey.batchID); + } + + // Given an ordered set of unique batchIDs perform a skipping scan over the main table to find + // the mutation batches. + std::unique_ptr<Iterator> mutationIterator(_db->NewIterator(StandardReadOptions())); + + for (FSTBatchID batchID : uniqueBatchIds) { + std::string mutationKey = [FSTLevelDBMutationKey keyWithUserID:userID batchID:batchID]; + mutationIterator->Seek(mutationKey); + if (!mutationIterator->Valid() || mutationIterator->key() != mutationKey) { + NSString *foundKeyDescription = @"the end of the table"; + if (mutationIterator->Valid()) { + foundKeyDescription = [FSTLevelDBKey descriptionForKey:mutationIterator->key()]; + } + FSTFail(@"Dangling document-mutation reference found: " + @"Missing batch %@; seeking there found %@", + [FSTLevelDBKey descriptionForKey:mutationKey], foundKeyDescription); + } + + [result addObject:[self decodedMutationBatch:mutationIterator->value()]]; + } + return result; +} + +- (NSArray<FSTMutationBatch *> *)allMutationBatches { + std::string userKey = [FSTLevelDBMutationKey keyPrefixWithUserID:self.userID]; + + std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions())); + it->Seek(userKey); + + NSMutableArray *result = [NSMutableArray array]; + for (; it->Valid() && it->key().starts_with(userKey); it->Next()) { + [result addObject:[self decodedMutationBatch:it->value()]]; + } + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"Find all mutation batches failed with status: %s", status.ToString().c_str()); + } + + return result; +} + +- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches group:(FSTWriteGroup *)group { + NSString *userID = self.userID; + id<FSTGarbageCollector> garbageCollector = self.garbageCollector; + + std::unique_ptr<Iterator> checkIterator(_db->NewIterator(StandardReadOptions())); + + for (FSTMutationBatch *batch in batches) { + FSTBatchID batchID = batch.batchID; + std::string key = [FSTLevelDBMutationKey keyWithUserID:userID batchID:batchID]; + + // As a sanity check, verify that the mutation batch exists before deleting it. + checkIterator->Seek(key); + FSTAssert(checkIterator->Valid(), @"Mutation batch %@ did not exist", + [FSTLevelDBKey descriptionForKey:key]); + + FSTAssert(key == checkIterator->key(), @"Mutation batch %@ not found; found %@", + [FSTLevelDBKey descriptionForKey:key], + [FSTLevelDBKey descriptionForKey:checkIterator->key()]); + + [group removeMessageForKey:key]; + + for (FSTMutation *mutation in batch.mutations) { + key = [FSTLevelDBDocumentMutationKey keyWithUserID:userID + documentKey:mutation.key + batchID:batchID]; + [group removeMessageForKey:key]; + [garbageCollector addPotentialGarbageKey:mutation.key]; + } + } +} + +- (void)performConsistencyCheck { + if (![self isEmpty]) { + return; + } + + // Verify that there are no entries in the document-mutation index if the queue is empty. + std::string indexPrefix = [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID]; + std::unique_ptr<Iterator> indexIterator(_db->NewIterator(StandardReadOptions())); + indexIterator->Seek(indexPrefix); + + NSMutableArray<NSString *> *danglingMutationReferences = [NSMutableArray array]; + + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + // Only consider rows matching this index prefix for the current user. + if (!indexKey.starts_with(indexPrefix)) { + break; + } + + [danglingMutationReferences addObject:[FSTLevelDBKey descriptionForKey:indexKey]]; + } + + FSTAssert(danglingMutationReferences.count == 0, + @"Document leak -- detected dangling mutation references when queue " + @"is empty. Dangling keys: %@", + danglingMutationReferences); +} + +- (std::string)mutationKeyForBatch:(FSTMutationBatch *)batch { + return [FSTLevelDBMutationKey keyWithUserID:self.userID batchID:batch.batchID]; +} + +- (std::string)mutationKeyForBatchID:(FSTBatchID)batchID { + return [FSTLevelDBMutationKey keyWithUserID:self.userID batchID:batchID]; +} + +/** Parses the MutationQueue metadata from the given LevelDB row contents. */ +- (FSTPBMutationQueue *)parsedMetadata:(Slice)slice { + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO]; + + NSError *error; + FSTPBMutationQueue *proto = [FSTPBMutationQueue parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBMutationQueue failed to parse: %@", error); + } + + return proto; +} + +- (FSTMutationBatch *)decodedMutationBatch:(Slice)slice { + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO]; + + NSError *error; + FSTPBWriteBatch *proto = [FSTPBWriteBatch parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBMutationBatch failed to parse: %@", error); + } + + return [self.serializer decodedMutationBatch:proto]; +} + +#pragma mark - FSTGarbageSource implementation + +- (BOOL)containsKey:(FSTDocumentKey *)documentKey { + std::string indexPrefix = + [FSTLevelDBDocumentMutationKey keyPrefixWithUserID:self.userID resourcePath:documentKey.path]; + std::unique_ptr<Iterator> indexIterator(_db->NewIterator(StandardReadOptions())); + indexIterator->Seek(indexPrefix); + + if (indexIterator->Valid()) { + FSTLevelDBDocumentMutationKey *rowKey = [[FSTLevelDBDocumentMutationKey alloc] init]; + Slice iteratorKey = indexIterator->key(); + + // Check both that the key prefix matches and that the decoded document key is exactly the key + // we're looking for. + if (iteratorKey.starts_with(indexPrefix) && [rowKey decodeKey:iteratorKey] && + [rowKey.documentKey isEqualToKey:documentKey]) { + return YES; + } + } + + return NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.h b/Firestore/Source/Local/FSTLevelDBQueryCache.h new file mode 100644 index 0000000..3f24e6a --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBQueryCache.h @@ -0,0 +1,54 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTQueryCache.h" + +#ifdef __cplusplus +#include <memory> + +namespace leveldb { +class DB; +} +#endif + +@class FSTLocalSerializer; +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +/** Cached Queries backed by LevelDB. */ +@interface FSTLevelDBQueryCache : NSObject <FSTQueryCache> + +- (instancetype)init NS_UNAVAILABLE; + +/** The garbage collector to notify about potential garbage keys. */ +@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector; + +#ifdef __cplusplus +/** + * Creates a new query cache in the given LevelDB. + * + * @param db The LevelDB in which to create the cache. + */ +- (instancetype)initWithDB:(std::shared_ptr<leveldb::DB>)db + serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBQueryCache.mm b/Firestore/Source/Local/FSTLevelDBQueryCache.mm new file mode 100644 index 0000000..c1ba654 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBQueryCache.mm @@ -0,0 +1,340 @@ +/* + * 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 "FSTLevelDBQueryCache.h" + +#include <leveldb/db.h> +#include <leveldb/write_batch.h> +#include <string> + +#import "Target.pbobjc.h" +#import "FSTQuery.h" +#import "FSTLevelDBKey.h" +#import "FSTLocalSerializer.h" +#import "FSTQueryData.h" +#import "FSTWriteGroup.h" +#import "FSTDocumentKey.h" +#import "FSTAssert.h" + +#include "ordered_code.h" +#include "string_util.h" + +NS_ASSUME_NONNULL_BEGIN + +using Firestore::OrderedCode; +using Firestore::StringView; +using leveldb::DB; +using leveldb::Iterator; +using leveldb::ReadOptions; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteOptions; + +/** + * Returns a standard set of read options. + * + * For now this is paranoid, but perhaps disable that in production builds. + */ +static ReadOptions GetStandardReadOptions() { + ReadOptions options; + options.verify_checksums = true; + return options; +} + +@interface FSTLevelDBQueryCache () + +/** A write-through cached copy of the metadata for the query cache. */ +@property(nonatomic, strong, nullable) FSTPBTargetGlobal *metadata; + +@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + +@end + +@implementation FSTLevelDBQueryCache { + // The DB pointer is shared with all cooperating LevelDB-related objects. + std::shared_ptr<DB> _db; + + /** + * The last received snapshot version. This is part of `metadata` but we store it separately to + * avoid extra conversion to/from GPBTimestamp. + */ + FSTSnapshotVersion *_lastRemoteSnapshotVersion; +} + +- (instancetype)initWithDB:(std::shared_ptr<DB>)db serializer:(FSTLocalSerializer *)serializer { + if (self = [super init]) { + FSTAssert(db, @"db must not be NULL"); + _db = db; + _serializer = serializer; + } + return self; +} + +- (void)start { + std::string key = [FSTLevelDBTargetGlobalKey key]; + FSTPBTargetGlobal *metadata = [self metadataForKey:key]; + if (!metadata) { + metadata = [FSTPBTargetGlobal message]; + } + _lastRemoteSnapshotVersion = [self.serializer decodedVersion:metadata.lastRemoteSnapshotVersion]; + + self.metadata = metadata; +} + +#pragma mark - FSTQueryCache implementation + +- (FSTTargetID)highestTargetID { + return self.metadata.highestTargetId; +} + +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { + return _lastRemoteSnapshotVersion; +} + +- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + group:(FSTWriteGroup *)group { + _lastRemoteSnapshotVersion = snapshotVersion; + self.metadata.lastRemoteSnapshotVersion = [self.serializer encodedVersion:snapshotVersion]; + [group setMessage:self.metadata forKey:[FSTLevelDBTargetGlobalKey key]]; +} + +- (void)shutdown { + _db.reset(); +} + +- (void)addQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group { + // TODO(mcg): actually populate listen sequence number + FSTTargetID targetID = queryData.targetID; + std::string key = [FSTLevelDBTargetKey keyWithTargetID:targetID]; + [group setMessage:[self.serializer encodedQueryData:queryData] forKey:key]; + + NSString *canonicalID = queryData.query.canonicalID; + std::string indexKey = + [FSTLevelDBQueryTargetKey keyWithCanonicalID:canonicalID targetID:targetID]; + std::string emptyBuffer; + [group setData:emptyBuffer forKey:indexKey]; + + FSTPBTargetGlobal *metadata = self.metadata; + if (targetID > metadata.highestTargetId) { + metadata.highestTargetId = targetID; + [group setMessage:metadata forKey:[FSTLevelDBTargetGlobalKey key]]; + } +} + +- (void)removeQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group { + FSTTargetID targetID = queryData.targetID; + + [self removeMatchingKeysForTargetID:targetID group:group]; + + std::string key = [FSTLevelDBTargetKey keyWithTargetID:targetID]; + [group removeMessageForKey:key]; + + std::string indexKey = + [FSTLevelDBQueryTargetKey keyWithCanonicalID:queryData.query.canonicalID targetID:targetID]; + [group removeMessageForKey:indexKey]; +} + +/** + * Looks up the query global metadata associated with the given key. + * + * @return the parsed protocol buffer message or nil if the row referenced by the given key does + * not exist. + */ +- (nullable FSTPBTargetGlobal *)metadataForKey:(const std::string &)key { + std::string value; + Status status = _db->Get(GetStandardReadOptions(), key, &value); + if (status.IsNotFound()) { + return nil; + } else if (!status.ok()) { + FSTFail(@"metadataForKey: failed loading key %s with status: %s", key.c_str(), + status.ToString().c_str()); + } + + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)value.data() length:value.size() freeWhenDone:NO]; + + NSError *error; + FSTPBTargetGlobal *proto = [FSTPBTargetGlobal parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBTargetGlobal failed to parse: %@", error); + } + + return proto; +} + +/** + * Parses the given bytes as an FSTPBTarget protocol buffer and then converts to the equivalent + * query data. + */ +- (FSTQueryData *)decodedTargetWithSlice:(Slice)slice { + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO]; + + NSError *error; + FSTPBTarget *proto = [FSTPBTarget parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBTarget failed to parse: %@", error); + } + + return [self.serializer decodedQueryData:proto]; +} + +- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + // Scan the query-target index starting with a prefix starting with the given query's canonicalID. + // Note that this is a scan rather than a get because canonicalIDs are not required to be unique + // per target. + Slice canonicalID = StringView(query.canonicalID); + std::unique_ptr<Iterator> indexItererator(_db->NewIterator(GetStandardReadOptions())); + std::string indexPrefix = [FSTLevelDBQueryTargetKey keyPrefixWithCanonicalID:canonicalID]; + indexItererator->Seek(indexPrefix); + + // Simultaneously scan the targets table. This works because each (canonicalID, targetID) pair is + // unique and ordered, so when scanning a table prefixed by exactly one canonicalID, all the + // targetIDs will be unique and in order. + std::string targetPrefix = [FSTLevelDBTargetKey keyPrefix]; + std::unique_ptr<Iterator> targetIterator(_db->NewIterator(GetStandardReadOptions())); + + FSTLevelDBQueryTargetKey *rowKey = [[FSTLevelDBQueryTargetKey alloc] init]; + for (; indexItererator->Valid(); indexItererator->Next()) { + Slice indexKey = indexItererator->key(); + + // Only consider rows matching exactly the specific canonicalID of interest. + if (!indexKey.starts_with(indexPrefix) || ![rowKey decodeKey:indexKey] || + canonicalID != rowKey.canonicalID) { + // End of this canonicalID's possible targets. + break; + } + + // Each row is a unique combination of canonicalID and targetID, so this foreign key reference + // can only occur once. + std::string targetKey = [FSTLevelDBTargetKey keyWithTargetID:rowKey.targetID]; + targetIterator->Seek(targetKey); + if (!targetIterator->Valid() || targetIterator->key() != targetKey) { + NSString *foundKeyDescription = @"the end of the table"; + if (targetIterator->Valid()) { + foundKeyDescription = [FSTLevelDBKey descriptionForKey:targetIterator->key()]; + } + FSTFail(@"Dangling query-target reference found: " + @"%@ points to %@; seeking there found %@", + [FSTLevelDBKey descriptionForKey:indexKey], + [FSTLevelDBKey descriptionForKey:targetKey], foundKeyDescription); + } + + // Finally after finding a potential match, check that the query is actually equal to the + // requested query. + FSTQueryData *target = [self decodedTargetWithSlice:targetIterator->value()]; + if ([target.query isEqual:query]) { + return target; + } + } + + return nil; +} + +#pragma mark Matching Key tracking + +- (void)addMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(FSTWriteGroup *)group { + // Store an empty value in the index which is equivalent to serializing a GPBEmpty message. In the + // future if we wanted to store some other kind of value here, we can parse these empty values as + // with some other protocol buffer (and the parser will see all default values). + std::string emptyBuffer; + + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *documentKey, BOOL *stop) { + [group setData:emptyBuffer + forKey:[FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:documentKey]]; + [group setData:emptyBuffer + forKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:documentKey targetID:targetID]]; + }]; +} + +- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(FSTWriteGroup *)group { + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + [group + removeMessageForKey:[FSTLevelDBTargetDocumentKey keyWithTargetID:targetID documentKey:key]]; + [group + removeMessageForKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:key targetID:targetID]]; + [self.garbageCollector addPotentialGarbageKey:key]; + }]; +} + +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(FSTWriteGroup *)group { + std::string indexPrefix = [FSTLevelDBTargetDocumentKey keyPrefixWithTargetID:targetID]; + std::unique_ptr<Iterator> indexIterator(_db->NewIterator(GetStandardReadOptions())); + indexIterator->Seek(indexPrefix); + + FSTLevelDBTargetDocumentKey *rowKey = [[FSTLevelDBTargetDocumentKey alloc] init]; + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + // Only consider rows matching this specific targetID. + if (![rowKey decodeKey:indexKey] || rowKey.targetID != targetID) { + break; + } + FSTDocumentKey *documentKey = rowKey.documentKey; + + // Delete both index rows + [group removeMessageForKey:indexKey]; + [group removeMessageForKey:[FSTLevelDBDocumentTargetKey keyWithDocumentKey:documentKey + targetID:targetID]]; + [self.garbageCollector addPotentialGarbageKey:documentKey]; + } +} + +- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { + std::string indexPrefix = [FSTLevelDBTargetDocumentKey keyPrefixWithTargetID:targetID]; + std::unique_ptr<Iterator> indexIterator(_db->NewIterator(GetStandardReadOptions())); + indexIterator->Seek(indexPrefix); + + FSTDocumentKeySet *result = [FSTDocumentKeySet keySet]; + FSTLevelDBTargetDocumentKey *rowKey = [[FSTLevelDBTargetDocumentKey alloc] init]; + for (; indexIterator->Valid(); indexIterator->Next()) { + Slice indexKey = indexIterator->key(); + + // Only consider rows matching this specific targetID. + if (![rowKey decodeKey:indexKey] || rowKey.targetID != targetID) { + break; + } + + result = [result setByAddingObject:rowKey.documentKey]; + } + + return result; +} + +#pragma mark - FSTGarbageSource implementation + +- (BOOL)containsKey:(FSTDocumentKey *)key { + std::string indexPrefix = [FSTLevelDBDocumentTargetKey keyPrefixWithResourcePath:key.path]; + std::unique_ptr<Iterator> indexIterator(_db->NewIterator(GetStandardReadOptions())); + indexIterator->Seek(indexPrefix); + + if (indexIterator->Valid()) { + FSTLevelDBDocumentTargetKey *rowKey = [[FSTLevelDBDocumentTargetKey alloc] init]; + if ([rowKey decodeKey:indexIterator->key()] && [rowKey.documentKey isEqualToKey:key]) { + return YES; + } + } + + return NO; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h new file mode 100644 index 0000000..f327813 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h @@ -0,0 +1,50 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTRemoteDocumentCache.h" + +#ifdef __cplusplus +#include <memory> + +namespace leveldb { +class DB; +} +#endif + +@class FSTLocalSerializer; + +NS_ASSUME_NONNULL_BEGIN + +/** Cached Remote Documents backed by leveldb. */ +@interface FSTLevelDBRemoteDocumentCache : NSObject <FSTRemoteDocumentCache> + +- (instancetype)init NS_UNAVAILABLE; + +#ifdef __cplusplus +/** + * Creates a new remote documents cache in the given leveldb. + * + * @param db The leveldb in which to create the cache. + */ +- (instancetype)initWithDB:(std::shared_ptr<leveldb::DB>)db + serializer:(FSTLocalSerializer *)serializer NS_DESIGNATED_INITIALIZER; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm new file mode 100644 index 0000000..e2424b9 --- /dev/null +++ b/Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm @@ -0,0 +1,153 @@ +/* + * 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 "FSTLevelDBRemoteDocumentCache.h" + +#include <leveldb/db.h> +#include <leveldb/write_batch.h> +#include <string> + +#import "MaybeDocument.pbobjc.h" +#import "FSTLevelDBKey.h" +#import "FSTLocalSerializer.h" +#import "FSTWriteGroup.h" +#import "FSTDocument.h" +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentSet.h" +#import "FSTAssert.h" + +#include "ordered_code.h" +#include "string_util.h" + +NS_ASSUME_NONNULL_BEGIN + +using Firestore::OrderedCode; +using leveldb::DB; +using leveldb::Iterator; +using leveldb::ReadOptions; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteOptions; + +@interface FSTLevelDBRemoteDocumentCache () + +@property(nonatomic, strong, readonly) FSTLocalSerializer *serializer; + +@end + +/** + * Returns a standard set of read options. + * + * For now this is paranoid, but perhaps disable that in production builds. + */ +static ReadOptions StandardReadOptions() { + ReadOptions options; + options.verify_checksums = true; + return options; +} + +@implementation FSTLevelDBRemoteDocumentCache { + // The DB pointer is shared with all cooperating LevelDB-related objects. + std::shared_ptr<DB> _db; +} + +- (instancetype)initWithDB:(std::shared_ptr<DB>)db serializer:(FSTLocalSerializer *)serializer { + if (self = [super init]) { + _db = db; + _serializer = serializer; + } + return self; +} + +- (void)shutdown { + _db.reset(); +} + +- (void)addEntry:(FSTMaybeDocument *)document group:(FSTWriteGroup *)group { + std::string key = [self remoteDocumentKey:document.key]; + [group setMessage:[self.serializer encodedMaybeDocument:document] forKey:key]; +} + +- (void)removeEntryForKey:(FSTDocumentKey *)documentKey group:(FSTWriteGroup *)group { + std::string key = [self remoteDocumentKey:documentKey]; + [group removeMessageForKey:key]; +} + +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey { + std::string key = [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:documentKey]; + std::string value; + Status status = _db->Get(StandardReadOptions(), key, &value); + if (status.IsNotFound()) { + return nil; + } else if (status.ok()) { + return [self decodedMaybeDocument:value withKey:documentKey]; + } else { + FSTFail(@"Fetch document for key (%@) failed with status: %s", documentKey, + status.ToString().c_str()); + } +} + +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { + // TODO(mikelehen): PERF: At least filter to the documents that match the path of the query. + FSTDocumentDictionary *results = [FSTDocumentDictionary documentDictionary]; + + std::string startKey = [FSTLevelDBRemoteDocumentKey keyPrefix]; + std::unique_ptr<Iterator> it(_db->NewIterator(StandardReadOptions())); + it->Seek(startKey); + + FSTLevelDBRemoteDocumentKey *currentKey = [[FSTLevelDBRemoteDocumentKey alloc] init]; + for (; it->Valid() && [currentKey decodeKey:it->key()]; it->Next()) { + FSTMaybeDocument *maybeDoc = + [self decodedMaybeDocument:it->value() withKey:currentKey.documentKey]; + if ([maybeDoc isKindOfClass:[FSTDocument class]]) { + results = [results dictionaryBySettingObject:(FSTDocument *)maybeDoc forKey:maybeDoc.key]; + } + } + + Status status = it->status(); + if (!status.ok()) { + FSTFail(@"Find documents matching query (%@) failed with status: %s", query, + status.ToString().c_str()); + } + + return results; +} + +- (std::string)remoteDocumentKey:(FSTDocumentKey *)key { + return [FSTLevelDBRemoteDocumentKey keyWithDocumentKey:key]; +} + +- (FSTMaybeDocument *)decodedMaybeDocument:(Slice)slice withKey:(FSTDocumentKey *)documentKey { + NSData *data = + [[NSData alloc] initWithBytesNoCopy:(void *)slice.data() length:slice.size() freeWhenDone:NO]; + + NSError *error; + FSTPBMaybeDocument *proto = [FSTPBMaybeDocument parseFromData:data error:&error]; + if (!proto) { + FSTFail(@"FSTPBMaybeDocument failed to parse: %@", error); + } + + FSTMaybeDocument *maybeDocument = [self.serializer decodedMaybeDocument:proto]; + FSTAssert([maybeDocument.key isEqualToKey:documentKey], + @"Read document has key (%@) instead of expected key (%@).", maybeDocument.key, + documentKey); + return maybeDocument; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.h b/Firestore/Source/Local/FSTLocalDocumentsView.h new file mode 100644 index 0000000..60571c2 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalDocumentsView.h @@ -0,0 +1,62 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKeySet.h" + +@class FSTDocumentKey; +@class FSTMaybeDocument; +@class FSTQuery; +@protocol FSTMutationQueue; +@protocol FSTRemoteDocumentCache; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A readonly view of the local state of all documents we're tracking (i.e. we have a cached + * version in remoteDocumentCache or local mutations for the document). The view is computed by + * applying the mutations in the FSTMutationQueue to the FSTRemoteDocumentCache. + */ +@interface FSTLocalDocumentsView : NSObject + ++ (instancetype)viewWithRemoteDocumentCache:(id<FSTRemoteDocumentCache>)remoteDocumentCache + mutationQueue:(id<FSTMutationQueue>)mutationQueue; + +- (instancetype)init __attribute__((unavailable("Use a static constructor"))); + +/** + * Get the local view of the document identified by `key`. + * + * @return Local view of the document or nil if we don't have any cached state for it. + */ +- (nullable FSTMaybeDocument *)documentForKey:(FSTDocumentKey *)key; + +/** + * Gets the local view of the documents identified by `keys`. + * + * If we don't have cached state for a document in `keys`, a FSTDeletedDocument will be stored + * for that key in the resulting set. + */ +- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys; + +/** Performs a query against the local view of all documents. */ +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalDocumentsView.m b/Firestore/Source/Local/FSTLocalDocumentsView.m new file mode 100644 index 0000000..0cad593 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalDocumentsView.m @@ -0,0 +1,182 @@ +/* + * 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 "FSTLocalDocumentsView.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKey.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTMutationQueue.h" +#import "FSTQuery.h" +#import "FSTRemoteDocumentCache.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalDocumentsView () +- (instancetype)initWithRemoteDocumentCache:(id<FSTRemoteDocumentCache>)remoteDocumentCache + mutationQueue:(id<FSTMutationQueue>)mutationQueue + NS_DESIGNATED_INITIALIZER; +@property(nonatomic, strong, readonly) id<FSTRemoteDocumentCache> remoteDocumentCache; +@property(nonatomic, strong, readonly) id<FSTMutationQueue> mutationQueue; +@end + +@implementation FSTLocalDocumentsView + ++ (instancetype)viewWithRemoteDocumentCache:(id<FSTRemoteDocumentCache>)remoteDocumentCache + mutationQueue:(id<FSTMutationQueue>)mutationQueue { + return [[FSTLocalDocumentsView alloc] initWithRemoteDocumentCache:remoteDocumentCache + mutationQueue:mutationQueue]; +} + +- (instancetype)initWithRemoteDocumentCache:(id<FSTRemoteDocumentCache>)remoteDocumentCache + mutationQueue:(id<FSTMutationQueue>)mutationQueue { + if (self = [super init]) { + _remoteDocumentCache = remoteDocumentCache; + _mutationQueue = mutationQueue; + } + return self; +} + +- (nullable FSTMaybeDocument *)documentForKey:(FSTDocumentKey *)key { + FSTMaybeDocument *_Nullable remoteDoc = [self.remoteDocumentCache entryForKey:key]; + return [self localDocument:remoteDoc key:key]; +} + +- (FSTMaybeDocumentDictionary *)documentsForKeys:(FSTDocumentKeySet *)keys { + FSTMaybeDocumentDictionary *results = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + for (FSTDocumentKey *key in keys.objectEnumerator) { + // TODO(mikelehen): PERF: Consider fetching all remote documents at once rather than one-by-one. + FSTMaybeDocument *maybeDoc = [self documentForKey:key]; + // TODO(http://b/32275378): Don't conflate missing / deleted. + if (!maybeDoc) { + maybeDoc = [FSTDeletedDocument documentWithKey:key version:[FSTSnapshotVersion noVersion]]; + } + results = [results dictionaryBySettingObject:maybeDoc forKey:key]; + } + return results; +} + +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { + if ([FSTDocumentKey isDocumentKey:query.path]) { + return [self documentsMatchingDocumentQuery:query.path]; + } else { + return [self documentsMatchingCollectionQuery:query]; + } +} + +- (FSTDocumentDictionary *)documentsMatchingDocumentQuery:(FSTResourcePath *)docPath { + FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary]; + // Just do a simple document lookup. + FSTMaybeDocument *doc = [self documentForKey:[FSTDocumentKey keyWithPath:docPath]]; + if ([doc isKindOfClass:[FSTDocument class]]) { + result = [result dictionaryBySettingObject:(FSTDocument *)doc forKey:doc.key]; + } + return result; +} + +- (FSTDocumentDictionary *)documentsMatchingCollectionQuery:(FSTQuery *)query { + // Query the remote documents and overlay mutations. + // TODO(mikelehen): There may be significant overlap between the mutations affecting these + // remote documents and the allMutationBatchesAffectingQuery mutations. Consider optimizing. + __block FSTDocumentDictionary *results = [self.remoteDocumentCache documentsMatchingQuery:query]; + results = [self localDocuments:results]; + + // Now use the mutation queue to discover any other documents that may match the query after + // applying mutations. + FSTDocumentKeySet *matchingKeys = [FSTDocumentKeySet keySet]; + NSArray<FSTMutationBatch *> *matchingMutationBatches = + [self.mutationQueue allMutationBatchesAffectingQuery:query]; + for (FSTMutationBatch *batch in matchingMutationBatches) { + for (FSTMutation *mutation in batch.mutations) { + // TODO(mikelehen): PERF: Check if this mutation actually affects the query to reduce work. + + // If the key is already in the results, we can skip it. + if (![results containsKey:mutation.key]) { + matchingKeys = [matchingKeys setByAddingObject:mutation.key]; + } + } + } + + // Now add in results for the matchingKeys. + for (FSTDocumentKey *key in matchingKeys.objectEnumerator) { + FSTMaybeDocument *doc = [self documentForKey:key]; + if ([doc isKindOfClass:[FSTDocument class]]) { + results = [results dictionaryBySettingObject:(FSTDocument *)doc forKey:key]; + } + } + + // Finally, filter out any documents that don't actually match the query. Note that the extra + // reference here prevents ARC from deallocating the initial unfiltered results while we're + // enumerating them. + FSTDocumentDictionary *unfiltered = results; + [unfiltered + enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *doc, BOOL *stop) { + if (![query matchesDocument:doc]) { + results = [results dictionaryByRemovingObjectForKey:key]; + } + }]; + + return results; +} + +/** + * Takes a remote document and applies local mutations to generate the local view of the + * document. + * + * @param document The base remote document to apply mutations to. + * @param documentKey The key of the document (necessary when remoteDocument is nil). + */ +- (nullable FSTMaybeDocument *)localDocument:(nullable FSTMaybeDocument *)document + key:(FSTDocumentKey *)documentKey { + NSArray<FSTMutationBatch *> *batches = + [self.mutationQueue allMutationBatchesAffectingDocumentKey:documentKey]; + for (FSTMutationBatch *batch in batches) { + document = [batch applyTo:document documentKey:documentKey]; + } + + return document; +} + +/** + * Takes a set of remote documents and applies local mutations to generate the local view of + * the documents. + * + * @param documents The base remote documents to apply mutations to. + * @return The local view of the documents. + */ +- (FSTDocumentDictionary *)localDocuments:(FSTDocumentDictionary *)documents { + __block FSTDocumentDictionary *result = documents; + [documents enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTDocument *remoteDocument, + BOOL *stop) { + FSTMaybeDocument *mutatedDoc = [self localDocument:remoteDocument key:key]; + if ([mutatedDoc isKindOfClass:[FSTDeletedDocument class]]) { + result = [documents dictionaryByRemovingObjectForKey:key]; + } else if ([mutatedDoc isKindOfClass:[FSTDocument class]]) { + result = [documents dictionaryBySettingObject:(FSTDocument *)mutatedDoc forKey:key]; + } else { + FSTFail(@"Unknown document: %@", mutatedDoc); + } + }]; + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalSerializer.h b/Firestore/Source/Local/FSTLocalSerializer.h new file mode 100644 index 0000000..6ca7f01 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalSerializer.h @@ -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 <Foundation/Foundation.h> + +@class FSTMaybeDocument; +@class FSTMutationBatch; +@class FSTQueryData; +@class FSTSerializerBeta; +@class FSTSnapshotVersion; + +@class FSTPBMaybeDocument; +@class FSTPBTarget; +@class FSTPBWriteBatch; + +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Serializer for values stored in the LocalStore. + * + * Note that FSTLocalSerializer currently delegates to the serializer for the Firestore v1beta1 RPC + * protocol to save implementation time and code duplication. We'll need to revisit this when the + * RPC protocol we use diverges from local storage. + */ +@interface FSTLocalSerializer : NSObject + +- (instancetype)initWithRemoteSerializer:(FSTSerializerBeta *)remoteSerializer; + +- (instancetype)init NS_UNAVAILABLE; + +/** Encodes an FSTMaybeDocument model to the equivalent protocol buffer for local storage. */ +- (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document; + +/** Decodes an FSTPBMaybeDocument proto to the equivalent model. */ +- (FSTMaybeDocument *)decodedMaybeDocument:(FSTPBMaybeDocument *)proto; + +/** Encodes an FSTMutationBatch model for local storage in the mutation queue. */ +- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch; + +/** Decodes an FSTPBWriteBatch proto into a MutationBatch model. */ +- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch; + +/** Encodes an FSTQueryData model for local storage in the query cache. */ +- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData; + +/** Decodes an FSTPBTarget proto from local storage into an FSTQueryData model. */ +- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target; + +/** Encodes an FSTSnapshotVersion model into a GPBTimestamp proto. */ +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version; + +/** Decodes a GPBTimestamp proto into a FSTSnapshotVersion model. */ +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalSerializer.m b/Firestore/Source/Local/FSTLocalSerializer.m new file mode 100644 index 0000000..58b09af --- /dev/null +++ b/Firestore/Source/Local/FSTLocalSerializer.m @@ -0,0 +1,208 @@ +/* + * 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 "FSTLocalSerializer.h" + +#import "Document.pbobjc.h" +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTFieldValue.h" +#import "FSTMutationBatch.h" +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTSerializerBeta.h" +#import "MaybeDocument.pbobjc.h" +#import "Mutation.pbobjc.h" +#import "Target.pbobjc.h" + +@interface FSTLocalSerializer () + +@property(nonatomic, strong, readonly) FSTSerializerBeta *remoteSerializer; + +@end + +/** Serializer for values stored in the LocalStore. */ +@implementation FSTLocalSerializer + +- (instancetype)initWithRemoteSerializer:(FSTSerializerBeta *)remoteSerializer { + self = [super init]; + if (self) { + _remoteSerializer = remoteSerializer; + } + return self; +} + +- (FSTPBMaybeDocument *)encodedMaybeDocument:(FSTMaybeDocument *)document { + FSTPBMaybeDocument *proto = [FSTPBMaybeDocument message]; + + if ([document isKindOfClass:[FSTDeletedDocument class]]) { + proto.noDocument = [self encodedDeletedDocument:(FSTDeletedDocument *)document]; + } else if ([document isKindOfClass:[FSTDocument class]]) { + proto.document = [self encodedDocument:(FSTDocument *)document]; + } else { + FSTFail(@"Unknown document type %@", NSStringFromClass([document class])); + } + + return proto; +} + +- (FSTMaybeDocument *)decodedMaybeDocument:(FSTPBMaybeDocument *)proto { + switch (proto.documentTypeOneOfCase) { + case FSTPBMaybeDocument_DocumentType_OneOfCase_Document: + return [self decodedDocument:proto.document]; + + case FSTPBMaybeDocument_DocumentType_OneOfCase_NoDocument: + return [self decodedDeletedDocument:proto.noDocument]; + + default: + FSTFail(@"Unknown MaybeDocument %@", proto); + } +} + +/** + * Encodes a Document for local storage. This differs from the v1beta1 RPC serializer for + * Documents in that it preserves the updateTime, which is considered an output only value by the + * server. + */ +- (GCFSDocument *)encodedDocument:(FSTDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + GCFSDocument *proto = [GCFSDocument message]; + proto.name = [remoteSerializer encodedDocumentKey:document.key]; + proto.fields = [remoteSerializer encodedFields:document.data]; + proto.updateTime = [remoteSerializer encodedVersion:document.version]; + + return proto; +} + +/** Decodes a Document proto to the equivalent model. */ +- (FSTDocument *)decodedDocument:(GCFSDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTObjectValue *data = [remoteSerializer decodedFields:document.fields]; + FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:document.name]; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:document.updateTime]; + return [FSTDocument documentWithData:data key:key version:version hasLocalMutations:NO]; +} + +/** Encodes a NoDocument value to the equivalent proto. */ +- (FSTPBNoDocument *)encodedDeletedDocument:(FSTDeletedDocument *)document { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTPBNoDocument *proto = [FSTPBNoDocument message]; + proto.name = [remoteSerializer encodedDocumentKey:document.key]; + proto.readTime = [remoteSerializer encodedVersion:document.version]; + return proto; +} + +/** Decodes a NoDocument proto to the equivalent model. */ +- (FSTDeletedDocument *)decodedDeletedDocument:(FSTPBNoDocument *)proto { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTDocumentKey *key = [remoteSerializer decodedDocumentKey:proto.name]; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:proto.readTime]; + return [FSTDeletedDocument documentWithKey:key version:version]; +} + +- (FSTPBWriteBatch *)encodedMutationBatch:(FSTMutationBatch *)batch { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTPBWriteBatch *proto = [FSTPBWriteBatch message]; + proto.batchId = batch.batchID; + proto.localWriteTime = [remoteSerializer encodedTimestamp:batch.localWriteTime]; + + NSMutableArray<GCFSWrite *> *writes = proto.writesArray; + for (FSTMutation *mutation in batch.mutations) { + [writes addObject:[remoteSerializer encodedMutation:mutation]]; + } + return proto; +} + +- (FSTMutationBatch *)decodedMutationBatch:(FSTPBWriteBatch *)batch { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + int batchID = batch.batchId; + NSMutableArray<FSTMutation *> *mutations = [NSMutableArray array]; + for (GCFSWrite *write in batch.writesArray) { + [mutations addObject:[remoteSerializer decodedMutation:write]]; + } + + FSTTimestamp *localWriteTime = [remoteSerializer decodedTimestamp:batch.localWriteTime]; + + return [[FSTMutationBatch alloc] initWithBatchID:batchID + localWriteTime:localWriteTime + mutations:mutations]; +} + +- (FSTPBTarget *)encodedQueryData:(FSTQueryData *)queryData { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTAssert(queryData.purpose == FSTQueryPurposeListen, + @"only queries with purpose %lu may be stored, got %lu", + (unsigned long)FSTQueryPurposeListen, (unsigned long)queryData.purpose); + + FSTPBTarget *proto = [FSTPBTarget message]; + proto.targetId = queryData.targetID; + proto.snapshotVersion = [remoteSerializer encodedVersion:queryData.snapshotVersion]; + proto.resumeToken = queryData.resumeToken; + + FSTQuery *query = queryData.query; + if ([query isDocumentQuery]) { + proto.documents = [remoteSerializer encodedDocumentsTarget:query]; + } else { + proto.query = [remoteSerializer encodedQueryTarget:query]; + } + + return proto; +} + +- (FSTQueryData *)decodedQueryData:(FSTPBTarget *)target { + FSTSerializerBeta *remoteSerializer = self.remoteSerializer; + + FSTTargetID targetID = target.targetId; + FSTSnapshotVersion *version = [remoteSerializer decodedVersion:target.snapshotVersion]; + NSData *resumeToken = target.resumeToken; + + FSTQuery *query; + switch (target.targetTypeOneOfCase) { + case FSTPBTarget_TargetType_OneOfCase_Documents: + query = [remoteSerializer decodedQueryFromDocumentsTarget:target.documents]; + break; + + case FSTPBTarget_TargetType_OneOfCase_Query: + query = [remoteSerializer decodedQueryFromQueryTarget:target.query]; + break; + + default: + FSTFail(@"Unknown Target.targetType %" PRId32, target.targetTypeOneOfCase); + } + + return [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeListen + snapshotVersion:version + resumeToken:resumeToken]; +} + +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { + return [self.remoteSerializer encodedVersion:version]; +} + +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { + return [self.remoteSerializer decodedVersion:version]; +} + +@end diff --git a/Firestore/Source/Local/FSTLocalStore.h b/Firestore/Source/Local/FSTLocalStore.h new file mode 100644 index 0000000..0fdc08e --- /dev/null +++ b/Firestore/Source/Local/FSTLocalStore.h @@ -0,0 +1,194 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKeySet.h" +#import "FSTDocumentVersionDictionary.h" +#import "FSTTypes.h" + +@class FSTLocalViewChanges; +@class FSTLocalWriteResult; +@class FSTMutation; +@class FSTMutationBatch; +@class FSTMutationBatchResult; +@class FSTQuery; +@class FSTQueryData; +@class FSTRemoteEvent; +@class FSTUser; +@protocol FSTPersistence; +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Local storage in the Firestore client. Coordinates persistence components like the mutation + * queue and remote document cache to present a latency compensated view of stored data. + * + * The LocalStore is responsible for accepting mutations from the Sync Engine. Writes from the + * client are put into a queue as provisional Mutations until they are processed by the RemoteStore + * and confirmed as having been written to the server. + * + * The local store provides the local version of documents that have been modified locally. It + * maintains the constraint: + * + * LocalDocument = RemoteDocument + Active(LocalMutations) + * + * (Active mutations are those that are enqueued and have not been previously acknowledged or + * rejected). + * + * The RemoteDocument ("ground truth") state is provided via the applyChangeBatch method. It will + * be some version of a server-provided document OR will be a server-provided document PLUS + * acknowledged mutations: + * + * RemoteDocument' = RemoteDocument + Acknowledged(LocalMutations) + * + * Note that this "dirty" version of a RemoteDocument will not be identical to a server base + * version, since it has LocalMutations added to it pending getting an authoritative copy from the + * server. + * + * Since LocalMutations can be rejected by the server, we have to be able to revert a LocalMutation + * that has already been applied to the LocalDocument (typically done by replaying all remaining + * LocalMutations to the RemoteDocument to re-apply). + * + * The LocalStore is responsible for the garbage collection of the documents it contains. For now, + * it every doc referenced by a view, the mutation queue, or the RemoteStore. + * + * It also maintains the persistence of mapping queries to resume tokens and target ids. It needs + * to know this data about queries to properly know what docs it would be allowed to garbage + * collect. + * + * The LocalStore must be able to efficiently execute queries against its local cache of the + * documents, to provide the initial set of results before any remote changes have been received. + */ +@interface FSTLocalStore : NSObject + +/** Creates a new instance of the FSTLocalStore with its required dependencies as parameters. */ +- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence + garbageCollector:(id<FSTGarbageCollector>)garbageCollector + initialUser:(FSTUser *)initialUser NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** Performs any initial startup actions required by the local store. */ +- (void)start; + +/** Releases any open resources. */ +- (void)shutdown; + +/** + * Tells the FSTLocalStore that the currently authenticated user has changed. + * + * In response the local store switches the mutation queue to the new user and returns any + * resulting document changes. + */ +- (FSTMaybeDocumentDictionary *)userDidChange:(FSTUser *)user; + +/** Accepts locally generated Mutations and commits them to storage. */ +- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray<FSTMutation *> *)mutations; + +/** Returns the current value of a document with a given key, or nil if not found. */ +- (nullable FSTMaybeDocument *)readDocument:(FSTDocumentKey *)key; + +/** + * Acknowledges the given batch. + * + * On the happy path when a batch is acknowledged, the local store will + * + * + remove the batch from the mutation queue; + * + apply the changes to the remote document cache; + * + recalculate the latency compensated view implied by those changes (there may be mutations in + * the queue that affect the documents but haven't been acknowledged yet); and + * + give the changed documents back the sync engine + * + * @return The resulting (modified) documents. + */ +- (FSTMaybeDocumentDictionary *)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult; + +/** + * Removes mutations from the MutationQueue for the specified batch. LocalDocuments will be + * recalculated. + * + * @return The resulting (modified) documents. + */ +- (FSTMaybeDocumentDictionary *)rejectBatchID:(FSTBatchID)batchID; + +/** Returns the last recorded stream token for the current user. */ +- (nullable NSData *)lastStreamToken; + +/** + * Sets the stream token for the current user without acknowledging any mutation batch. This is + * usually only useful after a stream handshake or in response to an error that requires clearing + * the stream token. + */ +- (void)setLastStreamToken:(nullable NSData *)streamToken; + +/** + * Returns the last consistent snapshot processed (used by the RemoteStore to determine whether to + * buffer incoming snapshots from the backend). + */ +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion; + +/** + * Updates the "ground-state" (remote) documents. We assume that the remote event reflects any + * write batches that have been acknowledged or rejected (i.e. we do not re-apply local mutations + * to updates from this event). + * + * LocalDocuments are re-calculated if there are remaining mutations in the queue. + */ +- (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent; + +/** + * Returns the keys of the documents that are associated with the given targetID in the remote + * table. + */ +- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID; + +/** + * Collects garbage if necessary. + * + * Should be called periodically by Sync Engine to recover resources. The implementation must + * guarantee that GC won't happen in other places than this method call. + */ +- (void)collectGarbage; + +/** + * Assigns @a query an internal ID so that its results can be pinned so they don't get GC'd. + * A query must be allocated in the local store before the store can be used to manage its view. + */ +- (FSTQueryData *)allocateQuery:(FSTQuery *)query; + +/** Unpin all the documents associated with @a query. */ +- (void)releaseQuery:(FSTQuery *)query; + +/** Runs @a query against all the documents in the local store and returns the results. */ +- (FSTDocumentDictionary *)executeQuery:(FSTQuery *)query; + +/** Notify the local store of the changed views to locally pin / unpin documents. */ +- (void)notifyLocalViewChanges:(NSArray<FSTLocalViewChanges *> *)viewChanges; + +/** + * Gets the mutation batch after the passed in batchId in the mutation queue or nil if empty. + * + * @param batchID The batch to search after, or -1 for the first mutation in the queue. + * @return the next mutation or nil if there wasn't one. + */ +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalStore.m b/Firestore/Source/Local/FSTLocalStore.m new file mode 100644 index 0000000..d31712a --- /dev/null +++ b/Firestore/Source/Local/FSTLocalStore.m @@ -0,0 +1,546 @@ +/* + * 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 "FSTLocalStore.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKey.h" +#import "FSTGarbageCollector.h" +#import "FSTLocalDocumentsView.h" +#import "FSTLocalViewChanges.h" +#import "FSTLocalWriteResult.h" +#import "FSTLogger.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTMutationQueue.h" +#import "FSTPersistence.h" +#import "FSTQuery.h" +#import "FSTQueryCache.h" +#import "FSTQueryData.h" +#import "FSTReferenceSet.h" +#import "FSTRemoteDocumentCache.h" +#import "FSTRemoteDocumentChangeBuffer.h" +#import "FSTRemoteEvent.h" +#import "FSTSnapshotVersion.h" +#import "FSTTargetIDGenerator.h" +#import "FSTTimestamp.h" +#import "FSTUser.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalStore () + +/** Manages our in-memory or durable persistence. */ +@property(nonatomic, strong, readonly) id<FSTPersistence> persistence; + +/** The set of all mutations that have been sent but not yet been applied to the backend. */ +@property(nonatomic, strong) id<FSTMutationQueue> mutationQueue; + +/** The set of all cached remote documents. */ +@property(nonatomic, strong) id<FSTRemoteDocumentCache> remoteDocumentCache; + +/** The "local" view of all documents (layering mutationQueue on top of remoteDocumentCache). */ +@property(nonatomic, strong) FSTLocalDocumentsView *localDocuments; + +/** The set of document references maintained by any local views. */ +@property(nonatomic, strong) FSTReferenceSet *localViewReferences; + +/** + * The garbage collector collects documents that should no longer be cached (e.g. if they are no + * longer retained by the above reference sets and the garbage collector is performing eager + * collection). + */ +@property(nonatomic, strong) id<FSTGarbageCollector> garbageCollector; + +/** Maps a query to the data about that query. */ +@property(nonatomic, strong) id<FSTQueryCache> queryCache; + +/** Maps a targetID to data about its query. */ +@property(nonatomic, strong) NSMutableDictionary<NSNumber *, FSTQueryData *> *targetIDs; + +/** Used to generate targetIDs for queries tracked locally. */ +@property(nonatomic, strong) FSTTargetIDGenerator *targetIDGenerator; + +/** + * A heldBatchResult is a mutation batch result (from a write acknowledgement) that arrived before + * the watch stream got notified of a snapshot that includes the write.  So we "hold" it until + * the watch stream catches up. It ensures that the local write remains visible (latency + * compensation) and doesn't temporarily appear reverted because the watch stream is slower than + * the write stream and so wasn't reflecting it. + * + * NOTE: Eventually we want to move this functionality into the remote store. + */ +@property(nonatomic, strong) NSMutableArray<FSTMutationBatchResult *> *heldBatchResults; + +@end + +@implementation FSTLocalStore + +- (instancetype)initWithPersistence:(id<FSTPersistence>)persistence + garbageCollector:(id<FSTGarbageCollector>)garbageCollector + initialUser:(FSTUser *)initialUser { + if (self = [super init]) { + _persistence = persistence; + _mutationQueue = [persistence mutationQueueForUser:initialUser]; + _remoteDocumentCache = [persistence remoteDocumentCache]; + _queryCache = [persistence queryCache]; + _localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:_remoteDocumentCache + mutationQueue:_mutationQueue]; + _localViewReferences = [[FSTReferenceSet alloc] init]; + + _garbageCollector = garbageCollector; + [_garbageCollector addGarbageSource:_queryCache]; + [_garbageCollector addGarbageSource:_localViewReferences]; + [_garbageCollector addGarbageSource:_mutationQueue]; + + _targetIDs = [NSMutableDictionary dictionary]; + _heldBatchResults = [NSMutableArray array]; + } + return self; +} + +- (void)start { + [self startMutationQueue]; + [self startQueryCache]; +} + +- (void)startMutationQueue { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Start MutationQueue"]; + [self.mutationQueue startWithGroup:group]; + + // If we have any leftover mutation batch results from a prior run, just drop them. + // TODO(http://b/33446471): We probably need to repopulate heldBatchResults or similar instead, + // but that is not straightforward since we're not persisting the write ack versions. + [self.heldBatchResults removeAllObjects]; + + // TODO(mikelehen): This is the only usage of getAllMutationBatchesThroughBatchId:. Consider + // removing it in favor of a getAcknowledgedBatches method. + FSTBatchID highestAck = [self.mutationQueue highestAcknowledgedBatchID]; + if (highestAck != kFSTBatchIDUnknown) { + NSArray<FSTMutationBatch *> *batches = + [self.mutationQueue allMutationBatchesThroughBatchID:highestAck]; + if (batches.count > 0) { + // NOTE: This could be more efficient if we had a removeBatchesThroughBatchID, but this set + // should be very small and this code should go away eventually. + [self.mutationQueue removeMutationBatches:batches group:group]; + } + } + [self.persistence commitGroup:group]; +} + +- (void)startQueryCache { + [self.queryCache start]; + + FSTTargetID targetID = [self.queryCache highestTargetID]; + self.targetIDGenerator = [FSTTargetIDGenerator generatorForLocalStoreStartingAfterID:targetID]; +} + +- (void)shutdown { + [self.mutationQueue shutdown]; + [self.remoteDocumentCache shutdown]; + [self.queryCache shutdown]; +} + +- (FSTMaybeDocumentDictionary *)userDidChange:(FSTUser *)user { + // Swap out the mutation queue, grabbing the pending mutation batches before and after. + NSArray<FSTMutationBatch *> *oldBatches = [self.mutationQueue allMutationBatches]; + + [self.mutationQueue shutdown]; + [self.garbageCollector removeGarbageSource:self.mutationQueue]; + + self.mutationQueue = [self.persistence mutationQueueForUser:user]; + [self.garbageCollector addGarbageSource:self.mutationQueue]; + + [self startMutationQueue]; + + NSArray<FSTMutationBatch *> *newBatches = [self.mutationQueue allMutationBatches]; + + // Recreate our LocalDocumentsView using the new MutationQueue. + self.localDocuments = [FSTLocalDocumentsView viewWithRemoteDocumentCache:self.remoteDocumentCache + mutationQueue:self.mutationQueue]; + + // Union the old/new changed keys. + FSTDocumentKeySet *changedKeys = [FSTDocumentKeySet keySet]; + for (NSArray<FSTMutationBatch *> *batches in @[ oldBatches, newBatches ]) { + for (FSTMutationBatch *batch in batches) { + for (FSTMutation *mutation in batch.mutations) { + changedKeys = [changedKeys setByAddingObject:mutation.key]; + } + } + } + + // Return the set of all (potentially) changed documents as the result of the user change. + return [self.localDocuments documentsForKeys:changedKeys]; +} + +- (FSTLocalWriteResult *)locallyWriteMutations:(NSArray<FSTMutation *> *)mutations { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Locally write mutations"]; + FSTTimestamp *localWriteTime = [FSTTimestamp timestamp]; + FSTMutationBatch *batch = [self.mutationQueue addMutationBatchWithWriteTime:localWriteTime + mutations:mutations + group:group]; + [self.persistence commitGroup:group]; + + FSTDocumentKeySet *keys = [batch keys]; + FSTMaybeDocumentDictionary *changedDocuments = [self.localDocuments documentsForKeys:keys]; + return [FSTLocalWriteResult resultForBatchID:batch.batchID changes:changedDocuments]; +} + +- (FSTMaybeDocumentDictionary *)acknowledgeBatchWithResult:(FSTMutationBatchResult *)batchResult { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Acknowledge batch"]; + id<FSTMutationQueue> mutationQueue = self.mutationQueue; + + [mutationQueue acknowledgeBatch:batchResult.batch + streamToken:batchResult.streamToken + group:group]; + + FSTDocumentKeySet *affected; + if ([self shouldHoldBatchResultWithVersion:batchResult.commitVersion]) { + [self.heldBatchResults addObject:batchResult]; + affected = [FSTDocumentKeySet keySet]; + } else { + FSTRemoteDocumentChangeBuffer *remoteDocuments = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache]; + + affected = + [self releaseBatchResults:@[ batchResult ] group:group remoteDocuments:remoteDocuments]; + + [remoteDocuments applyToWriteGroup:group]; + } + + [self.persistence commitGroup:group]; + [self.mutationQueue performConsistencyCheck]; + + return [self.localDocuments documentsForKeys:affected]; +} + +- (FSTMaybeDocumentDictionary *)rejectBatchID:(FSTBatchID)batchID { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Reject batch"]; + + FSTMutationBatch *toReject = [self.mutationQueue lookupMutationBatch:batchID]; + FSTAssert(toReject, @"Attempt to reject nonexistent batch!"); + + FSTBatchID lastAcked = [self.mutationQueue highestAcknowledgedBatchID]; + FSTAssert(batchID > lastAcked, @"Acknowledged batches can't be rejected."); + + FSTDocumentKeySet *affected = [self removeMutationBatch:toReject group:group]; + + [self.persistence commitGroup:group]; + [self.mutationQueue performConsistencyCheck]; + + return [self.localDocuments documentsForKeys:affected]; +} + +- (nullable NSData *)lastStreamToken { + return [self.mutationQueue lastStreamToken]; +} + +- (void)setLastStreamToken:(nullable NSData *)streamToken { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Set stream token"]; + + [self.mutationQueue setLastStreamToken:streamToken group:group]; + [self.persistence commitGroup:group]; +} + +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { + return [self.queryCache lastRemoteSnapshotVersion]; +} + +- (FSTMaybeDocumentDictionary *)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent { + id<FSTQueryCache> queryCache = self.queryCache; + + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Apply remote event"]; + FSTRemoteDocumentChangeBuffer *remoteDocuments = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache]; + + [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( + NSNumber *targetIDNumber, FSTTargetChange *change, BOOL *stop) { + FSTTargetID targetID = targetIDNumber.intValue; + + // Do not ref/unref unassigned targetIDs - it may lead to leaks. + FSTQueryData *queryData = self.targetIDs[targetIDNumber]; + if (!queryData) { + return; + } + + FSTTargetMapping *mapping = change.mapping; + if (mapping) { + // First make sure that all references are deleted. + if ([mapping isKindOfClass:[FSTResetMapping class]]) { + FSTResetMapping *reset = (FSTResetMapping *)mapping; + [queryCache removeMatchingKeysForTargetID:targetID group:group]; + [queryCache addMatchingKeys:reset.documents forTargetID:targetID group:group]; + + } else if ([mapping isKindOfClass:[FSTUpdateMapping class]]) { + FSTUpdateMapping *update = (FSTUpdateMapping *)mapping; + [queryCache removeMatchingKeys:update.removedDocuments forTargetID:targetID group:group]; + [queryCache addMatchingKeys:update.addedDocuments forTargetID:targetID group:group]; + + } else { + FSTFail(@"Unknown mapping type: %@", mapping); + } + } + + // Update the resume token if the change includes one. Don't clear any preexisting value. + NSData *resumeToken = change.resumeToken; + if (resumeToken.length > 0) { + queryData = [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion + resumeToken:resumeToken]; + self.targetIDs[targetIDNumber] = queryData; + [self.queryCache addQueryData:queryData group:group]; + } + }]; + + // TODO(klimt): This could probably be an NSMutableDictionary. + __block FSTDocumentKeySet *changedDocKeys = [FSTDocumentKeySet keySet]; + [remoteEvent.documentUpdates + enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *doc, BOOL *stop) { + changedDocKeys = [changedDocKeys setByAddingObject:key]; + FSTMaybeDocument *existingDoc = [remoteDocuments entryForKey:key]; + // Make sure we don't apply an old document version to the remote cache, though we + // make an exception for [SnapshotVersion noVersion] which can happen for manufactured + // events (e.g. in the case of a limbo document resolution failing). + if (!existingDoc || [doc.version isEqual:[FSTSnapshotVersion noVersion]] || + [doc.version compare:existingDoc.version] != NSOrderedAscending) { + [remoteDocuments addEntry:doc]; + } else { + FSTLog( + @"FSTLocalStore Ignoring outdated watch update for %@. " + "Current version: %@ Watch version: %@", + key, existingDoc.version, doc.version); + } + + // The document might be garbage because it was unreferenced by everything. + // Make sure to mark it as garbage if it is... + [self.garbageCollector addPotentialGarbageKey:key]; + }]; + + // HACK: The only reason we allow omitting snapshot version is so we can synthesize remote events + // when we get permission denied errors while trying to resolve the state of a locally cached + // document that is in limbo. + FSTSnapshotVersion *lastRemoteVersion = [self.queryCache lastRemoteSnapshotVersion]; + FSTSnapshotVersion *remoteVersion = remoteEvent.snapshotVersion; + if (![remoteVersion isEqual:[FSTSnapshotVersion noVersion]]) { + FSTAssert([remoteVersion compare:lastRemoteVersion] != NSOrderedAscending, + @"Watch stream reverted to previous snapshot?? (%@ < %@)", remoteVersion, + lastRemoteVersion); + [self.queryCache setLastRemoteSnapshotVersion:remoteVersion group:group]; + } + + FSTDocumentKeySet *releasedWriteKeys = + [self releaseHeldBatchResultsWithGroup:group remoteDocuments:remoteDocuments]; + + [remoteDocuments applyToWriteGroup:group]; + + [self.persistence commitGroup:group]; + + // Union the two key sets. + __block FSTDocumentKeySet *keysToRecalc = changedDocKeys; + [releasedWriteKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + keysToRecalc = [keysToRecalc setByAddingObject:key]; + }]; + + return [self.localDocuments documentsForKeys:keysToRecalc]; +} + +- (void)notifyLocalViewChanges:(NSArray<FSTLocalViewChanges *> *)viewChanges { + FSTReferenceSet *localViewReferences = self.localViewReferences; + for (FSTLocalViewChanges *view in viewChanges) { + FSTQueryData *queryData = [self.queryCache queryDataForQuery:view.query]; + FSTAssert(queryData, @"Local view changes contain unallocated query."); + FSTTargetID targetID = queryData.targetID; + [localViewReferences addReferencesToKeys:view.addedKeys forID:targetID]; + [localViewReferences removeReferencesToKeys:view.removedKeys forID:targetID]; + } +} + +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID { + return [self.mutationQueue nextMutationBatchAfterBatchID:batchID]; +} + +- (nullable FSTMaybeDocument *)readDocument:(FSTDocumentKey *)key { + return [self.localDocuments documentForKey:key]; +} + +- (FSTQueryData *)allocateQuery:(FSTQuery *)query { + FSTQueryData *cached = [self.queryCache queryDataForQuery:query]; + FSTTargetID targetID; + if (cached) { + // This query has been listened to previously, so reuse the previous targetID. + // TODO(mcg): freshen last accessed date? + targetID = cached.targetID; + } else { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Allocate query"]; + + targetID = [self.targetIDGenerator nextID]; + cached = + [[FSTQueryData alloc] initWithQuery:query targetID:targetID purpose:FSTQueryPurposeListen]; + [self.queryCache addQueryData:cached group:group]; + + [self.persistence commitGroup:group]; + } + + // Sanity check to ensure that even when resuming a query it's not currently active. + FSTBoxedTargetID *boxedTargetID = @(targetID); + FSTAssert(!self.targetIDs[boxedTargetID], @"Tried to allocate an already allocated query: %@", + query); + self.targetIDs[boxedTargetID] = cached; + return cached; +} + +- (void)releaseQuery:(FSTQuery *)query { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Release query"]; + + FSTQueryData *queryData = [self.queryCache queryDataForQuery:query]; + FSTAssert(queryData, @"Tried to release nonexistent query: %@", query); + + [self.localViewReferences removeReferencesForID:queryData.targetID]; + if (self.garbageCollector.isEager) { + [self.queryCache removeQueryData:queryData group:group]; + } + [self.targetIDs removeObjectForKey:@(queryData.targetID)]; + + // If this was the last watch target, then we won't get any more watch snapshots, so we should + // release any held batch results. + if ([self.targetIDs count] == 0) { + FSTRemoteDocumentChangeBuffer *remoteDocuments = + [FSTRemoteDocumentChangeBuffer changeBufferWithCache:self.remoteDocumentCache]; + + [self releaseHeldBatchResultsWithGroup:group remoteDocuments:remoteDocuments]; + + [remoteDocuments applyToWriteGroup:group]; + } + + [self.persistence commitGroup:group]; +} + +- (FSTDocumentDictionary *)executeQuery:(FSTQuery *)query { + return [self.localDocuments documentsMatchingQuery:query]; +} + +- (FSTDocumentKeySet *)remoteDocumentKeysForTarget:(FSTTargetID)targetID { + return [self.queryCache matchingKeysForTargetID:targetID]; +} + +- (void)collectGarbage { + // Call collectGarbage regardless of whether isGCEnabled so the referenceSet doesn't continue to + // accumulate the garbage keys. + NSSet<FSTDocumentKey *> *garbage = [self.garbageCollector collectGarbage]; + if (garbage.count > 0) { + FSTWriteGroup *group = [self.persistence startGroupWithAction:@"Garbage Collection"]; + for (FSTDocumentKey *key in garbage) { + [self.remoteDocumentCache removeEntryForKey:key group:group]; + } + [self.persistence commitGroup:group]; + } +} + +/** + * Releases all the held mutation batches up to the current remote version received, and + * applies their mutations to the docs in the remote documents cache. + * + * @return the set of keys of docs that were modified by those writes. + */ +- (FSTDocumentKeySet *)releaseHeldBatchResultsWithGroup:(FSTWriteGroup *)group + remoteDocuments: + (FSTRemoteDocumentChangeBuffer *)remoteDocuments { + NSMutableArray<FSTMutationBatchResult *> *toRelease = [NSMutableArray array]; + for (FSTMutationBatchResult *batchResult in self.heldBatchResults) { + if (![self isRemoteUpToVersion:batchResult.commitVersion]) { + break; + } + [toRelease addObject:batchResult]; + } + + if (toRelease.count == 0) { + return [FSTDocumentKeySet keySet]; + } else { + [self.heldBatchResults removeObjectsInRange:NSMakeRange(0, toRelease.count)]; + return [self releaseBatchResults:toRelease group:group remoteDocuments:remoteDocuments]; + } +} + +- (BOOL)isRemoteUpToVersion:(FSTSnapshotVersion *)version { + // If there are no watch targets, then we won't get remote snapshots, and are always "up-to-date." + return [version compare:self.queryCache.lastRemoteSnapshotVersion] != NSOrderedDescending || + self.targetIDs.count == 0; +} + +- (BOOL)shouldHoldBatchResultWithVersion:(FSTSnapshotVersion *)version { + // Check if watcher isn't up to date or prior results are already held. + return ![self isRemoteUpToVersion:version] || self.heldBatchResults.count > 0; +} + +- (FSTDocumentKeySet *)releaseBatchResults:(NSArray<FSTMutationBatchResult *> *)batchResults + group:(FSTWriteGroup *)group + remoteDocuments:(FSTRemoteDocumentChangeBuffer *)remoteDocuments { + NSMutableArray<FSTMutationBatch *> *batches = [NSMutableArray array]; + for (FSTMutationBatchResult *batchResult in batchResults) { + [self applyBatchResult:batchResult toRemoteDocuments:remoteDocuments]; + [batches addObject:batchResult.batch]; + } + + return [self removeMutationBatches:batches group:group]; +} + +- (FSTDocumentKeySet *)removeMutationBatch:(FSTMutationBatch *)batch group:(FSTWriteGroup *)group { + return [self removeMutationBatches:@[ batch ] group:group]; +} + +/** Removes all the mutation batches named in the given array. */ +- (FSTDocumentKeySet *)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches + group:(FSTWriteGroup *)group { + // TODO(klimt): Could this be an NSMutableDictionary? + __block FSTDocumentKeySet *affectedDocs = [FSTDocumentKeySet keySet]; + + for (FSTMutationBatch *batch in batches) { + for (FSTMutation *mutation in batch.mutations) { + FSTDocumentKey *key = mutation.key; + affectedDocs = [affectedDocs setByAddingObject:key]; + } + } + + [self.mutationQueue removeMutationBatches:batches group:group]; + + return affectedDocs; +} + +- (void)applyBatchResult:(FSTMutationBatchResult *)batchResult + toRemoteDocuments:(FSTRemoteDocumentChangeBuffer *)remoteDocuments { + FSTMutationBatch *batch = batchResult.batch; + FSTDocumentKeySet *docKeys = batch.keys; + [docKeys enumerateObjectsUsingBlock:^(FSTDocumentKey *docKey, BOOL *stop) { + FSTMaybeDocument *_Nullable remoteDoc = [remoteDocuments entryForKey:docKey]; + FSTMaybeDocument *_Nullable doc = remoteDoc; + FSTSnapshotVersion *ackVersion = batchResult.docVersions[docKey]; + FSTAssert(ackVersion, @"docVersions should contain every doc in the write."); + if (!doc || [doc.version compare:ackVersion] == NSOrderedAscending) { + doc = [batch applyTo:doc documentKey:docKey mutationBatchResult:batchResult]; + if (!doc) { + FSTAssert(!remoteDoc, @"Mutation batch %@ applied to document %@ resulted in nil.", batch, + remoteDoc); + } else { + [remoteDocuments addEntry:doc]; + } + } + }]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalViewChanges.h b/Firestore/Source/Local/FSTLocalViewChanges.h new file mode 100644 index 0000000..d44959e --- /dev/null +++ b/Firestore/Source/Local/FSTLocalViewChanges.h @@ -0,0 +1,51 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentKeySet.h" + +@class FSTDocumentKey; +@class FSTDocumentSet; +@class FSTMutation; +@class FSTQuery; +@class FSTRemoteEvent; +@class FSTViewSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A set of changes to what documents are currently in view and out of view for a given query. + * These changes are sent to the LocalStore by the View (via the SyncEngine) and are used to pin / + * unpin documents as appropriate. + */ +@interface FSTLocalViewChanges : NSObject + ++ (instancetype)changesForQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys; + ++ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot; + +- (id)init NS_UNAVAILABLE; + +@property(nonatomic, strong, readonly) FSTQuery *query; +@property(nonatomic, strong) FSTDocumentKeySet *addedKeys; +@property(nonatomic, strong) FSTDocumentKeySet *removedKeys; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalViewChanges.m b/Firestore/Source/Local/FSTLocalViewChanges.m new file mode 100644 index 0000000..05407b2 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalViewChanges.m @@ -0,0 +1,76 @@ +/* + * 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 "FSTLocalViewChanges.h" + +#import "FSTDocument.h" +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalViewChanges () +- (instancetype)initWithQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTLocalViewChanges + ++ (instancetype)changesForViewSnapshot:(FSTViewSnapshot *)viewSnapshot { + FSTDocumentKeySet *addedKeys = [FSTDocumentKeySet keySet]; + FSTDocumentKeySet *removedKeys = [FSTDocumentKeySet keySet]; + + for (FSTDocumentViewChange *docChange in viewSnapshot.documentChanges) { + switch (docChange.type) { + case FSTDocumentViewChangeTypeAdded: + addedKeys = [addedKeys setByAddingObject:docChange.document.key]; + break; + + case FSTDocumentViewChangeTypeRemoved: + removedKeys = [removedKeys setByAddingObject:docChange.document.key]; + break; + + default: + // Do nothing. + break; + } + } + + return [self changesForQuery:viewSnapshot.query addedKeys:addedKeys removedKeys:removedKeys]; +} + ++ (instancetype)changesForQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys { + return + [[FSTLocalViewChanges alloc] initWithQuery:query addedKeys:addedKeys removedKeys:removedKeys]; +} + +- (instancetype)initWithQuery:(FSTQuery *)query + addedKeys:(FSTDocumentKeySet *)addedKeys + removedKeys:(FSTDocumentKeySet *)removedKeys { + self = [super init]; + if (self) { + _query = query; + _addedKeys = addedKeys; + _removedKeys = removedKeys; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalWriteResult.h b/Firestore/Source/Local/FSTLocalWriteResult.h new file mode 100644 index 0000000..4cd7d34 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalWriteResult.h @@ -0,0 +1,36 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentDictionary.h" +#import "FSTTypes.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The result of a write to the local store. */ +@interface FSTLocalWriteResult : NSObject + ++ (instancetype)resultForBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes; + +- (id)init __attribute__((unavailable("Use resultForBatchID:changes:"))); + +@property(nonatomic, assign, readonly) FSTBatchID batchID; +@property(nonatomic, strong, readonly) FSTMaybeDocumentDictionary *changes; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTLocalWriteResult.m b/Firestore/Source/Local/FSTLocalWriteResult.m new file mode 100644 index 0000000..7586686 --- /dev/null +++ b/Firestore/Source/Local/FSTLocalWriteResult.m @@ -0,0 +1,43 @@ +/* + * 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 "FSTLocalWriteResult.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTLocalWriteResult () +- (instancetype)initWithBatchID:(FSTBatchID)batchID + changes:(FSTMaybeDocumentDictionary *)changes NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTLocalWriteResult + ++ (instancetype)resultForBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes { + return [[FSTLocalWriteResult alloc] initWithBatchID:batchID changes:changes]; +} + +- (instancetype)initWithBatchID:(FSTBatchID)batchID changes:(FSTMaybeDocumentDictionary *)changes { + self = [super init]; + if (self) { + _batchID = batchID; + _changes = changes; + } + return self; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.h b/Firestore/Source/Local/FSTMemoryMutationQueue.h new file mode 100644 index 0000000..6d917b7 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.h @@ -0,0 +1,34 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTMutationQueue.h" + +@protocol FSTGarbageCollector; + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryMutationQueue : NSObject <FSTMutationQueue> + ++ (instancetype)mutationQueue; + +/** The garbage collector to notify about potential garbage keys. */ +@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryMutationQueue.m b/Firestore/Source/Local/FSTMemoryMutationQueue.m new file mode 100644 index 0000000..6118ad6 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryMutationQueue.m @@ -0,0 +1,441 @@ +/* + * 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 "FSTMemoryMutationQueue.h" + +#import "FSTAssert.h" +#import "FSTComparison.h" +#import "FSTDocumentKey.h" +#import "FSTDocumentReference.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTPath.h" +#import "FSTQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryMutationQueue () + +/** + * A FIFO queue of all mutations to apply to the backend. Mutations are added to the end of the + * queue as they're written, and removed from the front of the queue as the mutations become + * visible or are rejected. + * + * When successfully applied, mutations must be acknowledged by the write stream and made visible + * on the watch stream. It's possible for the watch stream to fall behind in which case the batches + * at the head of the queue will be acknowledged but held until the watch stream sees the changes. + * + * If a batch is rejected while there are held write acknowledgements at the head of the queue + * the rejected batch is converted to a tombstone: its mutations are removed but the batch remains + * in the queue. This maintains a simple consecutive ordering of batches in the queue. + * + * Once the held write acknowledgements become visible they are removed from the head of the queue + * along with any tombstones that follow. + */ +@property(nonatomic, strong, readonly) NSMutableArray<FSTMutationBatch *> *queue; + +/** An ordered mapping between documents and the mutation batch IDs. */ +@property(nonatomic, strong) FSTImmutableSortedSet<FSTDocumentReference *> *batchesByDocumentKey; + +/** The next value to use when assigning sequential IDs to each mutation batch. */ +@property(nonatomic, assign) FSTBatchID nextBatchID; + +/** The highest acknowledged mutation in the queue. */ +@property(nonatomic, assign) FSTBatchID highestAcknowledgedBatchID; + +/** + * The last received stream token from the server, used to acknowledge which responses the client + * has processed. Stream tokens are opaque checkpoint markers whose only real value is their + * inclusion in the next request. + */ +@property(nonatomic, strong, nullable) NSData *lastStreamToken; + +@end + +@implementation FSTMemoryMutationQueue + ++ (instancetype)mutationQueue { + return [[FSTMemoryMutationQueue alloc] init]; +} + +- (instancetype)init { + if (self = [super init]) { + _queue = [NSMutableArray array]; + _batchesByDocumentKey = + [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; + + _nextBatchID = 1; + _highestAcknowledgedBatchID = kFSTBatchIDUnknown; + } + return self; +} + +#pragma mark - FSTMutationQueue implementation + +- (void)startWithGroup:(FSTWriteGroup *)group { + // Note: The queue may be shutdown / started multiple times, since we maintain the queue for the + // duration of the app session in case a user logs out / back in. To behave like the + // LevelDB-backed MutationQueue (and accommodate tests that expect as much), we reset nextBatchID + // and highestAcknowledgedBatchID if the queue is empty. + if (self.isEmpty) { + self.nextBatchID = 1; + self.highestAcknowledgedBatchID = kFSTBatchIDUnknown; + } + FSTAssert(self.highestAcknowledgedBatchID < self.nextBatchID, + @"highestAcknowledgedBatchID must be less than the nextBatchID"); +} + +- (void)shutdown { +} + +- (BOOL)isEmpty { + // If the queue has any entries at all, the first entry must not be a tombstone (otherwise it + // would have been removed already). + return self.queue.count == 0; +} + +- (FSTBatchID)highestAcknowledgedBatchID { + return _highestAcknowledgedBatchID; +} + +- (void)acknowledgeBatch:(FSTMutationBatch *)batch + streamToken:(nullable NSData *)streamToken + group:(__unused FSTWriteGroup *)group { + NSMutableArray<FSTMutationBatch *> *queue = self.queue; + + FSTBatchID batchID = batch.batchID; + FSTAssert(batchID > self.highestAcknowledgedBatchID, + @"Mutation batchIDs must be acknowledged in order"); + + NSInteger batchIndex = [self indexOfExistingBatchID:batchID action:@"acknowledged"]; + + // Verify that the batch in the queue is the one to be acknowledged. + FSTMutationBatch *check = queue[(NSUInteger)batchIndex]; + FSTAssert(batchID == check.batchID, @"Queue ordering failure: expected batch %d, got batch %d", + batchID, check.batchID); + FSTAssert(![check isTombstone], @"Can't acknowledge a previously removed batch"); + + self.highestAcknowledgedBatchID = batchID; + self.lastStreamToken = streamToken; +} + +- (void)setLastStreamToken:(nullable NSData *)streamToken group:(__unused FSTWriteGroup *)group { + self.lastStreamToken = streamToken; +} + +- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray<FSTMutation *> *)mutations + group:(FSTWriteGroup *)group { + FSTAssert(mutations.count > 0, @"Mutation batches should not be empty"); + + FSTBatchID batchID = self.nextBatchID; + self.nextBatchID += 1; + + NSMutableArray<FSTMutationBatch *> *queue = self.queue; + if (queue.count > 0) { + FSTMutationBatch *prior = queue[queue.count - 1]; + FSTAssert(prior.batchID < batchID, @"Mutation batchIDs must be monotonically increasing order"); + } + + FSTMutationBatch *batch = [[FSTMutationBatch alloc] initWithBatchID:batchID + localWriteTime:localWriteTime + mutations:mutations]; + [queue addObject:batch]; + + // Track references by document key. + FSTImmutableSortedSet<FSTDocumentReference *> *references = self.batchesByDocumentKey; + for (FSTMutation *mutation in batch.mutations) { + references = [references + setByAddingObject:[[FSTDocumentReference alloc] initWithKey:mutation.key ID:batchID]]; + } + self.batchesByDocumentKey = references; + + return batch; +} + +- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID { + NSMutableArray<FSTMutationBatch *> *queue = self.queue; + + NSInteger index = [self indexOfBatchID:batchID]; + if (index < 0 || index >= queue.count) { + return nil; + } + + FSTMutationBatch *batch = queue[(NSUInteger)index]; + FSTAssert(batch.batchID == batchID, @"If found batch must match"); + return [batch isTombstone] ? nil : batch; +} + +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID { + NSMutableArray<FSTMutationBatch *> *queue = self.queue; + NSUInteger count = queue.count; + + // All batches with batchID <= self.highestAcknowledgedBatchID have been acknowledged so the + // first unacknowledged batch after batchID will have a batchID larger than both of these values. + batchID = MAX(batchID + 1, self.highestAcknowledgedBatchID); + + // The requested batchID may still be out of range so normalize it to the start of the queue. + NSInteger rawIndex = [self indexOfBatchID:batchID]; + NSUInteger index = rawIndex < 0 ? 0 : (NSUInteger)rawIndex; + + // Finally return the first non-tombstone batch. + for (; index < count; index++) { + FSTMutationBatch *batch = queue[index]; + if (![batch isTombstone]) { + return batch; + } + } + + return nil; +} + +- (NSArray<FSTMutationBatch *> *)allMutationBatches { + return [self allLiveMutationBatchesBeforeIndex:self.queue.count]; +} + +- (NSArray<FSTMutationBatch *> *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID { + NSMutableArray<FSTMutationBatch *> *queue = self.queue; + NSUInteger count = queue.count; + + NSInteger endIndex = [self indexOfBatchID:batchID]; + if (endIndex < 0) { + endIndex = 0; + } else if (endIndex >= count) { + endIndex = count; + } else { + // The endIndex is in the queue so increment to pull everything in the queue including it. + endIndex += 1; + } + + return [self allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex]; +} + +- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKey: + (FSTDocumentKey *)documentKey { + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:documentKey ID:0]; + + NSMutableArray<FSTMutationBatch *> *result = [NSMutableArray array]; + FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) { + if (![documentKey isEqualToKey:reference.key]) { + *stop = YES; + return; + } + + FSTMutationBatch *batch = [self lookupMutationBatch:reference.ID]; + FSTAssert(batch, @"Batches in the index must exist in the main table"); + [result addObject:batch]; + }; + + [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block]; + return result; +} + +- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingQuery:(FSTQuery *)query { + // Use the query path as a prefix for testing if a document matches the query. + FSTResourcePath *prefix = query.path; + int immediateChildrenPathLength = prefix.length + 1; + + // Construct a document reference for actually scanning the index. Unlike the prefix, the document + // key in this reference must have an even number of segments. The empty segment can be used as + // a suffix of the query path because it precedes all other segments in an ordered traversal. + FSTResourcePath *startPath = query.path; + if (![FSTDocumentKey isDocumentKey:startPath]) { + startPath = [startPath pathByAppendingSegment:@""]; + } + FSTDocumentReference *start = + [[FSTDocumentReference alloc] initWithKey:[FSTDocumentKey keyWithPath:startPath] ID:0]; + + // Find unique batchIDs referenced by all documents potentially matching the query. + __block FSTImmutableSortedSet<NSNumber *> *uniqueBatchIDs = + [FSTImmutableSortedSet setWithComparator:FSTNumberComparator]; + FSTDocumentReferenceBlock block = ^(FSTDocumentReference *reference, BOOL *stop) { + FSTResourcePath *rowKeyPath = reference.key.path; + if (![prefix isPrefixOfPath:rowKeyPath]) { + *stop = YES; + return; + } + + // Rows with document keys more than one segment longer than the query path can't be matches. + // For example, a query on 'rooms' can't match the document /rooms/abc/messages/xyx. + // TODO(mcg): we'll need a different scanner when we implement ancestor queries. + if (rowKeyPath.length != immediateChildrenPathLength) { + return; + } + + uniqueBatchIDs = [uniqueBatchIDs setByAddingObject:@(reference.ID)]; + }; + [self.batchesByDocumentKey enumerateObjectsFrom:start to:nil usingBlock:block]; + + // Construct an array of matching batches, sorted by batchID to ensure that multiple mutations + // affecting the same document key are applied in order. + NSMutableArray<FSTMutationBatch *> *result = [NSMutableArray array]; + [uniqueBatchIDs enumerateObjectsUsingBlock:^(NSNumber *batchID, BOOL *stop) { + FSTMutationBatch *batch = [self lookupMutationBatch:[batchID intValue]]; + if (batch) { + [result addObject:batch]; + } + }]; + + return result; +} + +- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches group:(FSTWriteGroup *)group { + NSUInteger batchCount = batches.count; + FSTAssert(batchCount > 0, @"Should not remove mutations when none exist."); + + FSTBatchID firstBatchID = batches[0].batchID; + + NSMutableArray<FSTMutationBatch *> *queue = self.queue; + NSUInteger queueCount = queue.count; + + // Find the position of the first batch for removal. This need not be the first entry in the + // queue. + NSUInteger startIndex = [self indexOfExistingBatchID:firstBatchID action:@"removed"]; + FSTAssert(queue[startIndex].batchID == firstBatchID, @"Removed batches must exist in the queue"); + + // Check that removed batches are contiguous (while excluding tombstones). + NSUInteger batchIndex = 1; + NSUInteger queueIndex = startIndex + 1; + while (batchIndex < batchCount && queueIndex < queueCount) { + FSTMutationBatch *batch = queue[queueIndex]; + if ([batch isTombstone]) { + queueIndex++; + continue; + } + + FSTAssert(batch.batchID == batches[batchIndex].batchID, + @"Removed batches must be contiguous in the queue"); + batchIndex++; + queueIndex++; + } + + // Only actually remove batches if removing at the front of the queue. Previously rejected batches + // may have left tombstones in the queue, so expand the removal range to include any tombstones. + if (startIndex == 0) { + for (; queueIndex < queueCount; queueIndex++) { + FSTMutationBatch *batch = queue[queueIndex]; + if (![batch isTombstone]) { + break; + } + } + + NSUInteger length = queueIndex - startIndex; + [queue removeObjectsInRange:NSMakeRange(startIndex, length)]; + + } else { + // Mark tombstones + for (NSUInteger i = startIndex; i < queueIndex; i++) { + queue[i] = [queue[i] toTombstone]; + } + } + + // Remove entries from the index too. + id<FSTGarbageCollector> garbageCollector = self.garbageCollector; + FSTImmutableSortedSet<FSTDocumentReference *> *references = self.batchesByDocumentKey; + for (FSTMutationBatch *batch in batches) { + FSTBatchID batchID = batch.batchID; + for (FSTMutation *mutation in batch.mutations) { + FSTDocumentKey *key = mutation.key; + [garbageCollector addPotentialGarbageKey:key]; + + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:batchID]; + references = [references setByRemovingObject:reference]; + } + } + self.batchesByDocumentKey = references; +} + +- (void)performConsistencyCheck { + if (self.queue.count == 0) { + FSTAssert([self.batchesByDocumentKey isEmpty], + @"Document leak -- detected dangling mutation references when queue is empty."); + } +} + +#pragma mark - FSTGarbageSource implementation + +- (BOOL)containsKey:(FSTDocumentKey *)key { + // Create a reference with a zero ID as the start position to find any document reference with + // this key. + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0]; + + NSEnumerator<FSTDocumentReference *> *enumerator = + [self.batchesByDocumentKey objectEnumeratorFrom:reference]; + FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key; + return [firstKey isEqual:key]; +} + +#pragma mark - Helpers + +/** + * A private helper that collects all the mutation batches in the queue up to but not including + * the given endIndex. All tombstones in the queue are excluded. + */ +- (NSArray<FSTMutationBatch *> *)allLiveMutationBatchesBeforeIndex:(NSUInteger)endIndex { + NSMutableArray<FSTMutationBatch *> *result = [NSMutableArray arrayWithCapacity:endIndex]; + + NSUInteger index = 0; + for (FSTMutationBatch *batch in self.queue) { + if (index++ >= endIndex) break; + + if (![batch isTombstone]) { + [result addObject:batch]; + } + } + + return result; +} + +/** + * Finds the index of the given batchID in the mutation queue. This operation is O(1). + * + * @return The computed index of the batch with the given batchID, based on the state of the + * queue. Note this index can negative if the requested batchID has already been removed from + * the queue or past the end of the queue if the batchID is larger than the last added batch. + */ +- (NSInteger)indexOfBatchID:(FSTBatchID)batchID { + NSMutableArray<FSTMutationBatch *> *queue = self.queue; + NSUInteger count = queue.count; + if (count == 0) { + // As an index this is past the end of the queue + return 0; + } + + // Examine the front of the queue to figure out the difference between the batchID and indexes + // in the array. Note that since the queue is ordered by batchID, if the first batch has a larger + // batchID then the requested batchID doesn't exist in the queue. + FSTMutationBatch *firstBatch = queue[0]; + FSTBatchID firstBatchID = firstBatch.batchID; + return batchID - firstBatchID; +} + +/** + * Finds the index of the given batchID in the mutation queue and asserts that the resulting + * index is within the bounds of the queue. + * + * @param batchID The batchID to search for + * @param action A description of what the caller is doing, phrased in passive form (e.g. + * "acknowledged" in a routine that acknowledges batches). + */ +- (NSUInteger)indexOfExistingBatchID:(FSTBatchID)batchID action:(NSString *)action { + NSInteger index = [self indexOfBatchID:batchID]; + FSTAssert(index >= 0 && index < self.queue.count, @"Batches must exist to be %@", action); + return (NSUInteger)index; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.h b/Firestore/Source/Local/FSTMemoryPersistence.h new file mode 100644 index 0000000..c52962a --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryPersistence.h @@ -0,0 +1,33 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTPersistence.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An in-memory implementation of the FSTPersistence protocol. Values are stored only in RAM and + * are never persisted to any durable storage. + */ +@interface FSTMemoryPersistence : NSObject <FSTPersistence> + ++ (instancetype)persistence; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryPersistence.m b/Firestore/Source/Local/FSTMemoryPersistence.m new file mode 100644 index 0000000..9caf3e7 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryPersistence.m @@ -0,0 +1,107 @@ +/* + * 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 "FSTMemoryPersistence.h" + +#import "FSTAssert.h" +#import "FSTMemoryMutationQueue.h" +#import "FSTMemoryQueryCache.h" +#import "FSTMemoryRemoteDocumentCache.h" +#import "FSTUser.h" +#import "FSTWriteGroup.h" +#import "FSTWriteGroupTracker.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryPersistence () +@property(nonatomic, strong, nonnull) FSTWriteGroupTracker *writeGroupTracker; +@property(nonatomic, strong, nonnull) + NSMutableDictionary<FSTUser *, id<FSTMutationQueue>> *mutationQueues; +@property(nonatomic, assign, getter=isStarted) BOOL started; +@end + +@implementation FSTMemoryPersistence { + /** + * The FSTQueryCache representing the persisted cache of queries. + * + * Note that this is retained here to make it easier to write tests affecting both the in-memory + * and LevelDB-backed persistence layers. Tests can create a new FSTLocalStore wrapping this + * FSTPersistence instance and this will make the in-memory persistence layer behave as if it + * were actually persisting values. + */ + FSTMemoryQueryCache *_queryCache; + + /** The FSTRemoteDocumentCache representing the persisted cache of remote documents. */ + FSTMemoryRemoteDocumentCache *_remoteDocumentCache; +} + ++ (instancetype)persistence { + return [[FSTMemoryPersistence alloc] init]; +} + +- (instancetype)init { + if (self = [super init]) { + _writeGroupTracker = [FSTWriteGroupTracker tracker]; + _queryCache = [[FSTMemoryQueryCache alloc] init]; + _remoteDocumentCache = [[FSTMemoryRemoteDocumentCache alloc] init]; + _mutationQueues = [NSMutableDictionary dictionary]; + } + return self; +} + +- (BOOL)start:(NSError **)error { + // No durable state to read on startup. + FSTAssert(!self.isStarted, @"FSTMemoryPersistence double-started!"); + self.started = YES; + return YES; +} + +- (void)shutdown { + // No durable state to ensure is closed on shutdown. + FSTAssert(self.isStarted, @"FSTMemoryPersistence shutdown without start!"); + self.started = NO; +} + +- (id<FSTMutationQueue>)mutationQueueForUser:(FSTUser *)user { + id<FSTMutationQueue> queue = self.mutationQueues[user]; + if (!queue) { + queue = [FSTMemoryMutationQueue mutationQueue]; + self.mutationQueues[user] = queue; + } + return queue; +} + +- (id<FSTQueryCache>)queryCache { + return _queryCache; +} + +- (id<FSTRemoteDocumentCache>)remoteDocumentCache { + return _remoteDocumentCache; +} + +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { + return [self.writeGroupTracker startGroupWithAction:action]; +} + +- (void)commitGroup:(FSTWriteGroup *)group { + [self.writeGroupTracker endGroup:group]; + + FSTAssert(group.isEmpty, @"Memory persistence shouldn't use write groups: %@", group.action); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.h b/Firestore/Source/Local/FSTMemoryQueryCache.h new file mode 100644 index 0000000..58e0133 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryQueryCache.h @@ -0,0 +1,30 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTQueryCache.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An implementation of the FSTQueryCache protocol that merely keeps queries in memory, suitable + * for online only clients with persistence disabled. + */ +@interface FSTMemoryQueryCache : NSObject <FSTQueryCache> +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryQueryCache.m b/Firestore/Source/Local/FSTMemoryQueryCache.m new file mode 100644 index 0000000..1466caa --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryQueryCache.m @@ -0,0 +1,131 @@ +/* + * 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 "FSTMemoryQueryCache.h" + +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTReferenceSet.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryQueryCache () + +/** Maps a query to the data about that query. */ +@property(nonatomic, strong, readonly) NSMutableDictionary<FSTQuery *, FSTQueryData *> *queries; + +/** A ordered bidirectional mapping between documents and the remote target IDs. */ +@property(nonatomic, strong, readonly) FSTReferenceSet *references; + +/** The highest numbered target ID encountered. */ +@property(nonatomic, assign) FSTTargetID highestTargetID; + +@end + +@implementation FSTMemoryQueryCache { + /** The last received snapshot version. */ + FSTSnapshotVersion *_lastRemoteSnapshotVersion; +} + +- (instancetype)init { + if (self = [super init]) { + _queries = [NSMutableDictionary dictionary]; + _references = [[FSTReferenceSet alloc] init]; + _lastRemoteSnapshotVersion = [FSTSnapshotVersion noVersion]; + } + return self; +} + +#pragma mark - FSTQueryCache implementation +#pragma mark Query tracking + +- (void)start { + // Nothing to do. +} + +- (void)shutdown { + // No resources to release. +} + +- (FSTTargetID)highestTargetID { + return _highestTargetID; +} + +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion { + return _lastRemoteSnapshotVersion; +} + +- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + group:(FSTWriteGroup *)group { + _lastRemoteSnapshotVersion = snapshotVersion; +} + +- (void)addQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group { + self.queries[queryData.query] = queryData; + if (queryData.targetID > self.highestTargetID) { + self.highestTargetID = queryData.targetID; + } +} + +- (void)removeQueryData:(FSTQueryData *)queryData group:(__unused FSTWriteGroup *)group { + [self.queries removeObjectForKey:queryData.query]; + [self.references removeReferencesForID:queryData.targetID]; +} + +- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query { + return self.queries[query]; +} + +#pragma mark Reference tracking + +- (void)addMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(__unused FSTWriteGroup *)group { + [self.references addReferencesToKeys:keys forID:targetID]; +} + +- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(__unused FSTWriteGroup *)group { + [self.references removeReferencesToKeys:keys forID:targetID]; +} + +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(__unused FSTWriteGroup *)group { + [self.references removeReferencesForID:targetID]; +} + +- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID { + return [self.references referencedKeysForID:targetID]; +} + +#pragma mark - FSTGarbageSource implementation + +- (nullable id<FSTGarbageCollector>)garbageCollector { + return self.references.garbageCollector; +} + +- (void)setGarbageCollector:(nullable id<FSTGarbageCollector>)garbageCollector { + self.references.garbageCollector = garbageCollector; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + return [self.references containsKey:key]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h new file mode 100644 index 0000000..aca0ca1 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h @@ -0,0 +1,29 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTRemoteDocumentCache.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryRemoteDocumentCache : NSObject <FSTRemoteDocumentCache> + +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m new file mode 100644 index 0000000..175be43 --- /dev/null +++ b/Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m @@ -0,0 +1,84 @@ +/* + * 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 "FSTMemoryRemoteDocumentCache.h" + +#import "FSTDocument.h" +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKey.h" +#import "FSTPath.h" +#import "FSTQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMemoryRemoteDocumentCache () + +/** Underlying cache of documents. */ +@property(nonatomic, strong) FSTMaybeDocumentDictionary *docs; + +@end + +@implementation FSTMemoryRemoteDocumentCache + +- (instancetype)init { + if (self = [super init]) { + _docs = [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + } + return self; +} + +- (void)shutdown { +} + +- (void)addEntry:(FSTMaybeDocument *)document group:(FSTWriteGroup *)group { + self.docs = [self.docs dictionaryBySettingObject:document forKey:document.key]; +} + +- (void)removeEntryForKey:(FSTDocumentKey *)key group:(FSTWriteGroup *)group { + self.docs = [self.docs dictionaryByRemovingObjectForKey:key]; +} + +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)key { + return self.docs[key]; +} + +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query { + FSTDocumentDictionary *result = [FSTDocumentDictionary documentDictionary]; + + // Documents are ordered by key, so we can use a prefix scan to narrow down the documents + // we need to match the query against. + FSTDocumentKey *prefix = [FSTDocumentKey keyWithPath:[query.path pathByAppendingSegment:@""]]; + NSEnumerator<FSTDocumentKey *> *enumerator = [self.docs keyEnumeratorFrom:prefix]; + for (FSTDocumentKey *key in enumerator) { + if (![query.path isPrefixOfPath:key.path]) { + break; + } + FSTMaybeDocument *maybeDoc = self.docs[key]; + if (![maybeDoc isKindOfClass:[FSTDocument class]]) { + continue; + } + FSTDocument *doc = (FSTDocument *)maybeDoc; + if ([query matchesDocument:doc]) { + result = [result dictionaryBySettingObject:doc forKey:doc.key]; + } + } + + return result; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTMutationQueue.h b/Firestore/Source/Local/FSTMutationQueue.h new file mode 100644 index 0000000..c822b96 --- /dev/null +++ b/Firestore/Source/Local/FSTMutationQueue.h @@ -0,0 +1,159 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTGarbageCollector.h" +#import "FSTTypes.h" + +@class FSTDocumentKey; +@class FSTMutation; +@class FSTMutationBatch; +@class FSTQuery; +@class FSTTimestamp; +@class FSTWriteGroup; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTMutationQueue + +/** A queue of mutations to apply to the remote store. */ +@protocol FSTMutationQueue <NSObject, FSTGarbageSource> + +/** + * Starts the mutation queue, performing any initial reads that might be required to establish + * invariants, etc. + * + * After starting, the mutation queue must guarantee that the highestAcknowledgedBatchID is less + * than nextBatchID. This prevents the local store from creating new batches that the mutation + * queue would consider erroneously acknowledged. + */ +- (void)startWithGroup:(FSTWriteGroup *)group; + +/** Shuts this mutation queue down, closing open files, etc. */ +- (void)shutdown; + +/** Returns YES if this queue contains no mutation batches. */ +- (BOOL)isEmpty; + +/** + * Returns the next FSTBatchID that will be assigned to a new mutation batch. + * + * Callers generally don't care about this value except to test that the mutation queue is + * properly maintaining the invariant that highestAcknowledgedBatchID is less than nextBatchID. + */ +- (FSTBatchID)nextBatchID; + +/** + * Returns the highest batchID that has been acknowledged. If no batches have been acknowledged + * or if there are no batches in the queue this can return kFSTBatchIDUnknown. + */ +- (FSTBatchID)highestAcknowledgedBatchID; + +/** Acknowledges the given batch. */ +- (void)acknowledgeBatch:(FSTMutationBatch *)batch + streamToken:(nullable NSData *)streamToken + group:(FSTWriteGroup *)group; + +/** Returns the current stream token for this mutation queue. */ +- (nullable NSData *)lastStreamToken; + +/** Sets the stream token for this mutation queue. */ +- (void)setLastStreamToken:(nullable NSData *)streamToken group:(FSTWriteGroup *)group; + +/** Creates a new mutation batch and adds it to this mutation queue. */ +- (FSTMutationBatch *)addMutationBatchWithWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray<FSTMutation *> *)mutations + group:(FSTWriteGroup *)group; + +/** Loads the mutation batch with the given batchID. */ +- (nullable FSTMutationBatch *)lookupMutationBatch:(FSTBatchID)batchID; + +/** + * Gets the first unacknowledged mutation batch after the passed in batchId in the mutation queue + * or nil if empty. + * + * @param batchID The batch to search after, or kFSTBatchIDUnknown for the first mutation in the + * queue. + * + * @return the next mutation or nil if there wasn't one. + */ +- (nullable FSTMutationBatch *)nextMutationBatchAfterBatchID:(FSTBatchID)batchID; + +/** Gets all mutation batches in the mutation queue. */ +// TODO(mikelehen): PERF: Current consumer only needs mutated keys; if we can provide that +// cheaply, we should replace this. +- (NSArray<FSTMutationBatch *> *)allMutationBatches; + +/** + * Finds all mutations with a batchID less than or equal to the given batchID. + * + * Generally the caller should be asking for the next unacknowledged batchID and the number of + * acknowledged batches should be very small when things are functioning well. + * + * @param batchID The batch to search through. + * + * @return an NSArray containing all batches with matching batchIDs. + */ +// TODO(mcg): This should really return NSEnumerator and the caller should be adjusted to only +// loop through these once. +- (NSArray<FSTMutationBatch *> *)allMutationBatchesThroughBatchID:(FSTBatchID)batchID; + +/** + * Finds all mutation batches that could @em possibly affect the given document key. Not all + * mutations in a batch will necessarily affect the document key, so when looping through the + * batch you'll need to check that the mutation itself matches the key. + * + * Note that because of this requirement implementations are free to return mutation batches that + * don't contain the document key at all if it's convenient. + */ +// TODO(mcg): This should really return an NSEnumerator +// also for b/32992024, all backing stores should really index by document key +- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingDocumentKey: + (FSTDocumentKey *)documentKey; + +/** + * Finds all mutation batches that could affect the results for the given query. Not all + * mutations in a batch will necessarily affect the query, so when looping through the batch + * you'll need to check that the mutation itself matches the query. + * + * Note that because of this requirement implementations are free to return mutation batches that + * don't match the query at all if it's convenient. + * + * NOTE: A FSTPatchMutation does not need to include all fields in the query filter criteria in + * order to be a match (but any fields it does contain do need to match). + */ +// TODO(mikelehen): This should perhaps return an NSEnumerator, though I'm not sure we can avoid +// loading them all in memory. +- (NSArray<FSTMutationBatch *> *)allMutationBatchesAffectingQuery:(FSTQuery *)query; + +/** + * Removes the given mutation batches from the queue. This is useful in two circumstances: + * + * + Removing applied mutations from the head of the queue + * + Removing rejected mutations from anywhere in the queue + * + * In both cases, the array of mutations to remove must be a contiguous range of batchIds. This is + * most easily accomplished by loading mutations with @a -allMutationBatchesThroughBatchID:. + */ +- (void)removeMutationBatches:(NSArray<FSTMutationBatch *> *)batches group:(FSTWriteGroup *)group; + +/** Performs a consistency check, examining the mutation queue for any leaks, if possible. */ +- (void)performConsistencyCheck; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTNoOpGarbageCollector.h b/Firestore/Source/Local/FSTNoOpGarbageCollector.h new file mode 100644 index 0000000..8873a1b --- /dev/null +++ b/Firestore/Source/Local/FSTNoOpGarbageCollector.h @@ -0,0 +1,32 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTGarbageCollector.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A garbage collector implementation that does absolutely nothing. It ignores all + * addGarbageSource: and addPotentialGarbageKey: messages and never produces any garbage. + */ +@interface FSTNoOpGarbageCollector : NSObject <FSTGarbageCollector> +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTNoOpGarbageCollector.m b/Firestore/Source/Local/FSTNoOpGarbageCollector.m new file mode 100644 index 0000000..6e035ab --- /dev/null +++ b/Firestore/Source/Local/FSTNoOpGarbageCollector.m @@ -0,0 +1,45 @@ +/* + * 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 "FSTNoOpGarbageCollector.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTNoOpGarbageCollector + +- (BOOL)isEager { + return NO; +} + +- (void)addGarbageSource:(id<FSTGarbageSource>)garbageSource { + // Not tracking garbage so don't track sources. +} + +- (void)removeGarbageSource:(id<FSTGarbageSource>)garbageSource { + // Not tracking garbage so don't track sources. +} + +- (void)addPotentialGarbageKey:(FSTDocumentKey *)key { + // Not tracking garbage so ignore. +} + +- (NSSet<FSTDocumentKey *> *)collectGarbage { + return [NSSet set]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTPersistence.h b/Firestore/Source/Local/FSTPersistence.h new file mode 100644 index 0000000..cf07a9e --- /dev/null +++ b/Firestore/Source/Local/FSTPersistence.h @@ -0,0 +1,103 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTUser; +@class FSTWriteGroup; +@protocol FSTMutationQueue; +@protocol FSTQueryCache; +@protocol FSTRemoteDocumentCache; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTPersistence is the lowest-level shared interface to persistent storage in Firestore. + * + * FSTPersistence is used to create FSTMutationQueue and FSTRemoteDocumentCache instances backed + * by persistence (which might be in-memory or LevelDB). + * + * FSTPersistence also exposes an API to create and commit FSTWriteGroup instances. + * Implementations of FSTWriteGroup/FSTPersistence only need to guarantee that writes made + * against the FSTWriteGroup are not made to durable storage until commitGroup:action: is called + * here. Since memory-only storage components do not alter durable storage, they are free to ignore + * the group. + * + * This contract is enough to allow the FSTLocalStore be be written independently of whether or not + * the stored state actually is durably persisted. If persistent storage is enabled, writes are + * grouped together to avoid inconsistent state that could cause crashes. + * + * Concretely, when persistent storage is enabled, the persistent versions of FSTMutationQueue, + * FSTRemoteDocumentCache, and others (the mutators) will defer their writes into an FSTWriteGroup. + * Once the local store has completed one logical operation, it commits the write group using + * [FSTPersistence commitGroup:action:]. + * + * When persistent storage is disabled, the non-persistent versions of the mutators ignore the + * FSTWriteGroup and [FSTPersistence commitGroup:action:] is a no-op. This short-cut is allowed + * because memory-only storage leaves no state so it cannot be inconsistent. + * + * This simplifies the implementations of the mutators and allows memory-only implementations to + * supplement the persistent ones without requiring any special dual-store implementation of + * FSTPersistence. The cost is that the FSTLocalStore needs to be slightly careful about the order + * of its reads and writes in order to avoid relying on being able to read back uncommitted writes. + */ +@protocol FSTPersistence <NSObject> + +/** + * Starts persistent storage, opening the database or similar. + * + * @param error An error object that will be populated if startup fails. + * @return YES if persistent storage started successfully, NO otherwise. + */ +- (BOOL)start:(NSError **)error; + +/** Releases any resources held during eager shutdown. */ +- (void)shutdown; + +/** + * Returns an FSTMutationQueue representing the persisted mutations for the given user. + * + * <p>Note: The implementation is free to return the same instance every time this is called for a + * given user. In particular, the memory-backed implementation does this to emulate the persisted + * implementation to the extent possible (e.g. in the case of uid switching from + * sally=>jack=>sally, sally's mutation queue will be preserved). + */ +- (id<FSTMutationQueue>)mutationQueueForUser:(FSTUser *)user; + +/** Creates an FSTQueryCache representing the persisted cache of queries. */ +- (id<FSTQueryCache>)queryCache; + +/** Creates an FSTRemoteDocumentCache representing the persisted cache of remote documents. */ +- (id<FSTRemoteDocumentCache>)remoteDocumentCache; + +/** + * Creates an FSTWriteGroup with the specified action description. + * + * @param action A description of the action performed by this group, used for logging. + * @return The created group. + */ +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action; + +/** + * Commits all accumulated changes in the given group. If there are no changes this is a no-op. + * + * @param group The group of changes to write as a unit. + */ +- (void)commitGroup:(FSTWriteGroup *)group; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryCache.h b/Firestore/Source/Local/FSTQueryCache.h new file mode 100644 index 0000000..87ee342 --- /dev/null +++ b/Firestore/Source/Local/FSTQueryCache.h @@ -0,0 +1,113 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentKeySet.h" +#import "FSTGarbageCollector.h" +#import "FSTTypes.h" + +@class FSTDocumentKey; +@class FSTDocumentSet; +@class FSTMaybeDocument; +@class FSTQuery; +@class FSTQueryData; +@class FSTWriteGroup; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represents cached queries received from the remote backend. This contains both a mapping between + * queries and the documents that matched them according to the server, but also metadata about the + * queries. + * + * The cache is keyed by FSTQuery and entries in the cache are FSTQueryData instances. + */ +@protocol FSTQueryCache <NSObject, FSTGarbageSource> + +/** Starts the query cache up. */ +- (void)start; + +/** Shuts this cache down, closing open files, etc. */ +- (void)shutdown; + +/** + * Returns the highest target ID of any query in the cache. Typically called during startup to + * seed a target ID generator and avoid collisions with existing queries. If there are no queries + * in the cache, returns zero. + */ +- (FSTTargetID)highestTargetID; + +/** + * A global snapshot version representing the last consistent snapshot we received from the + * backend. This is monotonically increasing and any snapshots received from the backend prior to + * this version (e.g. for targets resumed with a resume_token) should be suppressed (buffered) + * until the backend has caught up to this snapshot version again. This prevents our cache from + * ever going backwards in time. + * + * This is updated whenever our we get a TargetChange with a read_time and empty target_ids. + */ +- (FSTSnapshotVersion *)lastRemoteSnapshotVersion; + +/** + * Set the snapshot version representing the last consistent snapshot received from the backend. + * (see -lastRemoteSnapshotVersion for more details). + * + * @param snapshotVersion The new snapshot version. + */ +- (void)setLastRemoteSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + group:(FSTWriteGroup *)group; + +/** + * Adds or replaces an entry in the cache. + * + * The cache key is extracted from `queryData.query`. If there is already a cache entry for the + * key, it will be replaced. + * + * @param queryData An FSTQueryData instance to put in the cache. + */ +- (void)addQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group; + +/** Removes the cached entry for the given query data (no-op if no entry exists). */ +- (void)removeQueryData:(FSTQueryData *)queryData group:(FSTWriteGroup *)group; + +/** + * Looks up an FSTQueryData entry in the cache. + * + * @param query The query corresponding to the entry to look up. + * @return The cached FSTQueryData entry, or nil if the cache has no entry for the query. + */ +- (nullable FSTQueryData *)queryDataForQuery:(FSTQuery *)query; + +/** Adds the given document keys to cached query results of the given target ID. */ +- (void)addMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(FSTWriteGroup *)group; + +/** Removes the given document keys from the cached query results of the given target ID. */ +- (void)removeMatchingKeys:(FSTDocumentKeySet *)keys + forTargetID:(FSTTargetID)targetID + group:(FSTWriteGroup *)group; + +/** Removes all the keys in the query results of the given target ID. */ +- (void)removeMatchingKeysForTargetID:(FSTTargetID)targetID group:(FSTWriteGroup *)group; + +- (FSTDocumentKeySet *)matchingKeysForTargetID:(FSTTargetID)targetID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryData.h b/Firestore/Source/Local/FSTQueryData.h new file mode 100644 index 0000000..060fd78 --- /dev/null +++ b/Firestore/Source/Local/FSTQueryData.h @@ -0,0 +1,82 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTTypes.h" + +@class FSTQuery; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** An enumeration of the different purposes we have for queries. */ +typedef NS_ENUM(NSInteger, FSTQueryPurpose) { + /** A regular, normal query. */ + FSTQueryPurposeListen, + + /** The query was used to refill a query after an existence filter mismatch. */ + FSTQueryPurposeExistenceFilterMismatch, + + /** The query was used to resolve a limbo document. */ + FSTQueryPurposeLimboResolution, +}; + +/** An immutable set of metadata that the store will need to keep track of for each query. */ +@interface FSTQueryData : NSObject + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + purpose:(FSTQueryPurpose)purpose + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken NS_DESIGNATED_INITIALIZER; + +/** Convenience initializer for use when creating an FSTQueryData for the first time. */ +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + purpose:(FSTQueryPurpose)purpose; + +- (instancetype)init NS_UNAVAILABLE; + +/** Creates a new query data instance with an updated snapshot version and resume token. */ +- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken; + +/** The query being listened to. */ +@property(nonatomic, strong, readonly) FSTQuery *query; + +/** + * The targetID to which the query corresponds, assigned by the FSTLocalStore for user queries or + * the FSTSyncEngine for limbo queries. + */ +@property(nonatomic, assign, readonly) FSTTargetID targetID; + +/** The purpose of the query. */ +@property(nonatomic, assign, readonly) FSTQueryPurpose purpose; + +/** The latest snapshot version seen for this target. */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** + * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting + * without retransmitting all the data that matches the query. The resume token essentially + * identifies a point in time from which the server should resume sending results. + */ +@property(nonatomic, copy, readonly) NSData *resumeToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTQueryData.m b/Firestore/Source/Local/FSTQueryData.m new file mode 100644 index 0000000..438f229 --- /dev/null +++ b/Firestore/Source/Local/FSTQueryData.m @@ -0,0 +1,93 @@ +/* + * 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 "FSTQueryData.h" + +#import "FSTQuery.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTQueryData + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + purpose:(FSTQueryPurpose)purpose + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + self = [super init]; + if (self) { + _query = query; + _targetID = targetID; + _purpose = purpose; + _snapshotVersion = snapshotVersion; + _resumeToken = [resumeToken copy]; + } + return self; +} + +- (instancetype)initWithQuery:(FSTQuery *)query + targetID:(FSTTargetID)targetID + purpose:(FSTQueryPurpose)purpose { + return [self initWithQuery:query + targetID:targetID + purpose:purpose + snapshotVersion:[FSTSnapshotVersion noVersion] + resumeToken:[NSData data]]; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTQueryData class]]) { + return NO; + } + + FSTQueryData *other = (FSTQueryData *)object; + return [self.query isEqual:other.query] && self.targetID == other.targetID && + self.purpose == other.purpose && [self.snapshotVersion isEqual:other.snapshotVersion] && + [self.resumeToken isEqual:other.resumeToken]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.query hash]; + result = result * 31 + self.targetID; + result = result * 31 + self.purpose; + result = result * 31 + [self.snapshotVersion hash]; + result = result * 31 + [self.resumeToken hash]; + return result; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"<FSTQueryData: query:%@ target:%d purpose:%lu version:%@ resumeToken:%@)>", + self.query, self.targetID, (unsigned long)self.purpose, self.snapshotVersion, + self.resumeToken]; +} + +- (instancetype)queryDataByReplacingSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + resumeToken:(NSData *)resumeToken { + return [[FSTQueryData alloc] initWithQuery:self.query + targetID:self.targetID + purpose:self.purpose + snapshotVersion:snapshotVersion + resumeToken:resumeToken]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTReferenceSet.h b/Firestore/Source/Local/FSTReferenceSet.h new file mode 100644 index 0000000..e4f50a7 --- /dev/null +++ b/Firestore/Source/Local/FSTReferenceSet.h @@ -0,0 +1,71 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentKeySet.h" +#import "FSTGarbageCollector.h" +#import "FSTTypes.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A collection of references to a document from some kind of numbered entity (either a targetID or + * batchID). As references are added to or removed from the set corresponding events are emitted to + * a registered garbage collector. + * + * Each reference is represented by a FSTDocumentReference object. Each of them contains enough + * information to uniquely identify the reference. They are all stored primarily in a set sorted + * by key. A document is considered garbage if there's no references in that set (this can be + * efficiently checked thanks to sorting by key). + * + * FSTReferenceSet also keeps a secondary set that contains references sorted by IDs. This one is + * used to efficiently implement removal of all references by some target ID. + */ +@interface FSTReferenceSet : NSObject <FSTGarbageSource> + +/** Keeps track of keys that have references. */ +@property(nonatomic, weak, readwrite, nullable) id<FSTGarbageCollector> garbageCollector; + +/** Returns YES if the reference set contains no references. */ +- (BOOL)isEmpty; + +/** Adds a reference to the given document key for the given ID. */ +- (void)addReferenceToKey:(FSTDocumentKey *)key forID:(int)ID; + +/** Add references to the given document keys for the given ID. */ +- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID; + +/** Removes a reference to the given document key for the given ID. */ +- (void)removeReferenceToKey:(FSTDocumentKey *)key forID:(int)ID; + +/** Removes references to the given document keys for the given ID. */ +- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID; + +/** Clears all references with a given ID. Calls -removeReferenceToKey: for each key removed. */ +- (void)removeReferencesForID:(int)ID; + +/** Clears all references for all IDs. */ +- (void)removeAllReferences; + +/** Returns all of the document keys that have had references added for the given ID. */ +- (FSTDocumentKeySet *)referencedKeysForID:(int)ID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTReferenceSet.m b/Firestore/Source/Local/FSTReferenceSet.m new file mode 100644 index 0000000..2326ded --- /dev/null +++ b/Firestore/Source/Local/FSTReferenceSet.m @@ -0,0 +1,135 @@ +/* + * 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 "FSTReferenceSet.h" + +#import "FSTDocumentKey.h" +#import "FSTDocumentReference.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTReferenceSet + +@interface FSTReferenceSet () + +/** A set of outstanding references to a document sorted by key. */ +@property(nonatomic, strong) FSTImmutableSortedSet<FSTDocumentReference *> *referencesByKey; + +/** A set of outstanding references to a document sorted by target ID (or batch ID). */ +@property(nonatomic, strong) FSTImmutableSortedSet<FSTDocumentReference *> *referencesByID; + +@end + +@implementation FSTReferenceSet + +#pragma mark - Initializer + +- (instancetype)init { + self = [super init]; + if (self) { + _referencesByKey = + [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByKey]; + _referencesByID = [FSTImmutableSortedSet setWithComparator:FSTDocumentReferenceComparatorByID]; + } + return self; +} + +#pragma mark - Testing helper methods + +- (BOOL)isEmpty { + return [self.referencesByKey isEmpty]; +} + +- (NSUInteger)count { + return self.referencesByKey.count; +} + +#pragma mark - Public methods + +- (void)addReferenceToKey:(FSTDocumentKey *)key forID:(int)ID { + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:ID]; + self.referencesByKey = [self.referencesByKey setByAddingObject:reference]; + self.referencesByID = [self.referencesByID setByAddingObject:reference]; +} + +- (void)addReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + [self addReferenceToKey:key forID:ID]; + }]; +} + +- (void)removeReferenceToKey:(FSTDocumentKey *)key forID:(int)ID { + [self removeReference:[[FSTDocumentReference alloc] initWithKey:key ID:ID]]; +} + +- (void)removeReferencesToKeys:(FSTDocumentKeySet *)keys forID:(int)ID { + [keys enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + [self removeReferenceToKey:key forID:ID]; + }]; +} + +- (void)removeReferencesForID:(int)ID { + FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID]; + FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)]; + + [self.referencesByID enumerateObjectsFrom:start + to:end + usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { + [self removeReference:reference]; + }]; +} + +- (void)removeAllReferences { + for (FSTDocumentReference *reference in self.referencesByKey.objectEnumerator) { + [self removeReference:reference]; + } +} + +- (void)removeReference:(FSTDocumentReference *)reference { + self.referencesByKey = [self.referencesByKey setByRemovingObject:reference]; + self.referencesByID = [self.referencesByID setByRemovingObject:reference]; + [self.garbageCollector addPotentialGarbageKey:reference.key]; +} + +- (FSTDocumentKeySet *)referencedKeysForID:(int)ID { + FSTDocumentKey *emptyKey = [FSTDocumentKey keyWithSegments:@[]]; + FSTDocumentReference *start = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:ID]; + FSTDocumentReference *end = [[FSTDocumentReference alloc] initWithKey:emptyKey ID:(ID + 1)]; + + __block FSTDocumentKeySet *keys = [FSTDocumentKeySet keySet]; + [self.referencesByID enumerateObjectsFrom:start + to:end + usingBlock:^(FSTDocumentReference *reference, BOOL *stop) { + keys = [keys setByAddingObject:reference.key]; + }]; + return keys; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + // Create a reference with a zero ID as the start position to find any document reference with + // this key. + FSTDocumentReference *reference = [[FSTDocumentReference alloc] initWithKey:key ID:0]; + + NSEnumerator<FSTDocumentReference *> *enumerator = + [self.referencesByKey objectEnumeratorFrom:reference]; + FSTDocumentKey *_Nullable firstKey = [enumerator nextObject].key; + return [firstKey isEqual:reference.key]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTRemoteDocumentCache.h b/Firestore/Source/Local/FSTRemoteDocumentCache.h new file mode 100644 index 0000000..8979455 --- /dev/null +++ b/Firestore/Source/Local/FSTRemoteDocumentCache.h @@ -0,0 +1,76 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentDictionary.h" + +@class FSTDocumentKey; +@class FSTMaybeDocument; +@class FSTQuery; +@class FSTWriteGroup; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Represents cached documents received from the remote backend. + * + * The cache is keyed by FSTDocumentKey and entries in the cache are FSTMaybeDocument instances, + * meaning we can cache both FSTDocument instances (an actual document with data) as well as + * FSTDeletedDocument instances (indicating that the document is known to not exist). + */ +@protocol FSTRemoteDocumentCache <NSObject> + +/** Shuts this cache down, closing open files, etc. */ +- (void)shutdown; + +/** + * Adds or replaces an entry in the cache. + * + * The cache key is extracted from `maybeDocument.key`. If there is already a cache entry for + * the key, it will be replaced. + * + * @param maybeDocument A FSTDocument or FSTDeletedDocument to put in the cache. + */ +- (void)addEntry:(FSTMaybeDocument *)maybeDocument group:(FSTWriteGroup *)group; + +/** Removes the cached entry for the given key (no-op if no entry exists). */ +- (void)removeEntryForKey:(FSTDocumentKey *)documentKey group:(FSTWriteGroup *)group; + +/** + * Looks up an entry in the cache. + * + * @param documentKey The key of the entry to look up. + * @return The cached FSTDocument or FSTDeletedDocument entry, or nil if we have nothing cached. + */ +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey; + +/** + * Executes a query against the cached FSTDocument entries + * + * Implementations may return extra documents if convenient. The results should be re-filtered + * by the consumer before presenting them to the user. + * + * Cached FSTDeletedDocument entries have no bearing on query results. + * + * @param query The query to match documents against. + * @return The set of matching documents. + */ +- (FSTDocumentDictionary *)documentsMatchingQuery:(FSTQuery *)query; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h new file mode 100644 index 0000000..be0d609 --- /dev/null +++ b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h @@ -0,0 +1,66 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@protocol FSTRemoteDocumentCache; +@class FSTMaybeDocument; +@class FSTDocumentKey; +@class FSTWriteGroup; + +/** + * An in-memory buffer of entries to be written to an FSTRemoteDocumentCache. It can be used to + * batch up a set of changes to be written to the cache, but additionally supports reading entries + * back with the `entryForKey:` method, falling back to the underlying FSTRemoteDocumentCache if + * no entry is buffered. In the absence of LevelDB transactions (that would allow reading back + * uncommitted writes), this greatly simplifies the implementation of complex operations that + * may want to freely read/write entries to the FSTRemoteDocumentCache while still ensuring that + * the final writing of the buffered entries is atomic. + * + * For doing blind writes that don't depend on the current state of the FSTRemoteDocumentCache + * or for plain reads, you can/should still just use the FSTRemoteDocumentCache directly. + */ +@interface FSTRemoteDocumentChangeBuffer : NSObject + ++ (instancetype)changeBufferWithCache:(id<FSTRemoteDocumentCache>)cache; + +- (instancetype)init __attribute__((unavailable("Use a static constructor instead"))); + +/** Buffers an `FSTRemoteDocumentCache addEntry:group:` call. */ +- (void)addEntry:(FSTMaybeDocument *)maybeDocument; + +// NOTE: removeEntryForKey: is not presently necessary and so is omitted. + +/** + * Looks up an entry in the cache. The buffered changes will first be checked, and if no + * buffered change applies, this will forward to `FSTRemoteDocumentCache entryForKey:`. + * + * @param documentKey The key of the entry to look up. + * @return The cached FSTDocument or FSTDeletedDocument entry, or nil if we have nothing cached. + */ +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey; + +/** + * Applies buffered changes to the underlying FSTRemoteDocumentCache, using the provided + * FSTWriteGroup. + */ +- (void)applyToWriteGroup:(FSTWriteGroup *)group; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m new file mode 100644 index 0000000..12a68ff --- /dev/null +++ b/Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m @@ -0,0 +1,88 @@ +/* + * 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 "FSTRemoteDocumentChangeBuffer.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTRemoteDocumentCache.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTRemoteDocumentChangeBuffer () + +- (instancetype)initWithCache:(id<FSTRemoteDocumentCache>)cache; + +/** The underlying cache we're buffering changes for. */ +@property(nonatomic, strong, nonnull) id<FSTRemoteDocumentCache> remoteDocumentCache; + +/** The buffered changes, stored as a dictionary for easy lookups. */ +@property(nonatomic, strong, nullable) + NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *changes; + +@end + +@implementation FSTRemoteDocumentChangeBuffer + ++ (instancetype)changeBufferWithCache:(id<FSTRemoteDocumentCache>)cache { + return [[FSTRemoteDocumentChangeBuffer alloc] initWithCache:cache]; +} + +- (instancetype)initWithCache:(id<FSTRemoteDocumentCache>)cache { + if (self = [super init]) { + _remoteDocumentCache = cache; + _changes = [NSMutableDictionary dictionary]; + } + return self; +} + +- (void)addEntry:(FSTMaybeDocument *)maybeDocument { + [self assertValid]; + + self.changes[maybeDocument.key] = maybeDocument; +} + +- (nullable FSTMaybeDocument *)entryForKey:(FSTDocumentKey *)documentKey { + [self assertValid]; + + FSTMaybeDocument *bufferedEntry = self.changes[documentKey]; + if (bufferedEntry) { + return bufferedEntry; + } else { + return [self.remoteDocumentCache entryForKey:documentKey]; + } +} + +- (void)applyToWriteGroup:(FSTWriteGroup *)group { + [self assertValid]; + + [self.changes enumerateKeysAndObjectsUsingBlock:^(FSTDocumentKey *key, FSTMaybeDocument *value, + BOOL *stop) { + [self.remoteDocumentCache addEntry:value group:group]; + }]; + + // We should not be used to buffer any more changes. + self.changes = nil; +} + +- (void)assertValid { + FSTAssert(self.changes, @"Changes have already been applied."); +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroup.h b/Firestore/Source/Local/FSTWriteGroup.h new file mode 100644 index 0000000..21482af --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroup.h @@ -0,0 +1,97 @@ +/* + * 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 <Foundation/Foundation.h> + +#ifdef __cplusplus +#include <memory> + +#include "StringView.h" + +namespace leveldb { +class DB; +class Status; +} + +#endif + +NS_ASSUME_NONNULL_BEGIN + +@class GPBMessage; + +/** + * A group of writes that will be applied together atomically to persistent storage. + * + * This class is usable by both Objective-C and Objective-C++ clients. Objective-C clients are able + * to create a new group and commit it. Objective-C++ clients can additionally add to the group + * using deleteKey: and putKey:value:. + * + * Note that this is a write "group" even though the underlying LevelDB concept is a write "batch" + * because Firestore already has a concept of mutation batches, which are user-specified groups of + * changes. This means that an FSTWriteGroup may contain the application of multiple user-specified + * mutation batches. + */ +@interface FSTWriteGroup : NSObject + +/** + * Creates a new, empty write group. + * + * @param action A description of the action performed by this group, used for logging. + */ ++ (instancetype)groupWithAction:(NSString *)action; + +- (instancetype)init __attribute__((unavailable("Use a static constructor instead"))); + +/** The action description assigned to this write group. */ +@property(nonatomic, copy, readonly) NSString *action; + +/** Returns YES if the write group has no messages in it. */ +- (BOOL)isEmpty; + +#ifdef __cplusplus + +/** + * Marks the given key for deletion. + * + * @param key The LevelDB key of the row to delete + */ +- (void)removeMessageForKey:(Firestore::StringView)key; + +/** + * Sets the row identified by the given key to the value of the given protocol buffer message. + * + * @param key The LevelDB Key of the row to set. + * @param message The protocol buffer message whose serialized contents should be used for the + * value associated with the key. + */ +- (void)setMessage:(GPBMessage *)message forKey:(Firestore::StringView)key; + +/** + * Sets the row identified by the given key to the value of the given data bytes. + * + * @param key The LevelDB Key of the row to set. + * @param data The exact value to be associated with the key. + */ +- (void)setData:(Firestore::StringView)data forKey:(Firestore::StringView)key; + +/** Writes the contents to the given LevelDB. */ +- (leveldb::Status)writeToDB:(std::shared_ptr<leveldb::DB>)db; + +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroup.mm b/Firestore/Source/Local/FSTWriteGroup.mm new file mode 100644 index 0000000..e6da131 --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroup.mm @@ -0,0 +1,145 @@ +/* + * 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 "FSTWriteGroup.h" + +#import <Protobuf/GPBProtocolBuffers.h> +#include <leveldb/db.h> +#include <leveldb/write_batch.h> + +#import "FSTLevelDBKey.h" +#import "FSTAssert.h" + +#include "ordered_code.h" + +using Firestore::OrderedCode; +using Firestore::StringView; +using leveldb::DB; +using leveldb::Slice; +using leveldb::Status; +using leveldb::WriteBatch; +using leveldb::WriteOptions; + +NS_ASSUME_NONNULL_BEGIN + +namespace Firestore { + +/** + * A WriteBatch::Handler implementation that extracts batch details from a leveldb::WriteBatch. + * This is used for describing a write batch primarily in log messages after a failure. + */ +class BatchDescription : public WriteBatch::Handler { + public: + BatchDescription() : ops_(0), size_(0), message_([NSMutableString string]) {} + virtual ~BatchDescription(); + virtual void Put(const Slice &key, const Slice &value); + virtual void Delete(const Slice &key); + + // Converts the batch to a printable string description of it + NSString *ToString() const { + return [NSString + stringWithFormat:@"%d changes (%lu bytes):%@", ops_, (unsigned long)size_, message_]; + } + + // Disallow copies and moves + BatchDescription(const BatchDescription &) = delete; + BatchDescription &operator=(const BatchDescription &) = delete; + BatchDescription(BatchDescription &&) = delete; + BatchDescription &operator=(BatchDescription &&) = delete; + + private: + int ops_; + size_t size_; + NSMutableString *message_; +}; + +BatchDescription::~BatchDescription() {} + +void BatchDescription::Put(const Slice &key, const Slice &value) { + ops_ += 1; + size_ += value.size(); + + [message_ appendFormat:@"\n - Put %@ (%lu bytes)", [FSTLevelDBKey descriptionForKey:key], + (unsigned long)value.size()]; +} + +void BatchDescription::Delete(const Slice &key) { + ops_ += 1; + + [message_ appendFormat:@"\n - Delete %@", [FSTLevelDBKey descriptionForKey:key]]; +} + +} // namespace Firestore + +@interface FSTWriteGroup () +- (instancetype)initWithAction:(NSString *)action NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTWriteGroup { + int _changes; + WriteBatch _contents; +} + ++ (instancetype)groupWithAction:(NSString *)action { + return [[FSTWriteGroup alloc] initWithAction:action]; +} + +- (instancetype)initWithAction:(NSString *)action { + if (self = [super init]) { + _action = action; + } + return self; +} + +- (NSString *)description { + Firestore::BatchDescription description; + Status status = _contents.Iterate(&description); + if (!status.ok()) { + FSTFail(@"Iterate over write batch should not fail"); + } + return [NSString + stringWithFormat:@"<FSTWriteGroup for %@: %@>", self.action, description.ToString()]; +} + +- (void)removeMessageForKey:(StringView)key { + _contents.Delete(key); + _changes += 1; +} + +- (void)setMessage:(GPBMessage *)message forKey:(StringView)key { + NSData *data = [message data]; + Slice value((const char *)data.bytes, data.length); + + _contents.Put(key, value); + _changes += 1; +} + +- (void)setData:(StringView)data forKey:(StringView)key { + _contents.Put(key, data); + _changes += 1; +} + +- (leveldb::Status)writeToDB:(std::shared_ptr<leveldb::DB>)db { + return db->Write(leveldb::WriteOptions(), &_contents); +} + +- (BOOL)isEmpty { + return _changes == 0; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroupTracker.h b/Firestore/Source/Local/FSTWriteGroupTracker.h new file mode 100644 index 0000000..bd26a46 --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroupTracker.h @@ -0,0 +1,45 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTWriteGroup; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Helper class for FSTPersistence implementations to create WriteGroups and verify internal + * contracts are maintained: + * 1. Can't create a group when an uncommitted group exists (no nesting). + * 2. Can't commit a group that differs from the last created one. + */ +@interface FSTWriteGroupTracker : NSObject + +/** Creates and returns an FSTWriteGroupTracker instance. */ ++ (instancetype)tracker; + +/** + * Verifies there's no active group already and then creates a new group and stores it for later + * validation with `endGroup`. + */ +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action; + +/** Ends a group previously started with `startGroupWithAction`. */ +- (void)endGroup:(FSTWriteGroup *)group; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/FSTWriteGroupTracker.m b/Firestore/Source/Local/FSTWriteGroupTracker.m new file mode 100644 index 0000000..1c6c84d --- /dev/null +++ b/Firestore/Source/Local/FSTWriteGroupTracker.m @@ -0,0 +1,52 @@ +/* + * 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 "FSTWriteGroupTracker.h" + +#import "FSTAssert.h" +#import "FSTWriteGroup.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTWriteGroupTracker () +@property(nonatomic, strong, nullable) FSTWriteGroup *activeGroup; +@end + +@implementation FSTWriteGroupTracker + ++ (instancetype)tracker { + return [[FSTWriteGroupTracker alloc] init]; +} + +- (FSTWriteGroup *)startGroupWithAction:(NSString *)action { + // NOTE: We can relax this to allow nesting if/when we find we need it. + FSTAssert(!self.activeGroup, + @"Attempt to create write group (%@) while existing write group (%@) still active.", + action, self.activeGroup.action); + self.activeGroup = [FSTWriteGroup groupWithAction:action]; + return self.activeGroup; +} + +- (void)endGroup:(FSTWriteGroup *)group { + FSTAssert(self.activeGroup == group, + @"Attempted to end write group (%@) which is different from active group (%@)", + group.action, self.activeGroup.action); + self.activeGroup = nil; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Local/StringView.h b/Firestore/Source/Local/StringView.h new file mode 100644 index 0000000..799baf8 --- /dev/null +++ b/Firestore/Source/Local/StringView.h @@ -0,0 +1,85 @@ +/* + * 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. + */ + +#ifndef IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_ +#define IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_ + +#ifndef __cplusplus +#error "StringView is Objective-C++ and can only be included from .mm files" +#endif + +#import <Foundation/Foundation.h> + +#include <leveldb/slice.h> +#include <string> + +namespace Firestore { + +// A simple wrapper for the character data of any string-like type to which +// we'd like to temporarily refer as an argument. +// +// This is superficially similar to StringPiece and leveldb::Slice except +// that it also supports implicit conversion from NSString *, which is useful +// when writing Objective-C++ methods that accept any string-like type. +// +// Note that much like any other view-type class in C++, the caller is +// responsible for ensuring that the lifetime of the string-like data is longer +// than the lifetime of the StringView. +// +// Functions that take a StringView argument promise that they won't keep the +// pointer beyond the immediate scope of their own stack frame. +class StringView { + public: + // Creates a StringView from an NSString. When StringView is an argument type + // into which an NSString* is passed, the caller should ensure that the + // NSString is retained. + StringView(NSString *str) : data_([str UTF8String]), size_(str.length) { + } + + // Creates a StringView from the given char* pointer with an explicit size. + // The character data can contain NUL bytes as a result. + StringView(const char *data, size_t size) : data_(data), size_(size) { + } + + // Creates a StringView from the given char* pointer but computes the size + // with strlen. This is really only suitable for passing C string literals. + StringView(const char *data) : data_(data), size_(strlen(data)) { + } + + // Creates a StringView from the given slice. + StringView(leveldb::Slice slice) : data_(slice.data()), size_(slice.size()) { + } + + // Creates a StringView from the given std::string. The string must be an + // lvalue for the lifetime requirements to be satisfied. + StringView(const std::string &str) : data_(str.data()), size_(str.size()) { + } + + // Converts this StringView to a Slice, which is an equivalent (and more + // functional) type. The returned slice has the same lifetime as this + // StringView. + operator leveldb::Slice() { + return leveldb::Slice(data_, size_); + } + + private: + const char *data_; + const size_t size_; +}; + +} // namespace Firestore + +#endif // IPHONE_FIRESTORE_SOURCE_LOCAL_STRING_VIEW_H_ diff --git a/Firestore/Source/Model/FSTDatabaseID.h b/Firestore/Source/Model/FSTDatabaseID.h new file mode 100644 index 0000000..442e764 --- /dev/null +++ b/Firestore/Source/Model/FSTDatabaseID.h @@ -0,0 +1,48 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** FSTDatabaseID represents a particular database in the datastore. */ +@interface FSTDatabaseID : NSObject + +/** + * Creates and returns a new FSTDatabaseID. + * @param projectID The project for the database. + * @param databaseID The database in the project to use. + * @return A new instance of FSTDatabaseID. + */ ++ (instancetype)databaseIDWithProject:(NSString *)projectID database:(NSString *)databaseID; + +/** The project. */ +@property(nonatomic, copy, readonly) NSString *projectID; + +/** The database. */ +@property(nonatomic, copy, readonly) NSString *databaseID; + +/** Whether this is the default database of the project. */ +- (BOOL)isDefaultDatabase; + +- (NSComparisonResult)compare:(FSTDatabaseID *)other; +- (BOOL)isEqualToDatabaseId:(FSTDatabaseID *)databaseID; + +@end + +extern NSString *const kDefaultDatabaseID; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDatabaseID.m b/Firestore/Source/Model/FSTDatabaseID.m new file mode 100644 index 0000000..bf4b417 --- /dev/null +++ b/Firestore/Source/Model/FSTDatabaseID.m @@ -0,0 +1,90 @@ +/* + * 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 "FSTDatabaseID.h" + +#import "FSTAssert.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The default name for "unset" database ID in resource names. */ +NSString *const kDefaultDatabaseID = @"(default)"; + +#pragma mark - FSTDatabaseID + +@implementation FSTDatabaseID + ++ (instancetype)databaseIDWithProject:(NSString *)projectID database:(NSString *)databaseID { + return [[FSTDatabaseID alloc] initWithProject:projectID database:databaseID]; +} + +/** + * Designated initializer. + * + * @param projectID The project for the database. + * @param databaseID The database in the datastore. + */ +- (instancetype)initWithProject:(NSString *)projectID database:(NSString *)databaseID { + if (self = [super init]) { + FSTAssert(databaseID, @"databaseID cannot be nil"); + _projectID = [projectID copy]; + _databaseID = [databaseID copy]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + + return [self isEqualToDatabaseId:other]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.projectID hash]; + hash = hash * 31u + [self.databaseID hash]; + return hash; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"<FSTDatabaseID: project:%@ database:%@>", self.projectID, self.databaseID]; +} + +- (NSComparisonResult)compare:(FSTDatabaseID *)other { + NSComparisonResult cmp = [self.projectID compare:other.projectID]; + return cmp == NSOrderedSame ? [self.databaseID compare:other.databaseID] : cmp; +} + +- (BOOL)isDefaultDatabase { + return [self.databaseID isEqualToString:kDefaultDatabaseID]; +} + +- (BOOL)isEqualToDatabaseId:(FSTDatabaseID *)databaseID { + if (self == databaseID) return YES; + if (databaseID == nil) return NO; + if (self.projectID != databaseID.projectID && + ![self.projectID isEqualToString:databaseID.projectID]) + return NO; + if (self.databaseID != databaseID.databaseID && + ![self.databaseID isEqualToString:databaseID.databaseID]) + return NO; + return YES; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocument.h b/Firestore/Source/Model/FSTDocument.h new file mode 100644 index 0000000..100f553 --- /dev/null +++ b/Firestore/Source/Model/FSTDocument.h @@ -0,0 +1,58 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTDocumentKey; +@class FSTFieldPath; +@class FSTFieldValue; +@class FSTObjectValue; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** + * The result of a lookup for a given path may be an existing document or a tombstone that marks + * the path deleted. + */ +@interface FSTMaybeDocument : NSObject <NSCopying> +- (id)init __attribute__((unavailable("Abstract base class"))); + +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@property(nonatomic, readonly) FSTSnapshotVersion *version; +@end + +@interface FSTDocument : FSTMaybeDocument ++ (instancetype)documentWithData:(FSTObjectValue *)data + key:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version + hasLocalMutations:(BOOL)mutations; + +- (nullable FSTFieldValue *)fieldForPath:(FSTFieldPath *)path; + +@property(nonatomic, strong, readonly) FSTObjectValue *data; +@property(nonatomic, readonly, getter=hasLocalMutations) BOOL localMutations; + +@end + +@interface FSTDeletedDocument : FSTMaybeDocument ++ (instancetype)documentWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version; +@end + +/** An NSComparator suitable for comparing docs using only their keys. */ +extern const NSComparator FSTDocumentComparatorByKey; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocument.m b/Firestore/Source/Model/FSTDocument.m new file mode 100644 index 0000000..5146d46 --- /dev/null +++ b/Firestore/Source/Model/FSTDocument.m @@ -0,0 +1,139 @@ +/* + * 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 "FSTDocument.h" + +#import "FSTAssert.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTPath.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTMaybeDocument () + +- (instancetype)initWithKey:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTMaybeDocument + +- (instancetype)initWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version { + FSTAssert(!!version, @"Version must not be nil."); + self = [super init]; + if (self) { + _key = key; + _version = version; + } + return self; +} + +- (id)copyWithZone:(NSZone *_Nullable)zone { + // All document types are immutable + return self; +} + +@end + +@implementation FSTDocument + ++ (instancetype)documentWithData:(FSTObjectValue *)data + key:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version + hasLocalMutations:(BOOL)mutations { + return + [[FSTDocument alloc] initWithData:data key:key version:version hasLocalMutations:mutations]; +} + +- (instancetype)initWithData:(FSTObjectValue *)data + key:(FSTDocumentKey *)key + version:(FSTSnapshotVersion *)version + hasLocalMutations:(BOOL)mutations { + self = [super initWithKey:key version:version]; + if (self) { + _data = data; + _localMutations = mutations; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDocument class]]) { + return NO; + } + + FSTDocument *otherDoc = other; + return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version] && + [self.data isEqual:otherDoc.data] && self.hasLocalMutations == otherDoc.hasLocalMutations; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31 + [self.version hash]; + result = result * 31 + [self.data hash]; + result = result * 31 + (self.hasLocalMutations ? 1 : 0); + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTDocument: key:%@ version:%@ localMutations:%@ data:%@>", + self.key.path, self.version, + self.localMutations ? @"YES" : @"NO", self.data]; +} + +- (nullable FSTFieldValue *)fieldForPath:(FSTFieldPath *)path { + return [_data valueForPath:path]; +} + +@end + +@implementation FSTDeletedDocument + ++ (instancetype)documentWithKey:(FSTDocumentKey *)key version:(FSTSnapshotVersion *)version { + return [[FSTDeletedDocument alloc] initWithKey:key version:version]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDeletedDocument class]]) { + return NO; + } + + FSTDocument *otherDoc = other; + return [self.key isEqual:otherDoc.key] && [self.version isEqual:otherDoc.version]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = result * 31 + [self.version hash]; + return result; +} + +@end + +const NSComparator FSTDocumentComparatorByKey = + ^NSComparisonResult(FSTMaybeDocument *doc1, FSTMaybeDocument *doc2) { + return [doc1.key compare:doc2.key]; + }; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentDictionary.h b/Firestore/Source/Model/FSTDocumentDictionary.h new file mode 100644 index 0000000..8ae8e01 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentDictionary.h @@ -0,0 +1,44 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTImmutableSortedDictionary.h" + +@class FSTDocument; +@class FSTDocumentKey; +@class FSTMaybeDocument; + +NS_ASSUME_NONNULL_BEGIN + +/** Convenience type for a map of keys to MaybeDocuments, since they are so common. */ +typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTMaybeDocument *> + FSTMaybeDocumentDictionary; + +/** Convenience type for a map of keys to Documents, since they are so common. */ +typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTDocument *> FSTDocumentDictionary; + +@interface FSTImmutableSortedDictionary (FSTDocumentDictionary) + +/** Returns a new set using the DocumentKeyComparator. */ ++ (FSTMaybeDocumentDictionary *)maybeDocumentDictionary; + +/** Returns a new set using the DocumentKeyComparator. */ ++ (FSTDocumentDictionary *)documentDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentDictionary.m b/Firestore/Source/Model/FSTDocumentDictionary.m new file mode 100644 index 0000000..67e3ae7 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentDictionary.m @@ -0,0 +1,42 @@ +/* + * 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 "FSTDocumentDictionary.h" + +#import "FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedDictionary (FSTMaybeDocumentDictionary) + ++ (instancetype)maybeDocumentDictionary { + // Immutable dictionaries are contravariant in their value type, so just return a + // FSTDocumentDictionary here. + return [FSTDocumentDictionary documentDictionary]; +} + ++ (instancetype)documentDictionary { + static FSTDocumentDictionary *singleton; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + singleton = [FSTDocumentDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + }); + return singleton; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKey.h b/Firestore/Source/Model/FSTDocumentKey.h new file mode 100644 index 0000000..2af1c9a --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKey.h @@ -0,0 +1,66 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTResourcePath; + +NS_ASSUME_NONNULL_BEGIN + +/** FSTDocumentKey represents the location of a document in the Firestore database. */ +@interface FSTDocumentKey : NSObject <NSCopying> + +/** + * Creates and returns a new document key with the given path. + * + * @param path The path to the document. + * @return A new instance of FSTDocumentKey. + */ ++ (instancetype)keyWithPath:(FSTResourcePath *)path; + +/** + * Creates and returns a new document key with a path with the given segments. + * + * @param segments The segments of the path to the document. + * @return A new instance of FSTDocumentKey. + */ ++ (instancetype)keyWithSegments:(NSArray<NSString *> *)segments; + +/** + * Creates and returns a new document key from the given resource path string. + * + * @param resourcePath The slash-separated segments of the resource's path. + * @return A new instance of FSTDocumentKey. + */ ++ (instancetype)keyWithPathString:(NSString *)resourcePath; + +/** Returns true iff the given path is a path to a document. */ ++ (BOOL)isDocumentKey:(FSTResourcePath *)path; + +- (BOOL)isEqualToKey:(FSTDocumentKey *)other; +- (NSComparisonResult)compare:(FSTDocumentKey *)other; + +/** The path to the document. */ +@property(strong, nonatomic, readonly) FSTResourcePath *path; + +@end + +extern const NSComparator FSTDocumentKeyComparator; + +/** The field path string that represents the document's key. */ +extern NSString *const kDocumentKeyPath; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKey.m b/Firestore/Source/Model/FSTDocumentKey.m new file mode 100644 index 0000000..a412b13 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKey.m @@ -0,0 +1,105 @@ +/* + * 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 "FSTDocumentKey.h" + +#import "FSTAssert.h" +#import "FSTFirestoreClient.h" +#import "FSTPath.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDocumentKey () +/** The path to the document. */ +@property(strong, nonatomic, readwrite) FSTResourcePath *path; +@end + +@implementation FSTDocumentKey + ++ (instancetype)keyWithPath:(FSTResourcePath *)path { + return [[FSTDocumentKey alloc] initWithPath:path]; +} + ++ (instancetype)keyWithSegments:(NSArray<NSString *> *)segments { + return [FSTDocumentKey keyWithPath:[FSTResourcePath pathWithSegments:segments]]; +} + ++ (instancetype)keyWithPathString:(NSString *)resourcePath { + NSArray<NSString *> *segments = [resourcePath componentsSeparatedByString:@"/"]; + return [FSTDocumentKey keyWithSegments:segments]; +} + +/** Designated initializer. */ +- (instancetype)initWithPath:(FSTResourcePath *)path { + FSTAssert([FSTDocumentKey isDocumentKey:path], @"invalid document key path: %@", path); + + if (self = [super init]) { + _path = path; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTDocumentKey class]]) { + return NO; + } + return [self isEqualToKey:(FSTDocumentKey *)object]; +} + +- (NSUInteger)hash { + return self.path.hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTDocumentKey: %@>", self.path]; +} + +/** Implements NSCopying without actually copying because FSTDocumentKeys are immutable. */ +- (id)copyWithZone:(NSZone *_Nullable)zone { + return self; +} + +- (BOOL)isEqualToKey:(FSTDocumentKey *)other { + return FSTDocumentKeyComparator(self, other) == NSOrderedSame; +} + +- (NSComparisonResult)compare:(FSTDocumentKey *)other { + return FSTDocumentKeyComparator(self, other); +} + ++ (NSComparator)comparator { + return ^NSComparisonResult(id obj1, id obj2) { + return [obj1 compare:obj2]; + }; +} + ++ (BOOL)isDocumentKey:(FSTResourcePath *)path { + return path.length % 2 == 0; +} + +@end + +const NSComparator FSTDocumentKeyComparator = + ^NSComparisonResult(FSTDocumentKey *key1, FSTDocumentKey *key2) { + return [key1.path compare:key2.path]; + }; + +NSString *const kDocumentKeyPath = @"__name__"; + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKeySet.h b/Firestore/Source/Model/FSTDocumentKeySet.h new file mode 100644 index 0000000..7352985 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKeySet.h @@ -0,0 +1,35 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTImmutableSortedSet.h" + +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** Convenience type for a set of keys, since they are so common. */ +typedef FSTImmutableSortedSet<FSTDocumentKey *> FSTDocumentKeySet; + +@interface FSTImmutableSortedSet (FSTDocumentKey) + +/** Returns a new set using the DocumentKeyComparator. */ ++ (FSTDocumentKeySet *)keySet; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentKeySet.m b/Firestore/Source/Model/FSTDocumentKeySet.m new file mode 100644 index 0000000..54f1b2c --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentKeySet.m @@ -0,0 +1,31 @@ +/* + * 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 "FSTDocumentKeySet.h" + +#import "FSTDocumentKey.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedSet (FSTDocumentKey) + ++ (instancetype)keySet { + return [FSTDocumentKeySet setWithComparator:FSTDocumentKeyComparator]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentSet.h b/Firestore/Source/Model/FSTDocumentSet.h new file mode 100644 index 0000000..7457ea3 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentSet.h @@ -0,0 +1,95 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentDictionary.h" + +@class FSTDocument; +@class FSTDocumentKey; + +NS_ASSUME_NONNULL_BEGIN + +/** + * DocumentSet is an immutable (copy-on-write) collection that holds documents in order specified + * by the provided comparator. We always add a document key comparator on top of what is provided + * to guarantee document equality based on the key. + */ +@interface FSTDocumentSet : NSObject + +/** Creates a new, empty FSTDocumentSet sorted by the given comparator, then by keys. */ ++ (instancetype)documentSetWithComparator:(NSComparator)comparator; + +- (instancetype)init __attribute__((unavailable("Use a static constructor instead"))); + +- (NSUInteger)count; + +/** Returns true if the dictionary contains no elements. */ +- (BOOL)isEmpty; + +/** Returns YES if this set contains a document with the given key. */ +- (BOOL)containsKey:(FSTDocumentKey *)key; + +/** Returns the document from this set with the given key if it exists or nil if it doesn't. */ +- (FSTDocument *_Nullable)documentForKey:(FSTDocumentKey *)key; + +/** + * Returns the first document in the set according to its built in ordering, or nil if the set + * is empty. + */ +- (FSTDocument *_Nullable)firstDocument; + +/** + * Returns the last document in the set according to its built in ordering, or nil if the set + * is empty. + */ +- (FSTDocument *_Nullable)lastDocument; + +/** + * Returns the document previous to the document associated with the given key in the set according + * to its built in ordering. Returns nil if the document associated with the given key is the + * first document. + * + * @param key A key that must be present in the DocumentSet. + * @throws NSInvalidArgumentException if key is not present. + */ +- (FSTDocument *_Nullable)predecessorDocumentForKey:(FSTDocumentKey *)key; + +/** + * Returns the index of the document with the provided key in the document set. Returns NSNotFound + * if the key is not present. + */ +- (NSUInteger)indexOfKey:(FSTDocumentKey *)key; + +- (NSEnumerator<FSTDocument *> *)documentEnumerator; + +/** Returns a copy of the documents in this set as an array. This is O(n) on the size of the set. */ +- (NSArray<FSTDocument *> *)arrayValue; + +/** + * Returns the documents as a FSTMaybeDocumentDictionary. This is O(1) as this leverages our + * internal representation. + */ +- (FSTMaybeDocumentDictionary *)dictionaryValue; + +/** Returns a new FSTDocumentSet that contains the given document. */ +- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document; + +/** Returns a new FSTDocumentSet that excludes any document associated with the given key. */ +- (instancetype)documentSetByRemovingKey:(FSTDocumentKey *)key; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentSet.m b/Firestore/Source/Model/FSTDocumentSet.m new file mode 100644 index 0000000..94b7b58 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentSet.m @@ -0,0 +1,197 @@ +/* + * 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 "FSTDocumentSet.h" + +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTImmutableSortedSet.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The type of the index of the documents in an FSTDocumentSet. + * @see FSTDocumentSet#index + */ +typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTDocument *> IndexType; + +/** + * The type of the main collection of documents in an FSTDocumentSet. + * @see FSTDocumentSet#sortedSet + */ +typedef FSTImmutableSortedSet<FSTDocument *> SetType; + +@interface FSTDocumentSet () + +- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet NS_DESIGNATED_INITIALIZER; + +/** + * An index of the documents in the FSTDocumentSet, indexed by document key. The index + * exists to guarantee the uniqueness of document keys in the set and to allow lookup and removal + * of documents by key. + */ +@property(nonatomic, strong, readonly) IndexType *index; + +/** + * The main collection of documents in the FSTDocumentSet. The documents are ordered by a + * comparator supplied from a query. The SetType collection exists in addition to the index to + * allow ordered traversal of the FSTDocumentSet. + */ +@property(nonatomic, strong, readonly) SetType *sortedSet; +@end + +@implementation FSTDocumentSet + ++ (instancetype)documentSetWithComparator:(NSComparator)comparator { + IndexType *index = + [FSTImmutableSortedDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + SetType *set = [FSTImmutableSortedSet setWithComparator:comparator]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +- (instancetype)initWithIndex:(IndexType *)index set:(SetType *)sortedSet { + self = [super init]; + if (self) { + _index = index; + _sortedSet = sortedSet; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTDocumentSet class]]) { + return NO; + } + + FSTDocumentSet *otherSet = (FSTDocumentSet *)other; + if ([self count] != [otherSet count]) { + return NO; + } + + NSEnumerator<FSTDocument *> *selfIter = [self.sortedSet objectEnumerator]; + NSEnumerator<FSTDocument *> *otherIter = [otherSet.sortedSet objectEnumerator]; + + FSTDocument *selfDoc = [selfIter nextObject]; + FSTDocument *otherDoc = [otherIter nextObject]; + while (selfDoc) { + if (![selfDoc isEqual:otherDoc]) { + return NO; + } + selfDoc = [selfIter nextObject]; + otherDoc = [otherIter nextObject]; + } + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = 0; + for (FSTDocument *doc in self.sortedSet.objectEnumerator) { + hash = 31 * hash + [doc hash]; + } + return hash; +} + +- (NSString *)description { + return [self.sortedSet description]; +} + +- (NSUInteger)count { + return [self.index count]; +} + +- (BOOL)isEmpty { + return [self.index isEmpty]; +} + +- (BOOL)containsKey:(FSTDocumentKey *)key { + return [self.index objectForKey:key] != nil; +} + +- (FSTDocument *_Nullable)documentForKey:(FSTDocumentKey *)key { + return [self.index objectForKey:key]; +} + +- (FSTDocument *_Nullable)firstDocument { + return [self.sortedSet firstObject]; +} + +- (FSTDocument *_Nullable)lastDocument { + return [self.sortedSet lastObject]; +} + +- (FSTDocument *_Nullable)predecessorDocumentForKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + if (!doc) { + @throw [NSException exceptionWithName:NSInvalidArgumentException + reason:[NSString stringWithFormat:@"Key %@ does not exist", key] + userInfo:nil]; + } + return [self.sortedSet predecessorObject:doc]; +} + +- (NSUInteger)indexOfKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + return doc ? [self.sortedSet indexOfObject:doc] : NSNotFound; +} + +- (NSEnumerator<FSTDocument *> *)documentEnumerator { + return [self.sortedSet objectEnumerator]; +} + +- (NSArray *)arrayValue { + NSMutableArray<FSTDocument *> *result = [NSMutableArray arrayWithCapacity:self.count]; + for (FSTDocument *doc in self.documentEnumerator) { + [result addObject:doc]; + } + return result; +} + +- (FSTMaybeDocumentDictionary *)dictionaryValue { + return self.index; +} + +- (instancetype)documentSetByAddingDocument:(FSTDocument *_Nullable)document { + // TODO(mcg): look into making document nonnull. + if (!document) { + return self; + } + + // Remove any prior mapping of the document's key before adding, preventing sortedSet from + // accumulating values that aren't in the index. + FSTDocumentSet *removed = [self documentSetByRemovingKey:document.key]; + + IndexType *index = [removed.index dictionaryBySettingObject:document forKey:document.key]; + SetType *set = [removed.sortedSet setByAddingObject:document]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +- (instancetype)documentSetByRemovingKey:(FSTDocumentKey *)key { + FSTDocument *doc = [self.index objectForKey:key]; + if (!doc) { + return self; + } + + IndexType *index = [self.index dictionaryByRemovingObjectForKey:key]; + SetType *set = [self.sortedSet setByRemovingObject:doc]; + return [[FSTDocumentSet alloc] initWithIndex:index set:set]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.h b/Firestore/Source/Model/FSTDocumentVersionDictionary.h new file mode 100644 index 0000000..f94545f --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentVersionDictionary.h @@ -0,0 +1,40 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTImmutableSortedDictionary.h" + +@class FSTDocumentKey; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** A map of key to version number. */ +typedef FSTImmutableSortedDictionary<FSTDocumentKey *, FSTSnapshotVersion *> + FSTDocumentVersionDictionary; + +/** + * Extension to FSTImmutableSortedDictionary that allows natural construction of + * FSTDocumentVersionDictionary. + */ +@interface FSTImmutableSortedDictionary (FSTDocumentVersionDictionary) + ++ (instancetype)documentVersionDictionary; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTDocumentVersionDictionary.m b/Firestore/Source/Model/FSTDocumentVersionDictionary.m new file mode 100644 index 0000000..0eaf9f8 --- /dev/null +++ b/Firestore/Source/Model/FSTDocumentVersionDictionary.m @@ -0,0 +1,37 @@ +/* + * 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 "FSTDocumentVersionDictionary.h" + +#import "FSTDocumentKey.h" +#import "FSTSnapshotVersion.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTImmutableSortedDictionary (FSTDocumentVersionDictionary) + ++ (instancetype)documentVersionDictionary { + static FSTDocumentVersionDictionary *singleton; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + singleton = [FSTDocumentVersionDictionary dictionaryWithComparator:FSTDocumentKeyComparator]; + }); + return singleton; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTFieldValue.h b/Firestore/Source/Model/FSTFieldValue.h new file mode 100644 index 0000000..cbf7e3e --- /dev/null +++ b/Firestore/Source/Model/FSTFieldValue.h @@ -0,0 +1,242 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTImmutableSortedDictionary.h" + +@class FSTDatabaseID; +@class FSTDocumentKey; +@class FSTFieldPath; +@class FSTTimestamp; +@class FIRGeoPoint; + +NS_ASSUME_NONNULL_BEGIN + +/** The order of types in Firestore; this order is defined by the backend. */ +typedef NS_ENUM(NSInteger, FSTTypeOrder) { + FSTTypeOrderNull, + FSTTypeOrderBoolean, + FSTTypeOrderNumber, + FSTTypeOrderTimestamp, + FSTTypeOrderString, + FSTTypeOrderBlob, + FSTTypeOrderReference, + FSTTypeOrderGeoPoint, + FSTTypeOrderArray, + FSTTypeOrderObject, +}; + +/** + * 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. + * + * Supported types are: + * - Null + * - Boolean + * - Long + * - Double + * - Timestamp + * - ServerTimestamp (a sentinel used in uncommitted writes) + * - String + * - Binary + * - (Document) References + * - GeoPoint + * - Array + * - Object + */ +@interface FSTFieldValue : NSObject + +/** Returns the FSTTypeOrder for this value. */ +- (FSTTypeOrder)typeOrder; + +/** + * Converts an FSTFieldValue into the value that users will see in document snapshots. + * + * 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; + +/** Compares against another FSTFieldValue. */ +- (NSComparisonResult)compare:(FSTFieldValue *)other; + +@end + +/** + * A null value stored in Firestore. The |value| of a FSTNullValue is [NSNull null]. + */ +@interface FSTNullValue : FSTFieldValue ++ (instancetype)nullValue; +- (id)value; +@end + +/** + * A boolean value stored in Firestore. + */ +@interface FSTBooleanValue : FSTFieldValue ++ (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 +@end + +/** + * An integer value stored in Firestore. + */ +@interface FSTIntegerValue : FSTNumberValue ++ (instancetype)integerValue:(int64_t)value; +- (NSNumber *)value; +- (int64_t)internalValue; +@end + +/** + * A double-precision floating point number stored in Firestore. + */ +@interface FSTDoubleValue : FSTNumberValue ++ (instancetype)doubleValue:(double)value; ++ (instancetype)nanValue; +- (NSNumber *)value; +- (double)internalValue; +@end + +/** + * A string stored in Firestore. + */ +@interface FSTStringValue : FSTFieldValue ++ (instancetype)stringValue:(NSString *)value; +- (NSString *)value; +@end + +/** + * A timestamp value stored in Firestore. + */ +@interface FSTTimestampValue : FSTFieldValue ++ (instancetype)timestampValue:(FSTTimestamp *)value; +- (NSDate *)value; +- (FSTTimestamp *)internalValue; +@end + +/** + * Represents a locally-applied Server Timestamp. + * + * Notes: + * - 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). + * - They sort after all FSTTimestampValues. With respect to other FSTServerTimestampValues, they + * sort by their localWriteTime. + */ +@interface FSTServerTimestampValue : FSTFieldValue ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime; +- (NSNull *)value; +@property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime; +@end + +/** + * A geo point value stored in Firestore. + */ +@interface FSTGeoPointValue : FSTFieldValue ++ (instancetype)geoPointValue:(FIRGeoPoint *)value; +- (FIRGeoPoint *)value; +@end + +/** + * A blob value stored in Firestore. + */ +@interface FSTBlobValue : FSTFieldValue ++ (instancetype)blobValue:(NSData *)value; +- (NSData *)value; +@end + +/** + * A reference value stored in Firestore. + */ +@interface FSTReferenceValue : FSTFieldValue ++ (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID; +- (FSTDocumentKey *)value; +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; +@end + +/** + * A structured object value stored in Firestore. + */ +@interface FSTObjectValue : FSTFieldValue +/** Returns an empty FSTObjectValue. */ ++ (instancetype)objectValue; + +/** + * Initializes this FSTObjectValue with the given dictionary. + */ +- (instancetype)initWithDictionary:(NSDictionary<NSString *, FSTFieldValue *> *)value; + +/** + * Initializes this FSTObjectValue with the given immutable dictionary. + */ +- (instancetype)initWithImmutableDictionary: + (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)value NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (NSDictionary<NSString *, id> *)value; +- (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)internalValue; + +/** Returns the value at the given path if it exists. Returns nil otherwise. */ +- (nullable FSTFieldValue *)valueForPath:(FSTFieldPath *)fieldPath; + +/** + * Returns a new object where the field at the named path has its value set to the given value. + * This object remains unmodified. + */ +- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forPath:(FSTFieldPath *)fieldPath; + +/** + * Returns a new object where the field at the named path has been removed. If any segment of the + * path does not exist within this object's structure, no change is performed. + */ +- (FSTObjectValue *)objectByDeletingPath:(FSTFieldPath *)fieldPath; +@end + +/** + * An array value stored in Firestore. + */ +@interface FSTArrayValue : FSTFieldValue + +/** + * 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. + */ +- (instancetype)initWithValueNoCopy:(NSArray<FSTFieldValue *> *)value NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +- (NSArray<id> *)value; +- (NSArray<FSTFieldValue *> *)internalValue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTFieldValue.m b/Firestore/Source/Model/FSTFieldValue.m new file mode 100644 index 0000000..7f96a3c --- /dev/null +++ b/Firestore/Source/Model/FSTFieldValue.m @@ -0,0 +1,837 @@ +/* + * 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 "FSTFieldValue.h" + +#import "FIRGeoPoint+Internal.h" +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTComparison.h" +#import "FSTDatabaseID.h" +#import "FSTDocumentKey.h" +#import "FSTPath.h" +#import "FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldValue + +@interface FSTFieldValue () +- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other; +@end + +@implementation FSTFieldValue + +- (FSTTypeOrder)typeOrder { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (id)value { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (BOOL)isEqual:(id)other { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSUInteger)hash { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (NSString *)description { + return [[self value] description]; +} + +- (NSComparisonResult)defaultCompare:(FSTFieldValue *)other { + if (self.typeOrder > other.typeOrder) { + return NSOrderedDescending; + } else { + FSTAssert(self.typeOrder < other.typeOrder, + @"defaultCompare should not be used for values of same type."); + return NSOrderedAscending; + } +} + +@end + +#pragma mark - FSTNullValue + +@implementation FSTNullValue + ++ (instancetype)nullValue { + static FSTNullValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTNullValue alloc] init]; + }); + return sharedInstance; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderNull; +} + +- (id)value { + return [NSNull null]; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[self class]]; +} + +- (NSUInteger)hash { + return 47; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[self class]]) { + return NSOrderedSame; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTBooleanValue + +@interface FSTBooleanValue () +@property(nonatomic, assign, readonly) BOOL internalValue; +@end + +@implementation FSTBooleanValue + ++ (instancetype)trueValue { + static FSTBooleanValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTBooleanValue alloc] initWithValue:YES]; + }); + return sharedInstance; +} + ++ (instancetype)falseValue { + static FSTBooleanValue *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTBooleanValue alloc] initWithValue:NO]; + }); + return sharedInstance; +} + ++ (instancetype)booleanValue:(BOOL)value { + return value ? [FSTBooleanValue trueValue] : [FSTBooleanValue falseValue]; +} + +- (id)initWithValue:(BOOL)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderBoolean; +} + +- (id)value { + return self.internalValue ? @YES : @NO; +} + +- (BOOL)isEqual:(id)other { + // Since we create shared instances for true / false, we can use reference equality. + return self == other; +} + +- (NSUInteger)hash { + return self.internalValue ? 1231 : 1237; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTBooleanValue class]]) { + return FSTCompareBools(self.internalValue, ((FSTBooleanValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTNumberValue + +@implementation FSTNumberValue + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderNumber; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if (![other isKindOfClass:[FSTNumberValue class]]) { + return [self defaultCompare:other]; + } else { + if ([self isKindOfClass:[FSTDoubleValue class]]) { + double thisDouble = ((FSTDoubleValue *)self).internalValue; + if ([other isKindOfClass:[FSTDoubleValue class]]) { + return FSTCompareDoubles(thisDouble, ((FSTDoubleValue *)other).internalValue); + } else { + FSTAssert([other isKindOfClass:[FSTIntegerValue class]], @"Unknown number value: %@", + other); + return FSTCompareMixed(thisDouble, ((FSTIntegerValue *)other).internalValue); + } + } else { + int64_t thisInt = ((FSTIntegerValue *)self).internalValue; + if ([other isKindOfClass:[FSTIntegerValue class]]) { + return FSTCompareInt64s(thisInt, ((FSTIntegerValue *)other).internalValue); + } else { + FSTAssert([other isKindOfClass:[FSTDoubleValue class]], @"Unknown number value: %@", other); + return -1 * FSTCompareMixed(((FSTDoubleValue *)other).internalValue, thisInt); + } + } + } +} + +@end + +#pragma mark - FSTIntegerValue + +@interface FSTIntegerValue () +@property(nonatomic, assign, readonly) int64_t internalValue; +@end + +@implementation FSTIntegerValue + ++ (instancetype)integerValue:(int64_t)value { + return [[FSTIntegerValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(int64_t)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (id)value { + return @(self.internalValue); +} + +- (BOOL)isEqual:(id)other { + // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them + // equal via isEqual: + return [other isKindOfClass:[FSTIntegerValue class]] && + self.internalValue == ((FSTIntegerValue *)other).internalValue; +} + +- (NSUInteger)hash { + return (((NSUInteger)self.internalValue) ^ (NSUInteger)(self.internalValue >> 32)); +} + +// NOTE: compare: is implemented in NumberValue. + +@end + +#pragma mark - FSTDoubleValue + +@interface FSTDoubleValue () +@property(nonatomic, assign, readonly) double internalValue; +@end + +@implementation FSTDoubleValue + ++ (instancetype)doubleValue:(double)value { + // Normalize NaNs to match the behavior on the backend (which uses Double.doubletoLongBits()). + if (isnan(value)) { + return [FSTDoubleValue nanValue]; + } + return [[FSTDoubleValue alloc] initWithValue:value]; +} + ++ (instancetype)nanValue { + static FSTDoubleValue *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTDoubleValue alloc] initWithValue:NAN]; + }); + return sharedInstance; +} + +- (id)initWithValue:(double)value { + self = [super init]; + if (self) { + _internalValue = value; + } + return self; +} + +- (id)value { + return @(self.internalValue); +} + +- (BOOL)isEqual:(id)other { + // NOTE: DoubleValue and LongValue instances may compare: the same, but that doesn't make them + // equal via isEqual: + + // NOTE: isEqual: should compare NaN equal to itself and -0.0 not equal to 0.0. + + return [other isKindOfClass:[FSTDoubleValue class]] && + FSTDoubleBitwiseEquals(self.internalValue, ((FSTDoubleValue *)other).internalValue); +} + +- (NSUInteger)hash { + return FSTDoubleBitwiseHash(self.internalValue); +} + +// NOTE: compare: is implemented in NumberValue. + +@end + +#pragma mark - FSTStringValue + +@interface FSTStringValue () +@property(nonatomic, copy, readonly) NSString *internalValue; +@end + +// TODO(b/37267885): Add truncation support +@implementation FSTStringValue + ++ (instancetype)stringValue:(NSString *)value { + return [[FSTStringValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(NSString *)value { + self = [super init]; + if (self) { + _internalValue = [value copy]; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderString; +} + +- (id)value { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTStringValue class]] && + [self.internalValue isEqualToString:((FSTStringValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return self.internalValue ? 1 : 0; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTStringValue class]]) { + return FSTCompareStrings(self.internalValue, ((FSTStringValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTTimestampValue + +@interface FSTTimestampValue () +@property(nonatomic, strong, readonly) FSTTimestamp *internalValue; +@end + +@implementation FSTTimestampValue + ++ (instancetype)timestampValue:(FSTTimestamp *)value { + return [[FSTTimestampValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(FSTTimestamp *)value { + self = [super init]; + if (self) { + _internalValue = value; // FSTTimestamp is immutable. + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderTimestamp; +} + +- (id)value { + // For developers, we expose Timestamps as Dates. + return self.internalValue.approximateDateValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTTimestampValue class]] && + [self.internalValue isEqual:((FSTTimestampValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTTimestampValue class]]) { + return [self.internalValue compare:((FSTTimestampValue *)other).internalValue]; + } else if ([other isKindOfClass:[FSTServerTimestampValue class]]) { + // Concrete timestamps come before server timestamps. + return NSOrderedAscending; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTServerTimestampValue + +@implementation FSTServerTimestampValue + ++ (instancetype)serverTimestampValueWithLocalWriteTime:(FSTTimestamp *)localWriteTime { + return [[FSTServerTimestampValue alloc] initWithLocalWriteTime:localWriteTime]; +} + +- (id)initWithLocalWriteTime:(FSTTimestamp *)localWriteTime { + self = [super init]; + if (self) { + _localWriteTime = localWriteTime; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderTimestamp; +} + +- (NSNull *)value { + // For developers, server timestamps always evaluate to NSNull (for now, at least; b/62064202). + return [NSNull null]; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTServerTimestampValue class]] && + [self.localWriteTime isEqual:((FSTServerTimestampValue *)other).localWriteTime]; +} + +- (NSUInteger)hash { + return [self.localWriteTime hash]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<ServerTimestamp localTime=%@>", self.localWriteTime]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTServerTimestampValue class]]) { + return [self.localWriteTime compare:((FSTServerTimestampValue *)other).localWriteTime]; + } else if ([other isKindOfClass:[FSTTimestampValue class]]) { + // Server timestamps come after all concrete timestamps. + return NSOrderedDescending; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTGeoPointValue + +@interface FSTGeoPointValue () +@property(nonatomic, strong, readonly) FIRGeoPoint *internalValue; +@end + +@implementation FSTGeoPointValue + ++ (instancetype)geoPointValue:(FIRGeoPoint *)value { + return [[FSTGeoPointValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(FIRGeoPoint *)value { + self = [super init]; + if (self) { + _internalValue = value; // FIRGeoPoint is immutable. + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderGeoPoint; +} + +- (id)value { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTGeoPointValue class]] && + [self.internalValue isEqual:((FSTGeoPointValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTGeoPointValue class]]) { + return [self.internalValue compare:((FSTGeoPointValue *)other).internalValue]; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTBlobValue + +@interface FSTBlobValue () +@property(nonatomic, copy, readonly) NSData *internalValue; +@end + +// TODO(b/37267885): Add truncation support +@implementation FSTBlobValue + ++ (instancetype)blobValue:(NSData *)value { + return [[FSTBlobValue alloc] initWithValue:value]; +} + +- (id)initWithValue:(NSData *)value { + self = [super init]; + if (self) { + _internalValue = [value copy]; + } + return self; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderBlob; +} + +- (id)value { + return self.internalValue; +} + +- (BOOL)isEqual:(id)other { + return [other isKindOfClass:[FSTBlobValue class]] && + [self.internalValue isEqual:((FSTBlobValue *)other).internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTBlobValue class]]) { + return FSTCompareBytes(self.internalValue, ((FSTBlobValue *)other).internalValue); + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTReferenceValue + +@interface FSTReferenceValue () +@property(nonatomic, strong, readonly) FSTDocumentKey *key; +@end + +@implementation FSTReferenceValue + ++ (instancetype)referenceValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID { + return [[FSTReferenceValue alloc] initWithValue:value databaseID:databaseID]; +} + +- (id)initWithValue:(FSTDocumentKey *)value databaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _key = value; + _databaseID = databaseID; + } + return self; +} + +- (id)value { + return self.key; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderReference; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTReferenceValue class]]) { + return NO; + } + + FSTReferenceValue *otherRef = (FSTReferenceValue *)other; + return [self.key isEqualToKey:otherRef.key] && + [self.databaseID isEqualToDatabaseId:otherRef.databaseID]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.databaseID hash]; + result = 31 * result + [self.key hash]; + return result; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTReferenceValue class]]) { + FSTReferenceValue *ref = (FSTReferenceValue *)other; + NSComparisonResult cmp = [self.databaseID compare:ref.databaseID]; + return cmp != NSOrderedSame ? cmp : [self.key compare:ref.key]; + } else { + return [self defaultCompare:other]; + } +} + +@end + +#pragma mark - FSTObjectValue + +@interface FSTObjectValue () +@property(nonatomic, strong, readonly) + FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *internalValue; +@end + +@implementation FSTObjectValue + ++ (instancetype)objectValue { + static FSTObjectValue *sharedEmptyInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *empty = + [FSTImmutableSortedDictionary dictionaryWithComparator:FSTStringComparator]; + sharedEmptyInstance = [[FSTObjectValue alloc] initWithImmutableDictionary:empty]; + }); + return sharedEmptyInstance; +} + +- (instancetype)initWithImmutableDictionary: + (FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *)value { + self = [super init]; + if (self) { + _internalValue = value; // FSTImmutableSortedDictionary is immutable. + } + return self; +} + +- (id)initWithDictionary:(NSDictionary<NSString *, FSTFieldValue *> *)value { + FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *dictionary = + [FSTImmutableSortedDictionary dictionaryWithDictionary:value comparator:FSTStringComparator]; + return [self initWithImmutableDictionary:dictionary]; +} + +- (id)value { + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + [self.internalValue + enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { + result[key] = [obj value]; + }]; + return result; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderObject; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTObjectValue class]]) { + return NO; + } + + FSTObjectValue *otherObj = other; + return [self.internalValue isEqual:otherObj.internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTObjectValue class]]) { + FSTImmutableSortedDictionary *selfDict = self.internalValue; + FSTImmutableSortedDictionary *otherDict = ((FSTObjectValue *)other).internalValue; + NSEnumerator *enumerator1 = [selfDict keyEnumerator]; + NSEnumerator *enumerator2 = [otherDict keyEnumerator]; + NSString *key1 = [enumerator1 nextObject]; + NSString *key2 = [enumerator2 nextObject]; + while (key1 && key2) { + NSComparisonResult keyCompare = [key1 compare:key2]; + if (keyCompare != NSOrderedSame) { + return keyCompare; + } + NSComparisonResult valueCompare = [selfDict[key1] compare:otherDict[key2]]; + if (valueCompare != NSOrderedSame) { + return valueCompare; + } + key1 = [enumerator1 nextObject]; + key2 = [enumerator2 nextObject]; + } + // Only equal if both enumerators are exhausted. + return FSTCompareBools(key1 != nil, key2 != nil); + } else { + return [self defaultCompare:other]; + } +} + +- (nullable FSTFieldValue *)valueForPath:(FSTFieldPath *)fieldPath { + FSTFieldValue *value = self; + for (int i = 0, max = fieldPath.length; value && i < max; i++) { + if (![value isMemberOfClass:[FSTObjectValue class]]) { + return nil; + } + + NSString *fieldName = fieldPath[i]; + value = ((FSTObjectValue *)value).internalValue[fieldName]; + } + + return value; +} + +- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forPath:(FSTFieldPath *)fieldPath { + FSTAssert([fieldPath length] > 0, @"Cannot set value with an empty path"); + + NSString *childName = [fieldPath firstSegment]; + if ([fieldPath length] == 1) { + // Recursive base case: + return [self objectBySettingValue:value forField:childName]; + } else { + // Nested path. Recursively generate a new sub-object and then wrap a new FSTObjectValue around + // the result. + FSTFieldValue *child = [_internalValue objectForKey:childName]; + FSTObjectValue *childObject; + if ([child isKindOfClass:[FSTObjectValue class]]) { + childObject = (FSTObjectValue *)child; + } else { + // If the child is not found or is a primitive type, pretend as if an empty object lived + // there. + childObject = [FSTObjectValue objectValue]; + } + FSTFieldValue *newChild = + [childObject objectBySettingValue:value forPath:[fieldPath pathByRemovingFirstSegment]]; + return [self objectBySettingValue:newChild forField:childName]; + } +} + +- (FSTObjectValue *)objectByDeletingPath:(FSTFieldPath *)fieldPath { + FSTAssert([fieldPath length] > 0, @"Cannot delete an empty path"); + NSString *childName = [fieldPath firstSegment]; + if ([fieldPath length] == 1) { + return [[FSTObjectValue alloc] + initWithImmutableDictionary:[_internalValue dictionaryByRemovingObjectForKey:childName]]; + } else { + FSTFieldValue *child = _internalValue[childName]; + if ([child isKindOfClass:[FSTObjectValue class]]) { + FSTObjectValue *newChild = + [((FSTObjectValue *)child) objectByDeletingPath:[fieldPath pathByRemovingFirstSegment]]; + return [self objectBySettingValue:newChild forField:childName]; + } else { + // If the child is not found or is a primitive type, make no modifications + return self; + } + } +} + +- (FSTObjectValue *)objectBySettingValue:(FSTFieldValue *)value forField:(NSString *)field { + return [[FSTObjectValue alloc] + initWithImmutableDictionary:[_internalValue dictionaryBySettingObject:value forKey:field]]; +} + +@end + +@interface FSTArrayValue () +@property(nonatomic, strong, readonly) NSArray<FSTFieldValue *> *internalValue; +@end + +#pragma mark - FSTArrayValue + +@implementation FSTArrayValue + +- (id)initWithValueNoCopy:(NSArray<FSTFieldValue *> *)value { + self = [super init]; + if (self) { + // Does not copy, assumes the caller has already copied. + _internalValue = value; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[self class]]) { + return NO; + } + + // NSArray's isEqual does the right thing for our purposes. + FSTArrayValue *otherArray = other; + return [self.internalValue isEqual:otherArray.internalValue]; +} + +- (NSUInteger)hash { + return [self.internalValue hash]; +} + +- (id)value { + NSMutableArray *result = [NSMutableArray arrayWithCapacity:_internalValue.count]; + [self.internalValue enumerateObjectsUsingBlock:^(FSTFieldValue *obj, NSUInteger idx, BOOL *stop) { + [result addObject:[obj value]]; + }]; + return result; +} + +- (FSTTypeOrder)typeOrder { + return FSTTypeOrderArray; +} + +- (NSComparisonResult)compare:(FSTFieldValue *)other { + if ([other isKindOfClass:[FSTArrayValue class]]) { + NSArray<FSTFieldValue *> *selfArray = self.internalValue; + NSArray<FSTFieldValue *> *otherArray = ((FSTArrayValue *)other).internalValue; + NSUInteger minLength = MIN(selfArray.count, otherArray.count); + for (NSUInteger i = 0; i < minLength; i++) { + NSComparisonResult cmp = [selfArray[i] compare:otherArray[i]]; + if (cmp != NSOrderedSame) { + return cmp; + } + } + return FSTCompareUIntegers(selfArray.count, otherArray.count); + } else { + return [self defaultCompare:other]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutation.h b/Firestore/Source/Model/FSTMutation.h new file mode 100644 index 0000000..ef7f1c8 --- /dev/null +++ b/Firestore/Source/Model/FSTMutation.h @@ -0,0 +1,325 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTDocument; +@class FSTDocumentKey; +@class FSTFieldPath; +@class FSTFieldValue; +@class FSTMaybeDocument; +@class FSTObjectValue; +@class FSTSnapshotVersion; +@class FSTTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldMask + +/** + * Provides a set of fields that can be used to partially patch a document. FieldMask is used in + * conjunction with ObjectValue. + * + * Examples: + * foo - Overwrites foo entirely with the provided value. If foo is not present in the companion + * ObjectValue, the field is deleted. + * foo.bar - Overwrites only the field bar of the object foo. If foo is not an object, foo is + * replaced with an object containing bar. + */ +@interface FSTFieldMask : NSObject +- (id)init __attribute__((unavailable("Use initWithFields:"))); + +/** + * Initializes the field mask with the given field paths. Caller is expected to either copy or + * or release the array of fields. + */ +- (instancetype)initWithFields:(NSArray<FSTFieldPath *> *)fields NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong, readonly) NSArray<FSTFieldPath *> *fields; +@end + +#pragma mark - FSTFieldTransform + +/** Represents a transform within a TransformMutation. */ +@protocol FSTTransformOperation <NSObject> +@end + +/** Transforms a value into a server-generated timestamp. */ +@interface FSTServerTimestampTransform : NSObject <FSTTransformOperation> ++ (instancetype)serverTimestampTransform; +@end + +/** A field path and the FSTTransformOperation to perform upon it. */ +@interface FSTFieldTransform : NSObject +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithPath:(FSTFieldPath *)path + transform:(id<FSTTransformOperation>)transform NS_DESIGNATED_INITIALIZER; +@property(nonatomic, strong, readonly) FSTFieldPath *path; +@property(nonatomic, strong, readonly) id<FSTTransformOperation> transform; +@end + +#pragma mark - FSTPrecondition + +typedef NS_ENUM(NSUInteger, FSTPreconditionExists) { + FSTPreconditionExistsNotSet, + FSTPreconditionExistsYes, + FSTPreconditionExistsNo, +}; + +/** + * Encodes a precondition for a mutation. This follows the model that the backend accepts with the + * special case of an explicit "empty" precondition (meaning no precondition). + */ +@interface FSTPrecondition : NSObject + +/** Creates a new FSTPrecondition with an exists flag. */ ++ (FSTPrecondition *)preconditionWithExists:(BOOL)exists; + +/** Creates a new FSTPrecondition based on a time the document exists at. */ ++ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime; + +/** Returns a precondition representing no precondition. */ ++ (FSTPrecondition *)none; + +/** + * Returns true if the preconditions is valid for the given document (or null if no document is + * available). + */ +- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc; + +/** Returns whether this Precondition represents no precondition. */ +- (BOOL)isNone; + +/** If set, preconditions a mutation based on the last updateTime. */ +@property(nonatomic, strong, readonly, nullable) FSTSnapshotVersion *updateTime; + +/** + * If set, preconditions a mutation based on whether the document exists. + * Uses FSTPreconditionExistsNotSet to mark as unset. + */ +@property(nonatomic, assign, readonly) FSTPreconditionExists exists; + +@end + +#pragma mark - FSTMutationResult + +@interface FSTMutationResult : NSObject + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version + transformResults:(NSArray<FSTFieldValue *> *_Nullable)transformResults + NS_DESIGNATED_INITIALIZER; + +/** The version at which the mutation was committed or null for a delete. */ +@property(nonatomic, strong, readonly, nullable) FSTSnapshotVersion *version; + +/** + * The resulting fields returned from the backend after a FSTTransformMutation has been committed. + * Contains one FieldValue for each FSTFieldTransform that was in the mutation. + * + * Will be nil if the mutation was not a FSTTransformMutation. + */ +@property(nonatomic, strong, readonly) NSArray<FSTFieldValue *> *_Nullable transformResults; + +@end + +#pragma mark - FSTMutation + +/** + * A mutation describes a self-contained change to a document. Mutations can create, replace, + * delete, and update subsets of documents. + * + * ## Subclassing Notes + * + * Subclasses of FSTMutation need to implement -applyTo:hasLocalMutations: to implement the + * actual the behavior of mutation as applied to some source document. + */ +@interface FSTMutation : NSObject + +- (id)init NS_UNAVAILABLE; + +- (instancetype)initWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER; + +/** + * 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 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 + * this is merely a local (latency-compensated) application, and the resulting document will + * have its hasLocalMutations flag set. + * + * @return The mutated document. The returned document may be nil, but only if maybeDoc was nil + * and the mutation would not create a new document. + * + * NOTE: We preserve the version of the base document only in case of Set or Patch mutation to + * denote what version of original document we've changed. In case of DeleteMutation we always reset + * the version. + * + * Here's the expected transition table. + * + * MUTATION APPLIED TO RESULTS IN + * + * SetMutation Document(v3) Document(v3) + * SetMutation DeletedDocument(v3) Document(v0) + * SetMutation nil Document(v0) + * PatchMutation Document(v3) Document(v3) + * PatchMutation DeletedDocument(v3) DeletedDocument(v3) + * PatchMutation nil nil + * TransformMutation Document(v3) Document(v3) + * TransformMutation DeletedDocument(v3) DeletedDocument(v3) + * TransformMutation nil nil + * DeleteMutation Document(v3) DeletedDocument(v0) + * DeleteMutation DeletedDocument(v3) DeletedDocument(v0) + * DeleteMutation nil DeletedDocument(v0) + * + * Note that FSTTransformMutations don't create FSTDocuments (in the case of being applied to an + * FSTDeletedDocument), even though they would on the backend. This is because the client always + * combines the FSTTransformMutation with a FSTSetMutation or FSTPatchMutation and we only want to + * 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 + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)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; + +@property(nonatomic, strong, readonly) FSTDocumentKey *key; + +/** The precondition for this mutation. */ +@property(nonatomic, strong, readonly) FSTPrecondition *precondition; + +@end + +#pragma mark - FSTSetMutation + +/** + * A mutation that creates or replaces the document at the given key with the object value + * contents. + */ +@interface FSTSetMutation : FSTMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE; + +/** + * Initializes the set mutation. + * + * @param key Identifies the location of the document to mutate. + * @param value An object value that describes the contents to store at the location named by the + * key. + * @param precondition The precondition for this mutation. + */ +- (instancetype)initWithKey:(FSTDocumentKey *)key + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER; + +/** The object value to use when setting the document. */ +@property(nonatomic, strong, readonly) FSTObjectValue *value; +@end + +#pragma mark - FSTPatchMutation + +/** + * A mutation that modifies fields of the document at the given key with the given values. The + * values are applied through a field mask: + * + * * When a field is in both the mask and the values, the corresponding field is updated. + * * When a field is in neither the mask nor the values, the corresponding field is unmodified. + * * When a field is in the mask but not in the values, the corresponding field is deleted. + * * When a field is not in the mask but is in the values, the values map is ignored. + */ +@interface FSTPatchMutation : FSTMutation + +/** Returns the precondition for the given FSTPrecondition. */ +- (instancetype)initWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE; + +/** + * Initializes a new patch mutation with an explicit FSTFieldMask and FSTObjectValue representing + * the updates to perform + * + * @param key Identifies the location of the document to mutate. + * @param fieldMask The field mask specifying at what locations the data in value should be + * applied. + * @param value An FSTObjectValue containing the data to be written (using the paths in fieldMask + * to determine the locations at which it should be applied). + * @param precondition The precondition for this mutation. + */ +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldMask:(FSTFieldMask *)fieldMask + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition NS_DESIGNATED_INITIALIZER; + +/** The fields and associated values to use when patching the document. */ +@property(nonatomic, strong, readonly) FSTObjectValue *value; + +/** + * A mask to apply to |value|, where only fields that are in both the fieldMask and the value + * will be updated. + */ +@property(nonatomic, strong, readonly) FSTFieldMask *fieldMask; + +@end + +#pragma mark - FSTTransformMutation + +/** + * A mutation that modifies specific fields of the document with transform operations. Currently + * the only supported transform is a server timestamp, but IP Address, increment(n), etc. could + * be supported in the future. + * + * It is somewhat similar to an FSTPatchMutation in that it patches specific fields and has no + * effect when applied to nil or an FSTDeletedDocument (see comment on [FSTMutation applyTo] for + * rationale). + */ +@interface FSTTransformMutation : FSTMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + precondition:(FSTPrecondition *)precondition NS_UNAVAILABLE; + +/** + * Initializes a new transform mutation with the specified field transforms. + * + * @param key Identifies the location of the document to mutate. + * @param fieldTransforms A list of FSTFieldTransform objects to perform to the document. + */ +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms + NS_DESIGNATED_INITIALIZER; + +/** The field transforms to use when transforming the document. */ +@property(nonatomic, strong, readonly) NSArray<FSTFieldTransform *> *fieldTransforms; + +@end + +#pragma mark - FSTDeleteMutation + +@interface FSTDeleteMutation : FSTMutation + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutation.m b/Firestore/Source/Model/FSTMutation.m new file mode 100644 index 0000000..9704fde --- /dev/null +++ b/Firestore/Source/Model/FSTMutation.m @@ -0,0 +1,575 @@ +/* + * 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 "FSTMutation.h" + +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTFieldValue.h" +#import "FSTPath.h" +#import "FSTSnapshotVersion.h" +#import "FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTFieldMask + +@implementation FSTFieldMask + +- (instancetype)initWithFields:(NSArray<FSTFieldPath *> *)fields { + if (self = [super init]) { + _fields = fields; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTFieldMask class]]) { + return NO; + } + + FSTFieldMask *otherMask = (FSTFieldMask *)other; + return [self.fields isEqual:otherMask.fields]; +} + +- (NSUInteger)hash { + return self.fields.hash; +} +@end + +#pragma mark - FSTServerTimestampTransform + +@implementation FSTServerTimestampTransform + ++ (instancetype)serverTimestampTransform { + static FSTServerTimestampTransform *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [[FSTServerTimestampTransform alloc] init]; + }); + return sharedInstance; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + return [other isKindOfClass:[FSTServerTimestampTransform class]]; +} + +- (NSUInteger)hash { + // arbitrary number since all instances are equal. + return 37; +} + +@end + +#pragma mark - FSTFieldTransform + +@implementation FSTFieldTransform + +- (instancetype)initWithPath:(FSTFieldPath *)path transform:(id<FSTTransformOperation>)transform { + self = [super init]; + if (self) { + _path = path; + _transform = transform; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) return YES; + if (!other || ![[other class] isEqual:[self class]]) return NO; + FSTFieldTransform *otherFieldTransform = other; + return [self.path isEqual:otherFieldTransform.path] && + [self.transform isEqual:otherFieldTransform.transform]; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.path hash]; + hash = hash * 31 + [self.transform hash]; + return hash; +} + +@end + +#pragma mark - FSTPrecondition + +@implementation FSTPrecondition + ++ (FSTPrecondition *)preconditionWithExists:(BOOL)exists { + FSTPreconditionExists existsEnum = exists ? FSTPreconditionExistsYes : FSTPreconditionExistsNo; + return [[FSTPrecondition alloc] initWithUpdateTime:nil exists:existsEnum]; +} + ++ (FSTPrecondition *)preconditionWithUpdateTime:(FSTSnapshotVersion *)updateTime { + return [[FSTPrecondition alloc] initWithUpdateTime:updateTime exists:FSTPreconditionExistsNotSet]; +} + ++ (FSTPrecondition *)none { + static dispatch_once_t onceToken; + static FSTPrecondition *noPrecondition; + dispatch_once(&onceToken, ^{ + noPrecondition = + [[FSTPrecondition alloc] initWithUpdateTime:nil exists:FSTPreconditionExistsNotSet]; + }); + return noPrecondition; +} + +- (instancetype)initWithUpdateTime:(FSTSnapshotVersion *_Nullable)updateTime + exists:(FSTPreconditionExists)exists { + if (self = [super init]) { + _updateTime = updateTime; + _exists = exists; + } + return self; +} + +- (BOOL)isValidForDocument:(FSTMaybeDocument *_Nullable)maybeDoc { + if (self.updateTime) { + return + [maybeDoc isKindOfClass:[FSTDocument class]] && [maybeDoc.version isEqual:self.updateTime]; + } else if (self.exists != FSTPreconditionExistsNotSet) { + if (self.exists == FSTPreconditionExistsYes) { + return [maybeDoc isKindOfClass:[FSTDocument class]]; + } else { + FSTAssert(self.exists == FSTPreconditionExistsNo, @"Invalid precondition"); + return maybeDoc == nil || [maybeDoc isKindOfClass:[FSTDeletedDocument class]]; + } + } else { + FSTAssert(self.isNone, @"Precondition should be empty"); + return YES; + } +} + +- (BOOL)isNone { + return self.updateTime == nil && self.exists == FSTPreconditionExistsNotSet; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } + + if (![other isKindOfClass:[FSTPrecondition class]]) { + return NO; + } + + FSTPrecondition *otherPrecondition = (FSTPrecondition *)other; + // Compare references to cover nil equality + return (self.updateTime == otherPrecondition.updateTime || + [self.updateTime isEqual:otherPrecondition.updateTime]) && + self.exists == otherPrecondition.exists; +} + +- (NSUInteger)hash { + NSUInteger hash = [self.updateTime hash]; + hash = hash * 31 + self.exists; + return hash; +} + +- (NSString *)description { + if (self.isNone) { + return @"<FSTPrecondition <none>>"; + } else { + NSString *existsString; + switch (self.exists) { + case FSTPreconditionExistsYes: + existsString = @"yes"; + break; + case FSTPreconditionExistsNo: + existsString = @"no"; + break; + default: + existsString = @"<not-set>"; + break; + } + return [NSString stringWithFormat:@"<FSTPrecondition updateTime=%@ exists=%@>", self.updateTime, + existsString]; + } +} + +@end + +#pragma mark - FSTMutationResult + +@implementation FSTMutationResult + +- (instancetype)initWithVersion:(FSTSnapshotVersion *_Nullable)version + transformResults:(NSArray<FSTFieldValue *> *_Nullable)transformResults { + if (self = [super init]) { + _version = version; + _transformResults = transformResults; + } + return self; +} + +@end + +#pragma mark - FSTMutation + +@implementation FSTMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key precondition:(FSTPrecondition *)precondition { + if (self = [super init]) { + _key = key; + _precondition = precondition; + } + return self; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime { + return [self applyTo:maybeDoc localWriteTime:localWriteTime mutationResult:nil]; +} + +@end + +#pragma mark - FSTSetMutation + +@implementation FSTSetMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition { + if (self = [super initWithKey:key precondition:precondition]) { + _value = value; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTSetMutation key=%@ value=%@ precondition=%@>", self.key, + self.value, self.precondition]; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTSetMutation class]]) { + return NO; + } + + FSTSetMutation *otherMutation = (FSTSetMutation *)other; + return [self.key isEqual:otherMutation.key] && [self.value isEqual:otherMutation.value] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.value hash]; + return result; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTSetMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + BOOL hasLocalMutations = (mutationResult == nil); + if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { + // If the document didn't exist before, create it. + return [FSTDocument documentWithData:self.value + key:self.key + version:[FSTSnapshotVersion noVersion] + hasLocalMutations:hasLocalMutations]; + } + + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only set a document with the same key"); + return [FSTDocument documentWithData:self.value + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} +@end + +#pragma mark - FSTPatchMutation + +@implementation FSTPatchMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldMask:(FSTFieldMask *)fieldMask + value:(FSTObjectValue *)value + precondition:(FSTPrecondition *)precondition { + self = [super initWithKey:key precondition:precondition]; + if (self) { + _fieldMask = fieldMask; + _value = value; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTPatchMutation class]]) { + return NO; + } + + FSTPatchMutation *otherMutation = (FSTPatchMutation *)other; + return [self.key isEqual:otherMutation.key] && [self.fieldMask isEqual:otherMutation.fieldMask] && + [self.value isEqual:otherMutation.value] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.fieldMask hash]; + result = 31 * result + [self.value hash]; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTPatchMutation key=%@ mask=%@ value=%@ precondition=%@>", + self.key, self.fieldMask, self.value, self.precondition]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, @"Transform results received by FSTPatchMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + BOOL hasLocalMutations = (mutationResult == nil); + if (!maybeDoc || [maybeDoc isMemberOfClass:[FSTDeletedDocument class]]) { + // Precondition applied, so create the document if necessary + FSTDocumentKey *key = maybeDoc ? maybeDoc.key : self.key; + FSTSnapshotVersion *version = maybeDoc ? maybeDoc.version : [FSTSnapshotVersion noVersion]; + maybeDoc = [FSTDocument documentWithData:[FSTObjectValue objectValue] + key:key + version:version + hasLocalMutations:hasLocalMutations]; + } + + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + + FSTObjectValue *newData = [self patchObjectValue:doc.data]; + return [FSTDocument documentWithData:newData + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} + +- (FSTObjectValue *)patchObjectValue:(FSTObjectValue *)objectValue { + FSTObjectValue *result = objectValue; + for (FSTFieldPath *fieldPath in self.fieldMask.fields) { + FSTFieldValue *newValue = [self.value valueForPath:fieldPath]; + if (newValue) { + result = [result objectBySettingValue:newValue forPath:fieldPath]; + } else { + result = [result objectByDeletingPath:fieldPath]; + } + } + return result; +} + +@end + +@implementation FSTTransformMutation + +- (instancetype)initWithKey:(FSTDocumentKey *)key + fieldTransforms:(NSArray<FSTFieldTransform *> *)fieldTransforms { + // NOTE: We set a precondition of exists: true as a safety-check, since we always combine + // FSTTransformMutations with a FSTSetMutation or FSTPatchMutation which (if successful) should + // end up with an existing document. + if (self = [super initWithKey:key precondition:[FSTPrecondition preconditionWithExists:YES]]) { + _fieldTransforms = fieldTransforms; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTTransformMutation class]]) { + return NO; + } + + FSTTransformMutation *otherMutation = (FSTTransformMutation *)other; + return [self.key isEqual:otherMutation.key] && + [self.fieldTransforms isEqual:otherMutation.fieldTransforms] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + result = 31 * result + [self.fieldTransforms hash]; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTTransformMutation key=%@ transforms=%@ precondition=%@>", + self.key, self.fieldTransforms, self.precondition]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + if (mutationResult) { + FSTAssert(mutationResult.transformResults, + @"Transform results missing for FSTTransformMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + // We only support transforms with precondition exists, so we can only apply it to an existing + // document + FSTAssert([maybeDoc isMemberOfClass:[FSTDocument class]], @"Unknown MaybeDocument type %@", + [maybeDoc class]); + FSTDocument *doc = (FSTDocument *)maybeDoc; + + FSTAssert([doc.key isEqual:self.key], @"Can only patch a document with the same key"); + + BOOL hasLocalMutations = (mutationResult == nil); + NSArray<FSTFieldValue *> *transformResults = + mutationResult ? mutationResult.transformResults + : [self localTransformResultsWithWriteTime:localWriteTime]; + FSTObjectValue *newData = [self transformObject:doc.data transformResults:transformResults]; + return [FSTDocument documentWithData:newData + key:doc.key + version:doc.version + hasLocalMutations:hasLocalMutations]; +} + +/** + * 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 localWriteTime The local time of the transform mutation (used to generate + * FSTServerTimestampValues). + * @return The transform results array. + */ +- (NSArray<FSTFieldValue *> *)localTransformResultsWithWriteTime:(FSTTimestamp *)localWriteTime { + NSMutableArray<FSTFieldValue *> *transformResults = [NSMutableArray array]; + for (FSTFieldTransform *fieldTransform in self.fieldTransforms) { + if ([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]]) { + [transformResults addObject:[FSTServerTimestampValue + serverTimestampValueWithLocalWriteTime:localWriteTime]]; + } else { + FSTFail(@"Encountered unknown transform: %@", fieldTransform); + } + } + return transformResults; +} + +- (FSTObjectValue *)transformObject:(FSTObjectValue *)objectValue + transformResults:(NSArray<FSTFieldValue *> *)transformResults { + FSTAssert(transformResults.count == self.fieldTransforms.count, + @"Transform results length mismatch."); + + for (NSUInteger i = 0; i < self.fieldTransforms.count; i++) { + FSTFieldTransform *fieldTransform = self.fieldTransforms[i]; + id<FSTTransformOperation> transform = fieldTransform.transform; + FSTFieldPath *fieldPath = fieldTransform.path; + if ([transform isKindOfClass:[FSTServerTimestampTransform class]]) { + objectValue = [objectValue objectBySettingValue:transformResults[i] forPath:fieldPath]; + } else { + FSTFail(@"Encountered unknown transform: %@", transform); + } + } + return objectValue; +} + +@end + +#pragma mark - FSTDeleteMutation + +@implementation FSTDeleteMutation + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isKindOfClass:[FSTDeleteMutation class]]) { + return NO; + } + + FSTDeleteMutation *otherMutation = (FSTDeleteMutation *)other; + return [self.key isEqual:otherMutation.key] && + [self.precondition isEqual:otherMutation.precondition]; +} + +- (NSUInteger)hash { + NSUInteger result = [self.key hash]; + result = 31 * result + [self.precondition hash]; + return result; +} + +- (NSString *)description { + return [NSString + stringWithFormat:@"<FSTDeleteMutation key=%@ precondition=%@>", self.key, self.precondition]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + localWriteTime:(FSTTimestamp *)localWriteTime + mutationResult:(FSTMutationResult *_Nullable)mutationResult { + if (mutationResult) { + FSTAssert(!mutationResult.transformResults, + @"Transform results received by FSTDeleteMutation."); + } + + if (![self.precondition isValidForDocument:maybeDoc]) { + return maybeDoc; + } + + if (maybeDoc) { + FSTAssert([maybeDoc.key isEqual:self.key], @"Can only delete a document with the same key"); + } + + return [FSTDeletedDocument documentWithKey:self.key version:[FSTSnapshotVersion noVersion]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.h b/Firestore/Source/Model/FSTMutationBatch.h new file mode 100644 index 0000000..cbc31b6 --- /dev/null +++ b/Firestore/Source/Model/FSTMutationBatch.h @@ -0,0 +1,119 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentKeySet.h" +#import "FSTDocumentVersionDictionary.h" +#import "FSTTypes.h" + +@class FSTMutation; +@class FSTTimestamp; +@class FSTMutationResult; +@class FSTMutationBatchResult; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A BatchID that was searched for and not found or a batch ID value known to be before all known + * batches. + * + * FSTBatchID values from the local store are non-negative so this value is before all batches. + */ +extern const FSTBatchID kFSTBatchIDUnknown; + +/** + * A batch of mutations that will be sent as one unit to the backend. Batches can be marked as a + * tombstone if the mutation queue does not remove them immediately. When a batch is a tombstone + * it has no mutations. + */ +@interface FSTMutationBatch : NSObject + +/** Initializes a mutation batch with the given batchID, localWriteTime, and mutations. */ +- (instancetype)initWithBatchID:(FSTBatchID)batchID + localWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray<FSTMutation *> *)mutations NS_DESIGNATED_INITIALIZER; + +- (id)init NS_UNAVAILABLE; + +/** + * Applies all the mutations in this FSTMutationBatch to the specified document. + * + * @param maybeDoc The document to apply mutations to. + * @param documentKey The key of the document to apply mutations to. + * @param mutationBatchResult The result of applying the MutationBatch to the backend. If omitted + * it's assumed that this is a local (latency-compensated) application and documents will have + * their hasLocalMutations flag set. + */ +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey + mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult; + +/** + * A helper version of applyTo for applying mutations locally (without a mutation batch result from + * the backend). + */ +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey; + +/** + * Returns YES if this mutation batch has already been removed from the mutation queue. + * + * Note that not all implementations of the FSTMutationQueue necessarily use tombstones as a part + * of their implementation and generally speaking no code outside the mutation queues should really + * care about this. + */ +- (BOOL)isTombstone; + +/** Converts this batch to a tombstone. */ +- (FSTMutationBatch *)toTombstone; + +/** Returns the set of unique keys referenced by all mutations in the batch. */ +- (FSTDocumentKeySet *)keys; + +@property(nonatomic, assign, readonly) FSTBatchID batchID; +@property(nonatomic, strong, readonly) FSTTimestamp *localWriteTime; +@property(nonatomic, strong, readonly) NSArray<FSTMutation *> *mutations; + +@end + +#pragma mark - FSTMutationBatchResult + +/** The result of applying a mutation batch to the backend. */ +@interface FSTMutationBatchResult : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a new FSTMutationBatchResult for the given batch and results. There must be one result + * for each mutation in the batch. This static factory caches a document=>version mapping + * (as docVersions). + */ ++ (instancetype)resultWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)mutationResults + streamToken:(nullable NSData *)streamToken; + +@property(nonatomic, strong, readonly) FSTMutationBatch *batch; +@property(nonatomic, strong, readonly) FSTSnapshotVersion *commitVersion; +@property(nonatomic, strong, readonly) NSArray<FSTMutationResult *> *mutationResults; +@property(nonatomic, strong, readonly, nullable) NSData *streamToken; +@property(nonatomic, strong, readonly) FSTDocumentVersionDictionary *docVersions; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTMutationBatch.m b/Firestore/Source/Model/FSTMutationBatch.m new file mode 100644 index 0000000..ed0e659 --- /dev/null +++ b/Firestore/Source/Model/FSTMutationBatch.m @@ -0,0 +1,176 @@ +/* + * 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 "FSTMutationBatch.h" + +#import "FSTAssert.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTMutation.h" +#import "FSTSnapshotVersion.h" +#import "FSTTimestamp.h" + +NS_ASSUME_NONNULL_BEGIN + +const FSTBatchID kFSTBatchIDUnknown = -1; + +@implementation FSTMutationBatch + +- (instancetype)initWithBatchID:(FSTBatchID)batchID + localWriteTime:(FSTTimestamp *)localWriteTime + mutations:(NSArray<FSTMutation *> *)mutations { + self = [super init]; + if (self) { + _batchID = batchID; + _localWriteTime = localWriteTime; + _mutations = mutations; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (self == other) { + return YES; + } else if (![other isKindOfClass:[FSTMutationBatch class]]) { + return NO; + } + + FSTMutationBatch *otherBatch = (FSTMutationBatch *)other; + return self.batchID == otherBatch.batchID && + [self.localWriteTime isEqual:otherBatch.localWriteTime] && + [self.mutations isEqual:otherBatch.mutations]; +} + +- (NSUInteger)hash { + NSUInteger result = (NSUInteger)self.batchID; + result = result * 31 + self.localWriteTime.hash; + result = result * 31 + self.mutations.hash; + return result; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTMutationBatch: id=%d, localWriteTime=%@, mutations=%@>", + self.batchID, self.localWriteTime, self.mutations]; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey + mutationBatchResult:(FSTMutationBatchResult *_Nullable)mutationBatchResult { + FSTAssert(!maybeDoc || [maybeDoc.key isEqualToKey:documentKey], + @"applyTo: key %@ doesn't match maybeDoc key %@", documentKey, maybeDoc.key); + if (mutationBatchResult) { + FSTAssert(mutationBatchResult.mutationResults.count == self.mutations.count, + @"Mismatch between mutations length (%lu) and results length (%lu)", + (unsigned long)self.mutations.count, + (unsigned long)mutationBatchResult.mutationResults.count); + } + + for (NSUInteger i = 0; i < self.mutations.count; i++) { + FSTMutation *mutation = self.mutations[i]; + FSTMutationResult *_Nullable mutationResult = mutationBatchResult.mutationResults[i]; + if ([mutation.key isEqualToKey:documentKey]) { + maybeDoc = [mutation applyTo:maybeDoc + localWriteTime:self.localWriteTime + mutationResult:mutationResult]; + } + } + return maybeDoc; +} + +- (FSTMaybeDocument *_Nullable)applyTo:(FSTMaybeDocument *_Nullable)maybeDoc + documentKey:(FSTDocumentKey *)documentKey { + return [self applyTo:maybeDoc documentKey:documentKey mutationBatchResult:nil]; +} + +- (BOOL)isTombstone { + return self.mutations.count == 0; +} + +- (FSTMutationBatch *)toTombstone { + return [[FSTMutationBatch alloc] initWithBatchID:self.batchID + localWriteTime:self.localWriteTime + mutations:@[]]; +} + +// TODO(klimt): This could use NSMutableDictionary instead. +- (FSTDocumentKeySet *)keys { + FSTDocumentKeySet *set = [FSTDocumentKeySet keySet]; + for (FSTMutation *mutation in self.mutations) { + set = [set setByAddingObject:mutation.key]; + } + return set; +} + +@end + +#pragma mark - FSTMutationBatchResult + +@interface FSTMutationBatchResult () +- (instancetype)initWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)mutationResults + streamToken:(nullable NSData *)streamToken + docVersions:(FSTDocumentVersionDictionary *)docVersions NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTMutationBatchResult + +- (instancetype)initWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)mutationResults + streamToken:(nullable NSData *)streamToken + docVersions:(FSTDocumentVersionDictionary *)docVersions { + if (self = [super init]) { + _batch = batch; + _commitVersion = commitVersion; + _mutationResults = mutationResults; + _streamToken = streamToken; + _docVersions = docVersions; + } + return self; +} + ++ (instancetype)resultWithBatch:(FSTMutationBatch *)batch + commitVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)mutationResults + streamToken:(nullable NSData *)streamToken { + FSTAssert(batch.mutations.count == mutationResults.count, + @"Mutations sent %lu must equal results received %lu", + (unsigned long)batch.mutations.count, (unsigned long)mutationResults.count); + + FSTDocumentVersionDictionary *docVersions = + [FSTDocumentVersionDictionary documentVersionDictionary]; + NSArray<FSTMutation *> *mutations = batch.mutations; + for (NSUInteger i = 0; i < mutations.count; i++) { + FSTSnapshotVersion *_Nullable version = mutationResults[i].version; + if (!version) { + // deletes don't have a version, so we substitute the commitVersion + // of the entire batch. + version = commitVersion; + } + + docVersions = [docVersions dictionaryBySettingObject:version forKey:mutations[i].key]; + } + + return [[FSTMutationBatchResult alloc] initWithBatch:batch + commitVersion:commitVersion + mutationResults:mutationResults + streamToken:streamToken + docVersions:docVersions]; +} + +@end +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTPath.h b/Firestore/Source/Model/FSTPath.h new file mode 100644 index 0000000..1f63f17 --- /dev/null +++ b/Firestore/Source/Model/FSTPath.h @@ -0,0 +1,141 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTPath represents a path sequence in the Firestore database. It is composed of an ordered + * sequence of string segments. + * + * ## Subclassing Notes + * + * FSTPath itself is an abstract class that must be specialized by subclasses. Subclasses should + * implement constructors for common string-based representations of the path and also override + * -canonicalString which converts back to the canonical string-based representation of the path. + */ +@interface FSTPath <SelfType> : NSObject + +/** Returns the path segment of the given index. */ +- (NSString *)segmentAtIndex:(int)index; +- (id)objectAtIndexedSubscript:(int)index; + +- (BOOL)isEqual:(id)path; +- (NSComparisonResult)compare:(SelfType)other; + +/** + * Returns a new path whose segments are the current path's plus one more. + * + * @param segment The new segment to concatenate to the path. + * @return A new path with this path's segment plus the new one. + */ +- (instancetype)pathByAppendingSegment:(NSString *)segment; + +/** + * Returns a new path whose segments are the current path's plus another's. + * + * @param path The new path whose segments should be concatenated to the path. + * @return A new path with this path's segment plus the new ones. + */ +- (instancetype)pathByAppendingPath:(SelfType)path; + +/** Returns a new path whose segments are the same as this one's minus the first one. */ +- (instancetype)pathByRemovingFirstSegment; + +/** Returns a new path whose segments are the same as this one's minus the first `count`. */ +- (instancetype)pathByRemovingFirstSegments:(int)count; + +/** Returns a new path whose segments are the same as this one's minus the last one. */ +- (instancetype)pathByRemovingLastSegment; + +/** Convenience method for getting the first segment of this path. */ +- (NSString *)firstSegment; + +/** Convenience method for getting the last segment of this path. */ +- (NSString *)lastSegment; + +/** Returns true if this path is a prefix of the given path. */ +- (BOOL)isPrefixOfPath:(SelfType)other; + +/** Returns a standardized string representation of this path. */ +- (NSString *)canonicalString; + +/** The number of segments in the path. */ +@property(nonatomic, readonly) int length; + +/** True if the path is empty. */ +@property(nonatomic, readonly, getter=isEmpty) BOOL empty; + +@end + +/** A dot-separated path for navigating sub-objects within a document. */ +@class FSTFieldPath; + +@interface FSTFieldPath : FSTPath <FSTFieldPath *> + +/** + * Creates and returns a new path with the given segments. The array of segments is not copied, so + * one should not mutate the array once it is passed in here. + * + * @param segments The underlying array of segments for the path. + * @return A new instance of FSTPath. + */ ++ (instancetype)pathWithSegments:(NSArray<NSString *> *)segments; + +/** + * Creates and returns a new path from the server formatted field-path string, where path segments + * are separated by a dot "." and optionally encoded using backticks. + * + * @param fieldPath A dot-separated string representing the path. + */ ++ (instancetype)pathWithServerFormat:(NSString *)fieldPath; + +/** Returns a field path that represents a document key. */ ++ (instancetype)keyFieldPath; + +/** Returns a field path that represents an empty path. */ ++ (instancetype)emptyPath; + +/** Returns YES if this is the `FSTFieldPath.keyFieldPath` field path. */ +- (BOOL)isKeyFieldPath; + +@end + +/** A slash-separated path for navigating resources (documents and collections) within Firestore. */ +@class FSTResourcePath; + +@interface FSTResourcePath : FSTPath <FSTResourcePath *> + +/** + * Creates and returns a new path with the given segments. The array of segments is not copied, so + * one should not mutate the array once it is passed in here. + * + * @param segments The underlying array of segments for the path. + * @return A new instance of FSTPath. + */ ++ (instancetype)pathWithSegments:(NSArray<NSString *> *)segments; + +/** + * Creates and returns a new path from the given resource-path string, where the path segments are + * separated by a slash "/". + * + * @param resourcePath A slash-separated string representing the path. + */ ++ (instancetype)pathWithString:(NSString *)resourcePath; +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Model/FSTPath.m b/Firestore/Source/Model/FSTPath.m new file mode 100644 index 0000000..0588612 --- /dev/null +++ b/Firestore/Source/Model/FSTPath.m @@ -0,0 +1,356 @@ +/* + * 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 "FSTPath.h" + +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTDocumentKey.h" +#import "FSTUsageValidation.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTPath () +/** An underlying array of which a subset of elements are the segments of the path. */ +@property(strong, nonatomic) NSArray<NSString *> *segments; +/** The index into the segments array of the first segment in this path. */ +@property int offset; +@end + +@implementation FSTPath + +/** + * Designated initializer. + * + * @param segments The underlying array of segments for the path. + * @param offset The starting index in the underlying array for the subarray to use. + * @param length The length of the subarray to use. + */ +- (instancetype)initWithSegments:(NSArray<NSString *> *)segments + offset:(int)offset + length:(int)length { + FSTAssert(offset <= segments.count, @"offset %d out of range %d", offset, (int)segments.count); + FSTAssert(length <= segments.count - offset, @"offset %d out of range %d", offset, + (int)segments.count - offset); + + if (self = [super init]) { + _segments = segments; + _offset = offset; + _length = length; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + if (![object isKindOfClass:[FSTPath class]]) { + return NO; + } + FSTPath *path = object; + return [self compare:path] == NSOrderedSame; +} + +- (NSUInteger)hash { + NSUInteger hash = 0; + for (int i = 0; i < self.length; ++i) { + hash += [self segmentAtIndex:i].hash; + } + return hash; +} + +- (NSString *)description { + return [self canonicalString]; +} + +- (id)objectAtIndexedSubscript:(int)index { + return [self segmentAtIndex:index]; +} + +- (NSString *)segmentAtIndex:(int)index { + FSTAssert(index < self.length, @"index %d out of range", index); + return self.segments[self.offset + index]; +} + +- (NSString *)firstSegment { + FSTAssert(!self.isEmpty, @"Cannot call firstSegment on empty path"); + return [self segmentAtIndex:0]; +} + +- (NSString *)lastSegment { + FSTAssert(!self.isEmpty, @"Cannot call lastSegment on empty path"); + return [self segmentAtIndex:self.length - 1]; +} + +- (NSComparisonResult)compare:(FSTPath *)other { + int length = MIN(self.length, other.length); + for (int i = 0; i < length; ++i) { + NSString *left = [self segmentAtIndex:i]; + NSString *right = [other segmentAtIndex:i]; + NSComparisonResult result = [left compare:right]; + if (result != NSOrderedSame) { + return result; + } + } + if (self.length < other.length) { + return NSOrderedAscending; + } + if (self.length > other.length) { + return NSOrderedDescending; + } + return NSOrderedSame; +} + +- (instancetype)pathWithSegments:(NSArray<NSString *> *)segments + offset:(int)offset + length:(int)length { + return [[[self class] alloc] initWithSegments:segments offset:offset length:length]; +} + +- (instancetype)pathByAppendingSegment:(NSString *)segment { + int newLength = self.length + 1; + NSMutableArray<NSString *> *segments = [NSMutableArray arrayWithCapacity:newLength]; + for (int i = 0; i < self.length; ++i) { + [segments addObject:self[i]]; + } + [segments addObject:segment]; + return [self pathWithSegments:segments offset:0 length:newLength]; +} + +- (instancetype)pathByAppendingPath:(FSTPath *)path { + int newLength = self.length + path.length; + NSMutableArray<NSString *> *segments = [NSMutableArray arrayWithCapacity:newLength]; + for (int i = 0; i < self.length; ++i) { + [segments addObject:self[i]]; + } + for (int i = 0; i < path.length; ++i) { + [segments addObject:path[i]]; + } + return [self pathWithSegments:segments offset:0 length:newLength]; +} + +- (BOOL)isEmpty { + return self.length == 0; +} + +- (instancetype)pathByRemovingFirstSegment { + FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingFirstSegment on empty path"); + return [self pathWithSegments:self.segments offset:self.offset + 1 length:self.length - 1]; +} + +- (instancetype)pathByRemovingFirstSegments:(int)count { + FSTAssert(self.length >= count, @"pathByRemovingFirstSegments:%d on path of length %d", count, + self.length); + return + [self pathWithSegments:self.segments offset:self.offset + count length:self.length - count]; +} + +- (instancetype)pathByRemovingLastSegment { + FSTAssert(!self.isEmpty, @"Cannot call pathByRemovingLastSegment on empty path"); + return [self pathWithSegments:self.segments offset:self.offset length:self.length - 1]; +} + +- (BOOL)isPrefixOfPath:(FSTPath *)other { + if (other.length < self.length) { + return NO; + } + for (int i = 0; i < self.length; ++i) { + if (![self[i] isEqual:other[i]]) { + return NO; + } + } + return YES; +} + +/** Returns a standardized string representation of this path. */ +- (NSString *)canonicalString { + @throw FSTAbstractMethodException(); // NOLINT +} +@end + +@implementation FSTFieldPath ++ (instancetype)pathWithSegments:(NSArray<NSString *> *)segments { + return [[FSTFieldPath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; +} + ++ (instancetype)pathWithServerFormat:(NSString *)fieldPath { + NSMutableArray<NSString *> *segments = [NSMutableArray array]; + + // TODO(b/37244157): Once we move to v1beta1, we should make this more strict. Right now, it + // allows non-identifier path components, even if they aren't escaped. Technically, this will + // mangle paths with backticks in them used in v1alpha1, but that's fine. + + const char *source = [fieldPath UTF8String]; + char *segment = (char *)malloc(strlen(source) + 1); + char *segmentEnd = segment; + + // If we're inside '`' backticks, then we should ignore '.' dots. + BOOL inBackticks = NO; + + char c; + do { + // Examine current character. This is legit even on zero-length strings because there's always + // a null terminator. + c = *source++; + switch (c) { + case '\0': // Falls through + case '.': + if (!inBackticks) { + // Segment is complete + *segmentEnd = '\0'; + if (segment == segmentEnd) { + FSTThrowInvalidArgument( + @"Invalid field path (%@). Paths must not be empty, begin with " + @"'.', end with '.', or contain '..'", + fieldPath); + } + + [segments addObject:[NSString stringWithUTF8String:segment]]; + segmentEnd = segment; + } else { + // copy into the current segment + *segmentEnd++ = c; + } + break; + + case '`': + if (inBackticks) { + inBackticks = NO; + } else { + inBackticks = YES; + } + break; + + case '\\': + // advance to escaped character + 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 + + default: + // copy into the current segment + *segmentEnd++ = c; + break; + } + } while (c); + + FSTAssert(!inBackticks, @"Unterminated ` in path %@", fieldPath); + + free(segment); + return [FSTFieldPath pathWithSegments:segments]; +} + ++ (instancetype)keyFieldPath { + static FSTFieldPath *keyFieldPath; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + keyFieldPath = [FSTFieldPath pathWithSegments:@[ kDocumentKeyPath ]]; + }); + return keyFieldPath; +} + ++ (instancetype)emptyPath { + static FSTFieldPath *emptyPath; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + emptyPath = [FSTFieldPath pathWithSegments:@[]]; + }); + return emptyPath; +} + +/** Return YES if the string could be used as a segment in a field path without escaping. */ ++ (BOOL)isValidIdentifier:(NSString *)segment { + if (segment.length == 0) { + return NO; + } + unichar first = [segment characterAtIndex:0]; + if (first != '_' && (first < 'a' || first > 'z') && (first < 'A' || first > 'Z')) { + return NO; + } + for (int i = 1; i < segment.length; i++) { + unichar c = [segment characterAtIndex:i]; + if (c != '_' && (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9')) { + return NO; + } + } + return YES; +} + +- (BOOL)isKeyFieldPath { + return [self isEqual:FSTFieldPath.keyFieldPath]; +} + +- (NSString *)canonicalString { + NSMutableString *result = [NSMutableString string]; + for (int i = 0; i < self.length; i++) { + if (i > 0) { + [result appendString:@"."]; + } + + NSString *escaped = [self[i] stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"]; + escaped = [escaped stringByReplacingOccurrencesOfString:@"`" withString:@"\\`"]; + if (![FSTFieldPath isValidIdentifier:escaped]) { + escaped = [NSString stringWithFormat:@"`%@`", escaped]; + } + + [result appendString:escaped]; + } + return result; +} + +@end + +@implementation FSTResourcePath ++ (instancetype)pathWithSegments:(NSArray<NSString *> *)segments { + return [[FSTResourcePath alloc] initWithSegments:segments offset:0 length:(int)segments.count]; +} + ++ (instancetype)pathWithString:(NSString *)resourcePath { + // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) + // and just passes them through raw (they exist for legacy reasons and should not be used + // frequently). + + if ([resourcePath rangeOfString:@"//"].location != NSNotFound) { + FSTThrowInvalidArgument(@"Invalid path (%@). Paths must not contain // in them.", resourcePath); + } + + NSMutableArray *segments = [[resourcePath componentsSeparatedByString:@"/"] mutableCopy]; + // We may still have an empty segment at the beginning or end if they had a leading or trailing + // slash (which we allow). + [segments removeObject:@""]; + + return [self pathWithSegments:segments]; +} + +- (NSString *)canonicalString { + // NOTE: The client is ignorant of any path segments containing escape sequences (e.g. __id123__) + // and just passes them through raw (they exist for legacy reasons and should not be used + // frequently). + + NSMutableString *result = [NSMutableString string]; + for (int i = 0; i < self.length; i++) { + if (i > 0) { + [result appendString:@"/"]; + } + [result appendString:self[i]]; + } + return result; +} +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRCollectionReference.h b/Firestore/Source/Public/FIRCollectionReference.h new file mode 100644 index 0000000..11cb969 --- /dev/null +++ b/Firestore/Source/Public/FIRCollectionReference.h @@ -0,0 +1,99 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" +#import "FIRQuery.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentReference; + +/** + * A `FIRCollectionReference` object can be used for adding documents, getting document references, + * and querying for documents (using the methods inherited from `FIRQuery`). + */ +FIR_SWIFT_NAME(CollectionReference) +@interface FIRCollectionReference : FIRQuery + +/** */ +- (id)init __attribute__((unavailable("FIRCollectionReference cannot be created directly."))); + +/** ID of the referenced collection. */ +@property(nonatomic, strong, readonly) NSString *collectionID; + +/** + * For subcollections, `parent` returns the containing `FIRDocumentReference`. For root + * collections, nil is returned. + */ +@property(nonatomic, strong, nullable, readonly) FIRDocumentReference *parent; + +/** + * A string containing the slash-separated path to this this `FIRCollectionReference` (relative to + * the root of the database). + */ +@property(nonatomic, strong, readonly) NSString *path; + +/** + * Returns a FIRDocumentReference pointing to a new document with an auto-generated ID. + * + * @return A FIRDocumentReference pointing to a new document with an auto-generated ID. + */ +- (FIRDocumentReference *)documentWithAutoID FIR_SWIFT_NAME(document()); + +/** + * Gets a `FIRDocumentReference` referring to the document at the specified path, relative to this + * collection's own path. + * + * @param documentPath The slash-separated relative path of the document for which to get a + * `FIRDocumentReference`. + * + * @return The `FIRDocumentReference` for the specified document path. + */ +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath FIR_SWIFT_NAME(document(_:)); + +/** + * Add a new document to this collection with the specified data, assigning it a document ID + * automatically. + * + * @param data An `NSDictionary` containing the data for the new document. + * + * @return A `FIRDocumentReference` pointing to the newly created document. + */ +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data + FIR_SWIFT_NAME(addDocument(data:)); + +/** + * Add a new document to this collection with the specified data, assigning it a document ID + * automatically. + * + * @param data An `NSDictionary` containing the data for the new document. + * @param completion A block to execute once the document has been successfully written. + * + * @return A `FIRDocumentReference` pointing to the newly created document. + */ +// clang-format off +// clang-format breaks the FIR_SWIFT_NAME attribute +- (FIRDocumentReference *)addDocumentWithData:(NSDictionary<NSString *, id> *)data + completion: + (nullable void (^)(NSError *_Nullable error))completion + FIR_SWIFT_NAME(addDocument(data:completion:)); +// clang-format on + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRDocumentChange.h b/Firestore/Source/Public/FIRDocumentChange.h new file mode 100644 index 0000000..674e3b2 --- /dev/null +++ b/Firestore/Source/Public/FIRDocumentChange.h @@ -0,0 +1,70 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentSnapshot; + +/** An enumeration of document change types. */ +typedef NS_ENUM(NSInteger, FIRDocumentChangeType) { + /** Indicates a new document was added to the set of documents matching the query. */ + FIRDocumentChangeTypeAdded, + /** Indicates a document within the query was modified. */ + FIRDocumentChangeTypeModified, + /** + * Indicates a document within the query was removed (either deleted or no longer matches + * the query. + */ + FIRDocumentChangeTypeRemoved +} FIR_SWIFT_NAME(DocumentChangeType); + +/** + * A `FIRDocumentChange` represents a change to the documents matching a query. It contains the + * document affected and the type of change that occurred (added, modified, or removed). + */ +FIR_SWIFT_NAME(DocumentChange) +@interface FIRDocumentChange : NSObject + +/** */ +- (id)init __attribute__((unavailable("FIRDocumentChange cannot be created directly."))); + +/** The type of change that occurred (added, modified, or removed). */ +@property(nonatomic, readonly) FIRDocumentChangeType type; + +/** The document affected by this change. */ +@property(nonatomic, strong, readonly) FIRDocumentSnapshot *document; + +/** + * The index of the changed document in the result set immediately prior to this FIRDocumentChange + * (i.e. supposing that all prior FIRDocumentChange objects have been applied). NSNotFound for + * FIRDocumentChangeTypeAdded events. + */ +@property(nonatomic, readonly) NSUInteger oldIndex; + +/** + * The index of the changed document in the result set immediately after this FIRDocumentChange + * (i.e. supposing that all prior FIRDocumentChange objects and the current FIRDocumentChange object + * have been applied). NSNotFound for FIRDocumentChangeTypeRemoved events. + */ +@property(nonatomic, readonly) NSUInteger newIndex; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRDocumentReference.h b/Firestore/Source/Public/FIRDocumentReference.h new file mode 100644 index 0000000..03340c1 --- /dev/null +++ b/Firestore/Source/Public/FIRDocumentReference.h @@ -0,0 +1,219 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" +#import "FIRListenerRegistration.h" + +@class FIRFirestore; +@class FIRCollectionReference; +@class FIRDocumentSnapshot; +@class FIRSetOptions; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Options for use with `[FIRDocumentReference addSnapshotListener]` to control the behavior of the + * snapshot listener. + */ +FIR_SWIFT_NAME(DocumentListenOptions) +@interface FIRDocumentListenOptions : NSObject + ++ (instancetype)options NS_SWIFT_UNAVAILABLE("Use initializer"); + +- (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. + * + * @param includeMetadataChanges Whether to raise events for metadata-only changes. + * @return The receiver is returned for optional method chaining. + */ +- (instancetype)includeMetadataChanges:(BOOL)includeMetadataChanges + FIR_SWIFT_NAME(includeMetadataChanges(_:)); + +@end + +typedef void (^FIRDocumentSnapshotBlock)(FIRDocumentSnapshot *_Nullable snapshot, + NSError *_Nullable error); + +/** + * A `FIRDocumentReference` refers to a document location in a Firestore database and can be + * used to write, read, or listen to the location. The document at the referenced location + * may or may not exist. A `FIRDocumentReference` can also be used to create a + * `FIRCollectionReference` to a subcollection. + */ +FIR_SWIFT_NAME(DocumentReference) +@interface FIRDocumentReference : NSObject + +/** */ +- (instancetype)init + __attribute__((unavailable("FIRDocumentReference cannot be created directly."))); + +/** The ID of the document referred to. */ +@property(nonatomic, strong, readonly) NSString *documentID; + +/** A reference to the collection to which this `DocumentReference` belongs. */ +@property(nonatomic, strong, readonly) FIRCollectionReference *parent; + +/** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */ +@property(nonatomic, strong, readonly) FIRFirestore *firestore; + +/** + * A string representing the path of the referenced document (relative to the root of the + * database). + */ +@property(nonatomic, strong, readonly) NSString *path; + +/** + * Gets a `FIRCollectionReference` referring to the collection at the specified + * path, relative to this document. + * + * @param collectionPath The slash-separated relative path of the collection for which to get a + * `FIRCollectionReference`. + * + * @return The `FIRCollectionReference` at the specified _collectionPath_. + */ +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath + FIR_SWIFT_NAME(collection(_:)); + +#pragma mark - Writing Data + +/** + * Writes to the document referred to by `FIRDocumentReference`. If the document doesn't yet exist, + * this method creates it and then sets the data. If the document exists, this method overwrites + * the document data with the new values. + * + * @param documentData An `NSDictionary` that contains the fields and data to write to the + * document. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData; + +/** + * Writes to the document referred to by this DocumentReference. If the document does not yet + * exist, it will be created. If you pass `FIRSetOptions`, the provided data will be merged into + * an existing document. + * + * @param documentData An `NSDictionary` that contains the fields and data to write to the + * document. + * @param options A `FIRSetOptions` used to configure the set behavior. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData options:(FIRSetOptions *)options; + +/** + * Overwrites the document referred to by this `FIRDocumentReference`. If no document exists, it + * is created. If a document already exists, it is overwritten. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param completion A block to execute once the document has been successfully written. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData + completion:(nullable void (^)(NSError *_Nullable error))completion; + +/** + * Writes to the document referred to by this DocumentReference. If the document does not yet + * exist, it will be created. If you pass `FIRSetOptions`, the provided data will be merged into + * an existing document. + * + * @param documentData An `NSDictionary` containing the fields that make up the document + * to be written. + * @param options A `FIRSetOptions` used to configure the set behavior. + * @param completion A block to execute once the document has been successfully written. + */ +- (void)setData:(NSDictionary<NSString *, id> *)documentData + options:(FIRSetOptions *)options + completion:(nullable void (^)(NSError *_Nullable error))completion; + +/** + * Updates fields in the document referred to by this `FIRDocumentReference`. + * If the document does not exist, the update fails (specify a completion block to be notified). + * + * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or + * `FIRFieldPath`) and values with which to update the document. + */ +- (void)updateData:(NSDictionary<id, id> *)fields; + +/** + * Updates fields in the document referred to by this `FIRDocumentReference`. If the document + * does not exist, the update fails and the specified completion block receives an error. + * + * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or + * `FIRFieldPath`) and values with which to update the document. + * @param completion A block to execute when the update is complete. If the update is successful the + * error parameter will be nil, otherwise it will give an indication of how the update failed. + */ +- (void)updateData:(NSDictionary<id, id> *)fields + completion:(nullable void (^)(NSError *_Nullable error))completion; + +// NOTE: this is named 'deleteDocument' because 'delete' is a keyword in Objective-C++. +/** Deletes the document referred to by this `FIRDocumentReference`. */ +// clang-format off +- (void)deleteDocument FIR_SWIFT_NAME(delete()); +// clang-format on + +/** + * Deletes the document referred to by this `FIRDocumentReference`. + * + * @param completion A block to execute once the document has been successfully deleted. + */ +// clang-format off +- (void)deleteDocumentWithCompletion:(nullable void (^)(NSError *_Nullable error))completion + FIR_SWIFT_NAME(delete(completion:)); +// clang-format on + +#pragma mark - Retrieving Data + +/** + * Reads the document referenced by this `FIRDocumentReference`. + * + * @param completion a block to execute once the document has been successfully read. + */ +- (void)getDocumentWithCompletion:(FIRDocumentSnapshotBlock)completion + FIR_SWIFT_NAME(getDocument(completion:)); + +/** + * Attaches a listener for DocumentSnapshot events. + * + * @param listener The listener to attach. + * + * @return A FIRListenerRegistration that can be used to remove this listener. + */ +- (id<FIRListenerRegistration>)addSnapshotListener:(FIRDocumentSnapshotBlock)listener + FIR_SWIFT_NAME(addSnapshotListener(_:)); + +/** + * Attaches a listener for DocumentSnapshot events. + * + * @param options Options controlling the listener behavior. + * @param listener The listener to attach. + * + * @return A FIRListenerRegistration that can be used to remove this listener. + */ +// clang-format off +- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions: + (nullable FIRDocumentListenOptions *)options + listener:(FIRDocumentSnapshotBlock)listener + FIR_SWIFT_NAME(addSnapshotListener(options:listener:)); +// clang-format on + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRDocumentSnapshot.h b/Firestore/Source/Public/FIRDocumentSnapshot.h new file mode 100644 index 0000000..e923e3e --- /dev/null +++ b/Firestore/Source/Public/FIRDocumentSnapshot.h @@ -0,0 +1,68 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +@class FIRDocumentReference; +@class FIRSnapshotMetadata; + +NS_ASSUME_NONNULL_BEGIN + +/** + * 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. + */ +FIR_SWIFT_NAME(DocumentSnapshot) +@interface FIRDocumentSnapshot : NSObject + +/** */ +- (instancetype)init + __attribute__((unavailable("FIRDocumentSnapshot cannot be created directly."))); + +/** True if the document exists. */ +@property(nonatomic, assign, readonly) BOOL exists; + +/** A `FIRDocumentReference` to the document location. */ +@property(nonatomic, strong, readonly) FIRDocumentReference *reference; + +/** The ID of the document for which this `FIRDocumentSnapshot` contains data. */ +@property(nonatomic, copy, readonly) NSString *documentID; + +/** Metadata about this snapshot concerning its source and if it has local modifications. */ +@property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata; + +/** + * Retrieves all fields in the document as an `NSDictionary`. + * + * @return An `NSDictionary` containing all fields in the document. + */ +- (NSDictionary<NSString *, id> *)data; + +/** + * 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. + */ +- (nullable id)objectForKeyedSubscript:(id)key; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFieldPath.h b/Firestore/Source/Public/FIRFieldPath.h new file mode 100644 index 0000000..b80eda7 --- /dev/null +++ b/Firestore/Source/Public/FIRFieldPath.h @@ -0,0 +1,50 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * A `FieldPath` refers to a field in a document. The path may consist of a single field name + * (referring to a top level field in the document), or a list of field names (referring to a nested + * field in the document). + */ +FIR_SWIFT_NAME(FieldPath) +@interface FIRFieldPath : NSObject <NSCopying> + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a `FieldPath` from the provided field names. If more than one field name is provided, the + * path will point to a nested field in a document. + * + * @param fieldNames A list of field names. + * @return A `FieldPath` that points to a field location in a document. + */ +- (instancetype)initWithFields:(NSArray<NSString *> *)fieldNames FIR_SWIFT_NAME(init(_:)); + +/** + * A special sentinel `FieldPath` to refer to the ID of a document. It can be used in queries to + * sort or filter by the document ID. + */ ++ (instancetype)documentID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFieldValue.h b/Firestore/Source/Public/FIRFieldValue.h new file mode 100644 index 0000000..f7d19f0 --- /dev/null +++ b/Firestore/Source/Public/FIRFieldValue.h @@ -0,0 +1,45 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * Sentinel values that can be used when writing document fields with setData() or updateData(). + */ +FIR_SWIFT_NAME(FieldValue) +@interface FIRFieldValue : NSObject + +/** */ +- (instancetype)init NS_UNAVAILABLE; + +/** Used with updateData() to mark a field for deletion. */ +// clang-format off ++ (instancetype)fieldValueForDelete FIR_SWIFT_NAME(delete()); +// clang-format on + +/** + * Used with setData() or updateData() to include a server-generated timestamp in the written + * data. + */ ++ (instancetype)fieldValueForServerTimestamp FIR_SWIFT_NAME(serverTimestamp()); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestore.h b/Firestore/Source/Public/FIRFirestore.h new file mode 100644 index 0000000..c31fef6 --- /dev/null +++ b/Firestore/Source/Public/FIRFirestore.h @@ -0,0 +1,145 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +@class FIRApp; +@class FIRCollectionReference; +@class FIRDocumentReference; +@class FIRFirestoreSettings; +@class FIRTransaction; +@class FIRWriteBatch; + +NS_ASSUME_NONNULL_BEGIN + +/** + * `FIRFirestore` represents a Firestore Database and is the entry point for all Firestore + * operations. + */ +FIR_SWIFT_NAME(Firestore) +@interface FIRFirestore : NSObject + +#pragma mark - Initializing +/** */ +- (instancetype)init __attribute__((unavailable("Use a static constructor method."))); + +/** + * Creates, caches, and returns a `FIRFirestore` using the default `FIRApp`. Each subsequent + * invocation returns the same `FIRFirestore` object. + * + * @return The `FIRFirestore` instance. + */ ++ (instancetype)firestore FIR_SWIFT_NAME(firestore()); + +/** + * Creates, caches, and returns a `FIRFirestore` object for the specified _app_. Each subsequent + * invocation returns the same `FIRFirestore` object. + * + * @param app The `FIRApp` instance to use for authentication and as a source of the Google Cloud + * Project ID for your Firestore Database. If you want the default instance, you should explicitly + * set it to `[FIRApp defaultApp]`. + * + * @return The `FIRFirestore` instance. + */ ++ (instancetype)firestoreForApp:(FIRApp *)app FIR_SWIFT_NAME(firestore(app:)); + +/** + * Custom settings used to configure this `FIRFirestore` object. + */ +@property(nonatomic, copy) FIRFirestoreSettings *settings; + +/** + * The Firebase App associated with this Firestore instance. + */ +@property(strong, nonatomic, readonly) FIRApp *app; + +#pragma mark - Collections and Documents + +/** + * Gets a `FIRCollectionReference` referring to the collection at the specified path within the + * database. + * + * @param collectionPath The slash-separated path of the collection for which to get a + * `FIRCollectionReference`. + * + * @return The `FIRCollectionReference` at the specified _collectionPath_. + */ +- (FIRCollectionReference *)collectionWithPath:(NSString *)collectionPath + FIR_SWIFT_NAME(collection(_:)); + +/** + * Gets a `FIRDocumentReference` referring to the document at the specified path within the + * database. + * + * @param documentPath The slash-separated path of the document for which to get a + * `FIRDocumentReference`. + * + * @return The `FIRDocumentReference` for the specified _documentPath_. + */ +- (FIRDocumentReference *)documentWithPath:(NSString *)documentPath FIR_SWIFT_NAME(document(_:)); + +#pragma mark - Transactions and Write Batches + +/** + * Executes the given updateBlock and then attempts to commit the changes applied within an atomic + * transaction. + * + * In the updateBlock, a set of reads and writes can be performed atomically using the + * `FIRTransaction` object passed to the block. After the updateBlock is run, Firestore will attempt + * to apply the changes to the server. If any of the data read has been modified outside of this + * transaction since being read, then the transaction will be retried by executing the updateBlock + * again. If the transaction still fails after 5 retries, then the transaction will fail. + * + * Since the updateBlock may be executed multiple times, it should avoiding doing anything that + * would cause side effects. + * + * Any value maybe be returned from the updateBlock. If the transaction is successfully committed, + * then the completion block will be passed that value. The updateBlock also has an `NSError` out + * parameter. If this is set, then the transaction will not attempt to commit, and the given error + * will be passed to the completion block. + * + * The `FIRTransaction` object passed to the updateBlock contains methods for accessing documents + * and collections. Unlike other firestore access, data accessed with the transaction will not + * reflect local changes that have not been committed. For this reason, it is required that all + * reads are performed before any writes. Transactions must be performed while online. Otherwise, + * reads will fail, and the final commit will fail. + * + * @param updateBlock The block to execute within the transaction context. + * @param completion The block to call with the result or error of the transaction. + */ +- (void)runTransactionWithBlock:(id _Nullable (^)(FIRTransaction *, NSError **))updateBlock + completion:(void (^)(id _Nullable result, NSError *_Nullable error))completion; + +/** + * Creates a write batch, used for performing multiple writes as a single + * atomic operation. + * + * Unlike transactions, write batches are persisted offline and therefore are preferable when you + * don't need to condition your writes on read data. + */ +- (FIRWriteBatch *)batch; + +#pragma mark - Logging + +/** Enables or disables logging from the Firestore client. */ ++ (void)enableLogging:(BOOL)logging + DEPRECATED_MSG_ATTRIBUTE("Use FIRSetLoggerLevel(FIRLoggerLevelDebug) to enable logging"); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestoreErrors.h b/Firestore/Source/Public/FIRFirestoreErrors.h new file mode 100644 index 0000000..f2e19d9 --- /dev/null +++ b/Firestore/Source/Public/FIRFirestoreErrors.h @@ -0,0 +1,105 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** The Cloud Firestore error domain. */ +FOUNDATION_EXPORT NSString *const FIRFirestoreErrorDomain FIR_SWIFT_NAME(FirestoreErrorDomain); + +/** Error codes used by Cloud Firestore. */ +typedef NS_ENUM(NSInteger, FIRFirestoreErrorCode) { + /** + * The operation completed successfully. NSError objects will never have a code with this value. + */ + FIRFirestoreErrorCodeOK = 0, + + /** The operation was cancelled (typically by the caller). */ + FIRFirestoreErrorCodeCancelled = 1, + + /** Unknown error or an error from a different error domain. */ + FIRFirestoreErrorCodeUnknown = 2, + + /** + * Client specified an invalid argument. Note that this differs from FailedPrecondition. + * InvalidArgument indicates arguments that are problematic regardless of the state of the + * system (e.g., an invalid field name). + */ + FIRFirestoreErrorCodeInvalidArgument = 3, + + /** + * Deadline expired before operation could complete. For operations that change the state of the + * system, this error may be returned even if the operation has completed successfully. For + * example, a successful response from a server could have been delayed long enough for the + * deadline to expire. + */ + FIRFirestoreErrorCodeDeadlineExceeded = 4, + + /** Some requested document was not found. */ + FIRFirestoreErrorCodeNotFound = 5, + + /** Some document that we attempted to create already exists. */ + FIRFirestoreErrorCodeAlreadyExists = 6, + + /** The caller does not have permission to execute the specified operation. */ + FIRFirestoreErrorCodePermissionDenied = 7, + + /** + * Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system + * is out of space. + */ + FIRFirestoreErrorCodeResourceExhausted = 8, + + /** + * Operation was rejected because the system is not in a state required for the operation's + * execution. + */ + FIRFirestoreErrorCodeFailedPrecondition = 9, + + /** + * The operation was aborted, typically due to a concurrency issue like transaction aborts, etc. + */ + FIRFirestoreErrorCodeAborted = 10, + + /** Operation was attempted past the valid range. */ + FIRFirestoreErrorCodeOutOfRange = 11, + + /** Operation is not implemented or not supported/enabled. */ + FIRFirestoreErrorCodeUnimplemented = 12, + + /** + * Internal errors. Means some invariants expected by underlying system has been broken. If you + * see one of these errors, something is very broken. + */ + FIRFirestoreErrorCodeInternal = 13, + + /** + * The service is currently unavailable. This is a most likely a transient condition and may be + * corrected by retrying with a backoff. + */ + FIRFirestoreErrorCodeUnavailable = 14, + + /** Unrecoverable data loss or corruption. */ + FIRFirestoreErrorCodeDataLoss = 15, + + /** The request does not have valid authentication credentials for the operation. */ + FIRFirestoreErrorCodeUnauthenticated = 16 +} FIR_SWIFT_NAME(FirestoreErrorCode); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestoreSettings.h b/Firestore/Source/Public/FIRFirestoreSettings.h new file mode 100644 index 0000000..7097e60 --- /dev/null +++ b/Firestore/Source/Public/FIRFirestoreSettings.h @@ -0,0 +1,51 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** Settings used to configure a `FIRFirestore` instance. */ +FIR_SWIFT_NAME(FirestoreSettings) +@interface FIRFirestoreSettings : NSObject <NSCopying> + +/** + * Creates and returns an empty `FIRFirestoreSettings` object. + * + * @return The created `FIRFirestoreSettings` object. + */ +- (instancetype)init NS_DESIGNATED_INITIALIZER; + +/** The hostname to connect to. */ +@property(nonatomic, copy) NSString *host; + +/** Whether to use SSL when connecting. */ +@property(nonatomic, getter=isSSLEnabled) BOOL sslEnabled; + +/** + * A dispatch queue to be used to execute all completion handlers and event handlers. By default, + * the main queue is used. + */ +@property(nonatomic, strong) dispatch_queue_t dispatchQueue; + +/** Set to false to disable local persistent storage. */ +@property(nonatomic, getter=isPersistenceEnabled) BOOL persistenceEnabled; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h b/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h new file mode 100644 index 0000000..216c047 --- /dev/null +++ b/Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h @@ -0,0 +1,29 @@ +/* + * 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. + */ + +#ifndef FIR_SWIFT_NAME + +#import <Foundation/Foundation.h> + +// NS_SWIFT_NAME can only translate factory methods before the iOS 9.3 SDK. +// Wrap it in our own macro if it's a non-compatible SDK. +#ifdef __IPHONE_9_3 +#define FIR_SWIFT_NAME(X) NS_SWIFT_NAME(X) +#else +#define FIR_SWIFT_NAME(X) // Intentionally blank. +#endif // #ifdef __IPHONE_9_3 + +#endif // FIR_SWIFT_NAME diff --git a/Firestore/Source/Public/FIRGeoPoint.h b/Firestore/Source/Public/FIRGeoPoint.h new file mode 100644 index 0000000..de409b5 --- /dev/null +++ b/Firestore/Source/Public/FIRGeoPoint.h @@ -0,0 +1,49 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * An immutable object representing a geographical point in Firestore. The point is represented as + * a latitude/longitude pair. + * + * Latitude values are in the range of [-90, 90]. + * Longitude values are in the range of [-180, 180]. + */ +FIR_SWIFT_NAME(GeoPoint) +@interface FIRGeoPoint : NSObject <NSCopying> + +/** */ +- (instancetype)init NS_UNAVAILABLE; + +/** + * Creates a `GeoPoint` from the provided latitude and longitude degrees. + * @param latitude The latitude as number between -90 and 90. + * @param longitude The longitude as number between -180 and 180. + */ +- (instancetype)initWithLatitude:(double)latitude + longitude:(double)longitude NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, readonly) double latitude; +@property(nonatomic, readonly) double longitude; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRListenerRegistration.h b/Firestore/Source/Public/FIRListenerRegistration.h new file mode 100644 index 0000000..5fe7fd5 --- /dev/null +++ b/Firestore/Source/Public/FIRListenerRegistration.h @@ -0,0 +1,32 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** Represents a listener that can be removed by calling remove. */ +@protocol FIRListenerRegistration <NSObject> + +/** + * Removes the listener being tracked by this FIRListenerRegistration. After the initial call, + * subsequent calls have no effect. + */ +- (void)remove; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRQuery.h b/Firestore/Source/Public/FIRQuery.h new file mode 100644 index 0000000..5c5546d --- /dev/null +++ b/Firestore/Source/Public/FIRQuery.h @@ -0,0 +1,414 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" +#import "FIRListenerRegistration.h" + +@class FIRFieldPath; +@class FIRFirestore; +@class FIRQuerySnapshot; +@class FIRDocumentSnapshot; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Options for use with `[FIRQuery addSnapshotListener]` to control the behavior of the snapshot + * listener. + */ +FIR_SWIFT_NAME(QueryListenOptions) +@interface FIRQueryListenOptions : NSObject + ++ (instancetype)options NS_SWIFT_UNAVAILABLE("Use initializer"); + +- (instancetype)init; + +@property(nonatomic, assign, readonly) BOOL includeQueryMetadataChanges; + +/** + * Sets the includeQueryMetadataChanges option which controls whether metadata-only changes on the + * query (i.e. only `FIRQuerySnapshot.metadata` changed) should trigger snapshot events. Default is + * NO. + * + * @param includeQueryMetadataChanges Whether to raise events for metadata-only changes on the + * query. + * @return The receiver is returned for optional method chaining. + */ +- (instancetype)includeQueryMetadataChanges:(BOOL)includeQueryMetadataChanges + FIR_SWIFT_NAME(includeQueryMetadataChanges(_:)); + +@property(nonatomic, assign, readonly) BOOL includeDocumentMetadataChanges; + +/** + * Sets the includeDocumentMetadataChanges option which controls whether document metadata-only + * changes (i.e. only `FIRDocumentSnapshot.metadata` on a document contained in the query + * changed) should trigger snapshot events. Default is NO. + * + * @param includeDocumentMetadataChanges Whether to raise events for document metadata-only changes. + * @return The receiver is returned for optional method chaining. + */ +- (instancetype)includeDocumentMetadataChanges:(BOOL)includeDocumentMetadataChanges + FIR_SWIFT_NAME(includeDocumentMetadataChanges(_:)); + +@end + +typedef void (^FIRQuerySnapshotBlock)(FIRQuerySnapshot *_Nullable snapshot, + NSError *_Nullable error); + +/** + * A `FIRQuery` refers to a Query which you can read or listen to. You can also construct + * refined `FIRQuery` objects by adding filters and ordering. + */ +FIR_SWIFT_NAME(Query) +@interface FIRQuery : NSObject +/** */ +- (id)init __attribute__((unavailable("FIRQuery cannot be created directly."))); + +/** The `FIRFirestore` for the Firestore database (useful for performing transactions, etc.). */ +@property(nonatomic, strong, readonly) FIRFirestore *firestore; + +#pragma mark - Retrieving Data +/** + * Reads the documents matching this query. + * + * @param completion a block to execute once the documents have been successfully read. + * documentSet will be `nil` only if error is `non-nil`. + */ +- (void)getDocumentsWithCompletion:(FIRQuerySnapshotBlock)completion + FIR_SWIFT_NAME(getDocuments(completion:)); + +/** + * Attaches a listener for QuerySnapshot events. + * + * @param listener The listener to attach. + * + * @return A FIRListenerRegistration that can be used to remove this listener. + */ +- (id<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener + FIR_SWIFT_NAME(addSnapshotListener(_:)); + +/** + * Attaches a listener for QuerySnapshot events. + * + * @param options Options controlling the listener behavior. + * @param listener The listener to attach. + * + * @return A FIRListenerRegistration that can be used to remove this listener. + */ +// clang-format off +- (id<FIRListenerRegistration>)addSnapshotListenerWithOptions: + (nullable FIRQueryListenOptions *)options + listener:(FIRQuerySnapshotBlock)listener + FIR_SWIFT_NAME(addSnapshotListener(options:listener:)); +// clang-format on + +#pragma mark - Filtering Data +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be equal to the specified value. + * + * @param field The name of the field to compare. + * @param value The value the field must be equal to. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be equal to the specified value. + * + * @param path The path of the field to compare. + * @param value The value the field must be equal to. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be less than the specified value. + * + * @param field The name of the field to compare. + * @param value The value the field must be less than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isLessThan:(id)value FIR_SWIFT_NAME(whereField(_:isLessThan:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be less than the specified value. + * + * @param path The path of the field to compare. + * @param value The value the field must be less than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isLessThan:(id)value FIR_SWIFT_NAME(whereField(_:isLessThan:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be less than or equal to the specified value. + * + * @param field The name of the field to compare + * @param value The value the field must be less than or equal to. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isLessThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be less than or equal to the specified value. + * + * @param path The path of the field to compare + * @param value The value the field must be less than or equal to. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isLessThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isLessThanOrEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must greater than the specified value. + * + * @param field The name of the field to compare + * @param value The value the field must be greater than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isGreaterThan:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThan:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must greater than the specified value. + * + * @param path The path of the field to compare + * @param value The value the field must be greater than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isGreaterThan:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThan:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be greater than or equal to the specified value. + * + * @param field The name of the field to compare + * @param value The value the field must be greater than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereField:(NSString *)field + isGreaterThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` with the additional filter that documents must + * contain the specified field and the value must be greater than or equal to the specified value. + * + * @param path The path of the field to compare + * @param value The value the field must be greater than. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryWhereFieldPath:(FIRFieldPath *)path + isGreaterThanOrEqualTo:(id)value FIR_SWIFT_NAME(whereField(_:isGreaterThanOrEqualTo:)); +// clang-format on + +#pragma mark - Sorting Data +/** + * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field. + * + * @param field The field to sort by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryOrderedByField:(NSString *)field FIR_SWIFT_NAME(order(by:)); + +/** + * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field. + * + * @param path The field to sort by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)path FIR_SWIFT_NAME(order(by:)); + +/** + * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field, + * optionally in descending order instead of ascending. + * + * @param field The field to sort by. + * @param descending Whether to sort descending. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryOrderedByField:(NSString *)field + descending:(BOOL)descending FIR_SWIFT_NAME(order(by:descending:)); +// clang-format on + +/** + * Creates and returns a new `FIRQuery` that's additionally sorted by the specified field, + * optionally in descending order instead of ascending. + * + * @param path The field to sort by. + * @param descending Whether to sort descending. + * + * @return The created `FIRQuery`. + */ +// clang-format off +- (FIRQuery *)queryOrderedByFieldPath:(FIRFieldPath *)path + descending:(BOOL)descending FIR_SWIFT_NAME(order(by:descending:)); +// clang-format on + +#pragma mark - Limiting Data +/** + * Creates and returns a new `FIRQuery` that's additionally limited to only return up to + * the specified number of documents. + * + * @param limit The maximum number of items to return. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryLimitedTo:(NSInteger)limit FIR_SWIFT_NAME(limit(to:)); + +#pragma mark - Choosing Endpoints +/** + * Creates and returns a new `FIRQuery` that starts at the provided document (inclusive). The + * starting position is relative to the order of the query. The document must contain all of the + * fields provided in the orderBy of this query. + * + * @param document The snapshot of the document to start at. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryStartingAtDocument:(FIRDocumentSnapshot *)document + FIR_SWIFT_NAME(start(atDocument:)); + +/** + * Creates and returns a new `FIRQuery` that starts at the provided fields relative to the order of + * the query. The order of the field values must match the order of the order by clauses of the + * query. + * + * @param fieldValues The field values to start this query at, in order of the query's order by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryStartingAtValues:(NSArray *)fieldValues FIR_SWIFT_NAME(start(at:)); + +/** + * Creates and returns a new `FIRQuery` that starts after the provided document (exclusive). The + * starting position is relative to the order of the query. The document must contain all of the + * fields provided in the orderBy of this query. + * + * @param document The snapshot of the document to start after. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryStartingAfterDocument:(FIRDocumentSnapshot *)document + FIR_SWIFT_NAME(start(afterDocument:)); + +/** + * Creates and returns a new `FIRQuery` that starts after the provided fields relative to the order + * of the query. The order of the field values must match the order of the order by clauses of the + * query. + * + * @param fieldValues The field values to start this query after, in order of the query's order + * by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryStartingAfterValues:(NSArray *)fieldValues FIR_SWIFT_NAME(start(after:)); + +/** + * Creates and returns a new `FIRQuery` that ends before the provided document (exclusive). The end + * position is relative to the order of the query. The document must contain all of the fields + * provided in the orderBy of this query. + * + * @param document The snapshot of the document to end before. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryEndingBeforeDocument:(FIRDocumentSnapshot *)document + FIR_SWIFT_NAME(end(beforeDocument:)); + +/** + * Creates and returns a new `FIRQuery` that ends before the provided fields relative to the order + * of the query. The order of the field values must match the order of the order by clauses of the + * query. + * + * @param fieldValues The field values to end this query before, in order of the query's order by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryEndingBeforeValues:(NSArray *)fieldValues FIR_SWIFT_NAME(end(before:)); + +/** + * Creates and returns a new `FIRQuery` that ends at the provided document (exclusive). The end + * position is relative to the order of the query. The document must contain all of the fields + * provided in the orderBy of this query. + * + * @param document The snapshot of the document to end at. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryEndingAtDocument:(FIRDocumentSnapshot *)document + FIR_SWIFT_NAME(end(atDocument:)); + +/** + * Creates and returns a new `FIRQuery` that ends at the provided fields relative to the order of + * the query. The order of the field values must match the order of the order by clauses of the + * query. + * + * @param fieldValues The field values to end this query at, in order of the query's order by. + * + * @return The created `FIRQuery`. + */ +- (FIRQuery *)queryEndingAtValues:(NSArray *)fieldValues FIR_SWIFT_NAME(end(at:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRQuerySnapshot.h b/Firestore/Source/Public/FIRQuerySnapshot.h new file mode 100644 index 0000000..800368d --- /dev/null +++ b/Firestore/Source/Public/FIRQuerySnapshot.h @@ -0,0 +1,65 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentChange; +@class FIRDocumentSnapshot; +@class FIRQuery; +@class FIRSnapshotMetadata; + +/** + * A `FIRQuerySnapshot` contains zero or more `FIRDocumentSnapshot` objects. It can be enumerated + * using "for ... in documentSet.documents" and its size can be inspected with `isEmpty` and + * `count`. + */ +FIR_SWIFT_NAME(QuerySnapshot) +@interface FIRQuerySnapshot : NSObject + +/** */ +- (id)init __attribute__((unavailable("FIRQuerySnapshot cannot be created directly."))); + +/** + * The query on which you called `getDocuments` or listened to in order to get this + * `FIRQuerySnapshot`. + */ +@property(nonatomic, strong, readonly) FIRQuery *query; + +/** Metadata about this snapshot, concerning its source and if it has local modifications. */ +@property(nonatomic, strong, readonly) FIRSnapshotMetadata *metadata; + +/** Indicates whether this `FIRQuerySnapshot` is empty (contains no documents). */ +@property(nonatomic, readonly, getter=isEmpty) BOOL empty; + +/** The count of documents in this `FIRQuerySnapshot`. */ +@property(nonatomic, readonly) NSInteger count; + +/** An Array of the `FIRDocumentSnapshots` that make up this document set. */ +@property(nonatomic, strong, readonly) NSArray<FIRDocumentSnapshot *> *documents; + +/** + * An array of the documents that changed since the last snapshot. If this is the first snapshot, + * all documents will be in the list as Added changes. + */ +@property(nonatomic, strong, readonly) NSArray<FIRDocumentChange *> *documentChanges; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRSetOptions.h b/Firestore/Source/Public/FIRSetOptions.h new file mode 100644 index 0000000..c865e06 --- /dev/null +++ b/Firestore/Source/Public/FIRSetOptions.h @@ -0,0 +1,46 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** + * An options object that configures the behavior of setData() calls. By providing the + * `FIRSetOptions` objects returned by `merge:`, the setData() methods in `FIRDocumentReference`, + * `FIRWriteBatch` and `FIRTransaction` can be configured to perform granular merges instead + * of overwriting the target documents in their entirety. + */ +NS_SWIFT_NAME(SetOptions) +@interface FIRSetOptions : NSObject + +/** */ +- (id)init NS_UNAVAILABLE; +/** + * Changes the behavior of setData() calls to only replace the values specified in its data + * argument. Fields with no corresponding values in the data passed to setData() will remain + * untouched. + * + * @return The created `FIRSetOptions` object + */ ++ (instancetype)merge; + +/** Whether setData() should merge existing data instead of performing an overwrite. */ +@property(nonatomic, readonly, getter=isMerge) BOOL merge; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRSnapshotMetadata.h b/Firestore/Source/Public/FIRSnapshotMetadata.h new file mode 100644 index 0000000..7fdd49c --- /dev/null +++ b/Firestore/Source/Public/FIRSnapshotMetadata.h @@ -0,0 +1,44 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** Metadata about a snapshot, describing the state of the snapshot. */ +@interface FIRSnapshotMetadata : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Returns YES if the snapshot contains the result of local writes (e.g. set() or update() calls) + * that have not yet been committed to the backend. If your listener has opted into metadata updates + * (via `FIRDocumentListenOptions` or `FIRQueryListenOptions`) you will receive another snapshot + * with `hasPendingWrites` equal to NO once the writes have been committed to the backend. + */ +@property(nonatomic, assign, readonly, getter=hasPendingWrites) BOOL pendingWrites; + +/** + * Returns YES if the snapshot was created from cached data rather than guaranteed up-to-date server + * data. If your listener has opted into metadata updates (via `FIRDocumentListenOptions` or + * `FIRQueryListenOptions`) you will receive another snapshot with `isFromCache` equal to NO once + * the client has received up-to-date data from the backend. + */ +@property(nonatomic, assign, readonly, getter=isFromCache) BOOL fromCache; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRTransaction.h b/Firestore/Source/Public/FIRTransaction.h new file mode 100644 index 0000000..68e4600 --- /dev/null +++ b/Firestore/Source/Public/FIRTransaction.h @@ -0,0 +1,106 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentReference; +@class FIRDocumentSnapshot; +@class FIRSetOptions; + +/** + * `FIRTransaction` provides methods to read and write data within a transaction. + * + * @see FIRFirestore#transaction:completion: + */ +FIR_SWIFT_NAME(Transaction) +@interface FIRTransaction : NSObject + +/** */ +- (id)init __attribute__((unavailable("FIRTransaction cannot be created directly."))); + +/** + * Writes to the document referred to by `document`. If the document doesn't yet exist, + * this method creates it and then sets the data. If the document exists, this method overwrites + * the document data with the new values. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(setData(_:forDocument:)); +// clang-format on + +/** + * Writes to the document referred to by `document`. If the document doesn't yet exist, + * this method creates it and then sets the data. If you pass `FIRSetOptions`, the provided data + * will be merged into an existing document. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @param options A `FIRSetOptions` used to configure the set behavior. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options + FIR_SWIFT_NAME(setData(_:forDocument:options:)); +// clang-format on + +/** + * Updates fields in the document referred to by `document`. + * If the document does not exist, the transaction will fail. + * + * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or + * `FIRFieldPath`) and values with which to update the document. + * @param document A reference to the document whose data should be updated. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRTransaction *)updateData:(NSDictionary<id, id> *)fields + forDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(updateData(_:forDocument:)); +// clang-format on + +/** + * Deletes the document referred to by `document`. + * + * @param document A reference to the document that should be deleted. + * @return This `FIRTransaction` instance. Used for chaining method calls. + */ +- (FIRTransaction *)deleteDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(deleteDocument(_:)); + +/** + * Reads the document referenced by `document`. + * + * @param document A reference to the document to be read. + * @param error An out parameter to capture an error, if one occurred. + */ +- (FIRDocumentSnapshot *_Nullable)getDocument:(FIRDocumentReference *)document + error:(NSError *__autoreleasing *)error + FIR_SWIFT_NAME(getDocument(_:)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Public/FIRWriteBatch.h b/Firestore/Source/Public/FIRWriteBatch.h new file mode 100644 index 0000000..b88e6cc --- /dev/null +++ b/Firestore/Source/Public/FIRWriteBatch.h @@ -0,0 +1,107 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FIRFirestoreSwiftNameSupport.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FIRDocumentReference; +@class FIRSetOptions; + +/** + * A write batch is used to perform multiple writes as a single atomic unit. + * + * A WriteBatch object can be acquired by calling [FIRFirestore batch]. It provides methods for + * adding writes to the write batch. None of the writes will be committed (or visible locally) + * until [FIRWriteBatch commit] is called. + * + * Unlike transactions, write batches are persisted offline and therefore are preferable when you + * don't need to condition your writes on read data. + */ +FIR_SWIFT_NAME(WriteBatch) +@interface FIRWriteBatch : NSObject + +/** :nodoc: */ +- (id)init __attribute__((unavailable("FIRWriteBatch cannot be created directly."))); + +/** + * Writes to the document referred to by `document`. If the document doesn't yet exist, + * this method creates it and then sets the data. If the document exists, this method overwrites + * the document data with the new values. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document FIR_SWIFT_NAME(setData(_:forDocument:)); +// clang-format on + +/** + * Writes to the document referred to by `document`. If the document doesn't yet exist, + * this method creates it and then sets the data. If you pass `FIRSetOptions`, the provided data + * will be merged into an existing document. + * + * @param data An `NSDictionary` that contains the fields and data to write to the document. + * @param document A reference to the document whose data should be overwritten. + * @param options A `FIRSetOptions` used to configure the set behavior. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)setData:(NSDictionary<NSString *, id> *)data + forDocument:(FIRDocumentReference *)document + options:(FIRSetOptions *)options + FIR_SWIFT_NAME(setData(_:forDocument:options:)); +// clang-format on + +/** + * Updates fields in the document referred to by `document`. + * If document does not exist, the write batch will fail. + * + * @param fields An `NSDictionary` containing the fields (expressed as an `NSString` or + * `FIRFieldPath`) and values with which to update the document. + * @param document A reference to the document whose data should be updated. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +// clang-format off +- (FIRWriteBatch *)updateData:(NSDictionary<id, id> *)fields + forDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(updateData(_:forDocument:)); +// clang-format on + +/** + * Deletes the document referred to by `document`. + * + * @param document A reference to the document that should be deleted. + * @return This `FIRWriteBatch` instance. Used for chaining method calls. + */ +- (FIRWriteBatch *)deleteDocument:(FIRDocumentReference *)document + FIR_SWIFT_NAME(deleteDocument(_:)); + +/** + * 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. + */ +- (void)commitWithCompletion:(void (^)(NSError *_Nullable error))completion; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTBufferedWriter.h b/Firestore/Source/Remote/FSTBufferedWriter.h new file mode 100644 index 0000000..83fada6 --- /dev/null +++ b/Firestore/Source/Remote/FSTBufferedWriter.h @@ -0,0 +1,44 @@ +/* + * 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 <Foundation/Foundation.h> +#import <RxLibrary/GRXWriteable.h> +#import <RxLibrary/GRXWriter.h> + +NS_ASSUME_NONNULL_BEGIN + +/** + * A buffered GRXWriter. + * + * GRPC only allows a single message to be written to a channel at a time. While the channel is + * sending, GRPC sets the state of the GRXWriter representing the request stream to + * GRXWriterStatePaused. Once the channel is ready to accept more messages GRPC sets the state of + * the writer to GRXWriterStateStarted. + * + * This class is NOT thread safe, even though it is accessed from multiple threads. To conform with + * the contract GRPC uses, all method calls on the FSTBufferedWriter must be @synchronized on the + * receiver. + */ +@interface FSTBufferedWriter : GRXWriter <GRXWriteable> + +/** + * Writes a message into the buffer. Must be called inside an @synchronized block on the receiver. + */ +- (void)writeValue:(id)value; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTBufferedWriter.m b/Firestore/Source/Remote/FSTBufferedWriter.m new file mode 100644 index 0000000..d86e03a --- /dev/null +++ b/Firestore/Source/Remote/FSTBufferedWriter.m @@ -0,0 +1,134 @@ +/* + * 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 <Protobuf/GPBProtocolBuffers.h> + +#import "FSTBufferedWriter.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTBufferedWriter { + GRXWriterState _state; + NSMutableArray<NSData *> *_queue; + + id<GRXWriteable> _writeable; +} + +- (instancetype)init { + if (self = [super init]) { + _state = GRXWriterStateNotStarted; + _queue = [[NSMutableArray alloc] init]; + } + return self; +} + +#pragma mark - GRXWriteable implementation + +/** Push the next value of the sequence to the receiving object. */ +- (void)writeValue:(id)value { + if (_state == GRXWriterStateStarted && _queue.count == 0) { + // Skip the queue. + [_writeable writeValue:value]; + } else { + // Buffer the new value. Note that the value is assumed to be transient and doesn't need to + // be copied. + [_queue addObject:value]; + } +} + +/** + * Signal that the sequence is completed, or that an error ocurred. After this message is sent to + * the receiver, neither it nor writeValue: may be called again. + */ +- (void)writesFinishedWithError:(nullable NSError *)error { + // Unimplemented. If we ever wanted to implement sender-side initiated half close we could do so + // by buffering (or sending) and error. + [self doesNotRecognizeSelector:_cmd]; +} + +#pragma mark GRXWriter implementation +// The GRXWriter implementation defines the send side of the RPC stream. Once the RPC is ready it +// will call startWithWriteable passing a GRXWriteable into which requests can be written but only +// when the GRXWriter is in the started state. + +/** + * Called by GRPCCall when it is ready to accept for the first request. Requests should be written + * to the passed writeable. + * + * GRPCCall will synchronize on the receiver around this call. + */ +- (void)startWithWriteable:(id<GRXWriteable>)writeable { + _state = GRXWriterStateStarted; + _writeable = writeable; +} + +/** + * Called by GRPCCall to implement flow control on the sending side of the stream. After each + * writeValue: on the requestsWriteable, GRPCCall will call setState:GRXWriterStatePaused to apply + * backpressure. Once the stream is ready to accept another message, GRPCCall will call + * setState:GRXWriterStateStarted. + * + * GRPCCall will synchronize on the receiver around this call. + */ +- (void)setState:(GRXWriterState)newState { + // Manual transitions are only allowed from the started or paused states. + if (_state == GRXWriterStateNotStarted || _state == GRXWriterStateFinished) { + return; + } + + switch (newState) { + case GRXWriterStateFinished: + _state = newState; + // Per GRXWriter's contract, setting the state to Finished manually means one doesn't wish the + // writeable to be messaged anymore. + _queue = nil; + _writeable = nil; + return; + case GRXWriterStatePaused: + _state = newState; + return; + case GRXWriterStateStarted: + if (_state == GRXWriterStatePaused) { + _state = newState; + [self writeBufferedMessages]; + } + return; + case GRXWriterStateNotStarted: + return; + } +} + +- (void)finishWithError:(nullable NSError *)error { + [_writeable writesFinishedWithError:error]; + self.state = GRXWriterStateFinished; +} + +- (void)writeBufferedMessages { + while (_state == GRXWriterStateStarted && _queue.count > 0) { + id value = _queue[0]; + [_queue removeObjectAtIndex:0]; + + // In addition to writing the value here GRPC will apply backpressure by pausing the GRXWriter + // wrapping this buffer. That writer must call -pauseMessages which will cause this loop to + // exit. Synchronization is not required since the callback happens within the body of the + // writeValue implementation. + [_writeable writeValue:value]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTDatastore.h b/Firestore/Source/Remote/FSTDatastore.h new file mode 100644 index 0000000..840d2fe --- /dev/null +++ b/Firestore/Source/Remote/FSTDatastore.h @@ -0,0 +1,365 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTTypes.h" + +@class FSTDatabaseInfo; +@class FSTDocumentKey; +@class FSTDispatchQueue; +@class FSTMutation; +@class FSTMutationResult; +@class FSTQueryData; +@class FSTSnapshotVersion; +@class FSTWatchChange; +@class FSTWatchStream; +@class FSTWriteStream; +@class GRPCCall; +@class GRXWriter; + +@protocol FSTCredentialsProvider; +@protocol FSTWatchStreamDelegate; +@protocol FSTWriteStreamDelegate; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTDatastore represents a proxy for the remote server, hiding details of the RPC layer. It: + * + * - Manages connections to the server + * - Authenticates to the server + * - Manages threading and keeps higher-level code running on the worker queue + * - Serializes internal model objects to and from protocol buffers + * + * The FSTDatastore is generally not responsible for understanding the higher-level protocol + * involved in actually making changes or reading data, and aside from the connections it manages + * is otherwise stateless. + */ +@interface FSTDatastore : NSObject + +/** Creates a new Datastore instance with the given database info. */ ++ (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials; + +- (instancetype)init __attribute__((unavailable("Use a static constructor method."))); + +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + NS_DESIGNATED_INITIALIZER; + +/** Converts the error to a FIRFirestoreErrorDomain error. */ ++ (NSError *)firestoreErrorForError:(NSError *)error; + +/** Returns YES if the given error indicates the RPC associated with it may not be retried. */ ++ (BOOL)isPermanentWriteError:(NSError *)error; + +/** Returns YES if the given error is a GRPC ABORTED error. **/ ++ (BOOL)isAbortedError:(NSError *)error; + +/** Looks up a list of documents in datastore. */ +- (void)lookupDocuments:(NSArray<FSTDocumentKey *> *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion; + +/** Commits data to datastore. */ +- (void)commitMutations:(NSArray<FSTMutation *> *)mutations + completion:(FSTVoidErrorBlock)completion; + +/** Creates a new watch stream. */ +- (FSTWatchStream *)createWatchStreamWithDelegate:(id<FSTWatchStreamDelegate>)delegate; + +/** Creates a new write stream. */ +- (FSTWriteStream *)createWriteStreamWithDelegate:(id<FSTWriteStreamDelegate>)delegate; + +/** The name of the database and the backend. */ +@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; + +@end + +/** + * An FSTStream is an abstract base class that represents a restartable streaming RPC to the + * Firestore backend. It's built on top of GRPC's own support for streaming RPCs, and adds several + * critical features for our clients: + * + * - Restarting a stream is allowed (after failure) + * - Exponential backoff on failure (independent of the underlying channel) + * - Authentication via FSTCredentialsProvider + * - Dispatching all callbacks into the shared worker queue + * + * Subclasses of FSTStream implement serialization of models to and from bytes (via protocol + * buffers) for a specific streaming RPC and emit events specific to the stream. + * + * ## Starting and Stopping + * + * Streaming RPCs are stateful and need to be started before messages can be sent and received. + * The FSTStream will call its delegate's specific streamDidOpen method once the stream is ready + * to accept requests. + * + * Should a `start` fail, FSTStream will call its delegate's specific streamDidClose method with an + * NSError indicating what went wrong. The delegate is free to call start again. + * + * An FSTStream can also be explicitly stopped which indicates that the caller has discarded the + * stream and no further events should be emitted. Once explicitly stopped, a stream cannot be + * restarted. + * + * ## Subclassing Notes + * + * An implementation of FSTStream needs to implement the following methods: + * - `createRPCWithRequestsWriter`, should create the specific RPC (a GRPCCall object). + * - `handleStreamOpen`, should call through to the stream-specific streamDidOpen method. + * - `handleStreamMessage`, receives protocol buffer responses from GRPC and must deserialize and + * delegate to some stream specific response method. + * - `handleStreamClose`, calls through to the stream-specific streamDidClose method. + * + * Additionally, beyond these required methods, subclasses will want to implement methods that + * take request models, serialize them, and write them to using writeRequest:. + * + * ## RPC Message Type + * + * FSTStream intentionally uses the GRPCCall interface to GRPC directly, bypassing both GRPCProtoRPC + * and GRXBufferedPipe for sending data. This has been done to avoid race conditions that come out + * of a loosely specified locking contract on GRXWriter. There's essentially no way to safely use + * any of the wrapper objects for GRXWriter (that perform buffering or conversion to/from protos). + * + * See https://github.com/grpc/grpc/issues/10957 for the kinds of things we're trying to avoid. + */ +@interface FSTStream : NSObject + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * An abstract method used by `start` to create a streaming RPC specific to this type of stream. + * The RPC should be created such that requests are taken from `self`. + * + * Note that the returned GRPCCall must not be a GRPCProtoRPC, since the rest of the streaming + * mechanism assumes it is dealing in bytes-level requests and responses. + */ +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter; + +/** + * Returns YES if `start` has been called and no error has occurred. YES indicates the stream is + * open or in the process of opening (which encompasses respecting backoff, getting auth tokens, + * and starting the actual RPC). Use `isOpen` to determine if the stream is open and ready for + * outbound requests. + */ +- (BOOL)isStarted; + +/** Returns YES if the underlying RPC is open and the stream is ready for outbound requests. */ +- (BOOL)isOpen; + +/** + * Starts the RPC. Only allowed if isStarted returns NO. The stream is not immediately ready for + * use: the delegate's watchStreamDidOpen method will be invoked when the RPC is ready for outbound + * requests, at which point `isOpen` will return YES. + * + * When start returns, -isStarted will return YES. + */ +- (void)start; + +/** + * Stops the RPC. This call is idempotent and allowed regardless of the current isStarted state. + * + * Unlike a transient stream close, stopping a stream is permanent. This is guaranteed NOT to emit + * any further events on the stream-specific delegate, including the streamDidClose method. + * + * NOTE: This no-events contract may seem counter-intuitive but allows the caller to + * straightforwardly sequence stream tear-down without having to worry about when the delegate's + * streamDidClose methods will get called. For example if the stream must be exchanged for another + * during a user change this allows `stop` to be called eagerly without worrying about the + * streamDidClose method accidentally restarting the stream before the new one is ready. + * + * When stop returns, -isStarted and -isOpen will both return NO. + */ +- (void)stop; + +/** + * After an error the stream will usually back off on the next attempt to start it. If the error + * warrants an immediate restart of the stream, the sender can use this to indicate that the + * receiver should not back off. + * + * Each error will call the stream-specific streamDidClose method. That method can decide to + * inhibit backoff if required. + */ +- (void)inhibitBackoff; + +@end + +#pragma mark - FSTWatchStream + +/** A protocol defining the events that can be emitted by the FSTWatchStream. */ +@protocol FSTWatchStreamDelegate <NSObject> + +/** Called by the FSTWatchStream when it is ready to accept outbound request messages. */ +- (void)watchStreamDidOpen; + +/** + * Called by the FSTWatchStream with changes and the snapshot versions included in in the + * WatchChange responses sent back by the server. + */ +- (void)watchStreamDidChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion; + +/** + * Called by the FSTWatchStream when the underlying streaming RPC is closed for whatever reason, + * usually because of an error, but possibly due to an idle timeout. The error passed to this + * method may be nil, in which case the stream was closed without attributable fault. + * + * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping" + * on FSTStream for details. + */ +- (void)watchStreamDidClose:(NSError *_Nullable)error; + +@end + +/** + * An FSTStream that implements the StreamingWatch RPC. + * + * Once the FSTWatchStream has called the streamDidOpen method, any number of watchQuery and + * unwatchTargetId calls can be sent to control what changes will be sent from the server for + * WatchChanges. + */ +@interface FSTWatchStream : FSTStream + +/** + * Initializes the watch stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id<FSTWatchStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Registers interest in the results of the given query. If the query includes a resumeToken it + * will be included in the request. Results that affect the query will be streamed back as + * WatchChange messages that reference the targetID included in |query|. + */ +- (void)watchQuery:(FSTQueryData *)query; + +/** Unregisters interest in the results of the query associated with the given target ID. */ +- (void)unwatchTargetID:(FSTTargetID)targetID; + +@property(nonatomic, weak, readonly) id<FSTWatchStreamDelegate> delegate; + +@end + +#pragma mark - FSTWriteStream + +@protocol FSTWriteStreamDelegate <NSObject> + +/** Called by the FSTWriteStream when it is ready to accept outbound request messages. */ +- (void)writeStreamDidOpen; + +/** + * Called by the FSTWriteStream upon a successful handshake response from the server, which is the + * receiver's cue to send any pending writes. + */ +- (void)writeStreamDidCompleteHandshake; + +/** + * Called by the FSTWriteStream upon receiving a StreamingWriteResponse from the server that + * contains mutation results. + */ +- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)results; + +/** + * Called when the FSTWriteStream's underlying RPC is closed for whatever reason, usually because + * of an error, but possibly due to an idle timeout. The error passed to this method may be nil, in + * which case the stream was closed without attributable fault. + * + * NOTE: This will not be called after `stop` is called on the stream. See "Starting and Stopping" + * on FSTStream for details. + */ +- (void)writeStreamDidClose:(NSError *_Nullable)error; + +@end + +/** + * An FSTStream that implements the StreamingWrite RPC. + * + * The StreamingWrite RPC requires the caller to maintain special `streamToken` state in between + * calls, to help the server understand which responses the client has processed by the time the + * next request is made. Every response may contain a `streamToken`; this value must be passed to + * the next request. + * + * After calling `start` on this stream, the next request must be a handshake, containing whatever + * streamToken is on hand. Once a response to this request is received, all pending mutations may + * be submitted. When submitting multiple batches of mutations at the same time, it's okay to use + * the same streamToken for the calls to `writeMutations:`. + */ +@interface FSTWriteStream : FSTStream + +/** + * Initializes the write stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id<FSTWriteStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass NS_UNAVAILABLE; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Sends an initial streamToken to the server, performing the handshake required to make the + * StreamingWrite RPC work. Subsequent `writeMutations:` calls should wait until a response has + * been delivered to the delegate's writeStreamDidCompleteHandshake method. + */ +- (void)writeHandshake; + +/** Sends a group of mutations to the Firestore backend to apply. */ +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations; + +@property(nonatomic, weak, readonly) id<FSTWriteStreamDelegate> delegate; + +/** + * Tracks whether or not a handshake has been successfully exchanged and the stream is ready to + * accept mutations. + */ +@property(nonatomic, assign, readwrite, getter=isHandshakeComplete) BOOL handshakeComplete; + +/** + * The last received stream token from the server, used to acknowledge which responses the client + * has processed. Stream tokens are opaque checkpoint markers whose only real value is their + * inclusion in the next request. + * + * FSTWriteStream manages propagating this value from responses to the next request. + */ +@property(nonatomic, strong, nullable) NSData *lastStreamToken; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTDatastore.m b/Firestore/Source/Remote/FSTDatastore.m new file mode 100644 index 0000000..3ed2729 --- /dev/null +++ b/Firestore/Source/Remote/FSTDatastore.m @@ -0,0 +1,1027 @@ +/* + * 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 "FSTDatastore.h" + +#import <GRPCClient/GRPCCall+OAuth2.h> +#import <GRPCClient/GRPCCall.h> +#import <ProtoRPC/ProtoRPC.h> + +#import "FIRFirestore+Internal.h" +#import "FIRFirestoreErrors.h" +#import "FIRFirestoreVersion.h" +#import "FSTAssert.h" +#import "FSTBufferedWriter.h" +#import "FSTClasses.h" +#import "FSTCredentialsProvider.h" +#import "FSTDatabaseID.h" +#import "FSTDatabaseInfo.h" +#import "FSTDispatchQueue.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTExponentialBackoff.h" +#import "FSTLocalStore.h" +#import "FSTLogger.h" +#import "FSTMutation.h" +#import "FSTQueryData.h" +#import "FSTSerializerBeta.h" + +#import "Firestore.pbrpc.h" + +NS_ASSUME_NONNULL_BEGIN + +// GRPC does not publicly declare a means of disabling SSL, which we need for testing. Firestore +// directly exposes an sslEnabled setting so this is required to plumb that through. Note that our +// own tests depend on this working so we'll know if this changes upstream. +@interface GRPCHost ++ (nullable instancetype)hostWithAddress:(NSString *)address; +@property(nonatomic, getter=isSecure) BOOL secure; +@end + +/** + * Initial backoff time in seconds after an error. + * Set to 1s according to https://cloud.google.com/apis/design/errors. + */ +static const NSTimeInterval kBackoffInitialDelay = 1; +static const NSTimeInterval kBackoffMaxDelay = 60.0; +static const double kBackoffFactor = 1.5; +static NSString *const kXGoogAPIClientHeader = @"x-goog-api-client"; +static NSString *const kGoogleCloudResourcePrefix = @"google-cloud-resource-prefix"; + +/** Function typedef used to create RPCs. */ +typedef GRPCProtoCall * (^RPCFactory)(); + +#pragma mark - FSTStream + +/** The state of a stream. */ +typedef NS_ENUM(NSInteger, FSTStreamState) { + /** + * The streaming RPC is not running and there's no error condition. Calling `start` will + * start the stream immediately without backoff. While in this state -isStarted will return NO. + */ + FSTStreamStateInitial = 0, + + /** + * The stream is starting, and is waiting for an auth token to attach to the initial request. + * While in this state, isStarted will return YES but isOpen will return NO. + */ + FSTStreamStateAuth, + + /** + * The streaming RPC is up and running. Requests and responses can flow freely. Both + * isStarted and isOpen will return YES. + */ + FSTStreamStateOpen, + + /** + * The stream encountered an error. The next start attempt will back off. While in this state + * -isStarted will return NO. + */ + FSTStreamStateError, + + /** + * An in-between state after an error where the stream is waiting before re-starting. After + * waiting is complete, the stream will try to open. While in this state -isStarted will + * return YES but isOpen will return NO. + */ + FSTStreamStateBackoff, + + /** + * The stream has been explicitly stopped; no further events will be emitted. + */ + FSTStreamStateStopped, +}; + +// We need to declare these classes first so that Datastore can alloc them. + +@interface FSTWatchStream () + +/** The delegate that will receive events generated by the watch stream. */ +@property(nonatomic, weak, nullable) id<FSTWatchStreamDelegate> delegate; + +@end + +@interface FSTBetaWatchStream : FSTWatchStream + +/** + * Initializes the watch stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + serializer:(FSTSerializerBeta *)serializer + delegate:(id<FSTWatchStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id<FSTWatchStreamDelegate>)delegate NS_UNAVAILABLE; + +@end + +@interface FSTWriteStream () + +@property(nonatomic, weak, nullable) id<FSTWriteStreamDelegate> delegate; + +@end + +@interface FSTBetaWriteStream : FSTWriteStream + +/** + * Initializes the write stream with its dependencies. + */ +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + serializer:(FSTSerializerBeta *)serializer + delegate:(id<FSTWriteStreamDelegate>)delegate NS_DESIGNATED_INITIALIZER; + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id<FSTWriteStreamDelegate>)delegate NS_UNAVAILABLE; + +@end + +@interface FSTStream () <GRXWriteable> + +@property(nonatomic, strong, readonly) FSTDatabaseInfo *databaseInfo; +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; +@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentials; +@property(nonatomic, unsafe_unretained, readonly) Class responseMessageClass; +@property(nonatomic, strong, readonly) FSTExponentialBackoff *backoff; + +/** A flag tracking whether the stream received a message from the backend. */ +@property(nonatomic, assign) BOOL messageReceived; + +/** + * Stream state as exposed to consumers of FSTStream. This differs from GRXWriter's notion of the + * state of the stream. + */ +@property(nonatomic, assign) FSTStreamState state; + +/** The RPC handle. Used for cancellation. */ +@property(nonatomic, strong, nullable) GRPCCall *rpc; + +/** + * The send-side of the RPC stream in which to submit requests, but only once the underlying RPC has + * started. + */ +@property(nonatomic, strong, nullable) FSTBufferedWriter *requestsWriter; + +@end + +#pragma mark - FSTDatastore + +@interface FSTDatastore () + +/** The GRPC service for Firestore. */ +@property(nonatomic, strong, readonly) GCFSFirestore *service; + +@property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; + +/** An object for getting an auth token before each request. */ +@property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentials; + +@property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; + +@end + +@implementation FSTDatastore + ++ (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)databaseInfo + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials { + return [[FSTDatastore alloc] initWithDatabaseInfo:databaseInfo + workerDispatchQueue:workerDispatchQueue + credentials:credentials]; +} + +- (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials { + if (self = [super init]) { + _databaseInfo = databaseInfo; + if (!databaseInfo.isSSLEnabled) { + GRPCHost *hostConfig = [GRPCHost hostWithAddress:databaseInfo.host]; + hostConfig.secure = NO; + } + _service = [GCFSFirestore serviceWithHost:databaseInfo.host]; + _workerDispatchQueue = workerDispatchQueue; + _credentials = credentials; + _serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseInfo.databaseID]; + } + return self; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<FSTDatastore: %@>", self.databaseInfo]; +} + +/** + * Converts the error to an error within the domain FIRFirestoreErrorDomain. + */ ++ (NSError *)firestoreErrorForError:(NSError *)error { + if (!error) { + return error; + } else if ([error.domain isEqualToString:FIRFirestoreErrorDomain]) { + return error; + } else if ([error.domain isEqualToString:kGRPCErrorDomain]) { + FSTAssert(error.code >= GRPCErrorCodeCancelled && error.code <= GRPCErrorCodeUnauthenticated, + @"Unknown GRPC error code: %ld", (long)error.code); + return + [NSError errorWithDomain:FIRFirestoreErrorDomain code:error.code userInfo:error.userInfo]; + } else { + return [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeUnknown + userInfo:@{NSUnderlyingErrorKey : error}]; + } +} + ++ (BOOL)isAbortedError:(NSError *)error { + FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain], + @"isAbortedError: only works with errors emitted by FSTDatastore."); + return error.code == FIRFirestoreErrorCodeAborted; +} + ++ (BOOL)isPermanentWriteError:(NSError *)error { + FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain], + @"isPerminanteWriteError: only works with errors emitted by FSTDatastore."); + switch (error.code) { + case FIRFirestoreErrorCodeCancelled: + case FIRFirestoreErrorCodeUnknown: + case FIRFirestoreErrorCodeDeadlineExceeded: + case FIRFirestoreErrorCodeResourceExhausted: + case FIRFirestoreErrorCodeInternal: + case FIRFirestoreErrorCodeUnavailable: + case FIRFirestoreErrorCodeUnauthenticated: + // Unauthenticated means something went wrong with our token and we need + // to retry with new credentials which will happen automatically. + // TODO(b/37325376): Give up after second unauthenticated error. + return NO; + case FIRFirestoreErrorCodeInvalidArgument: + case FIRFirestoreErrorCodeNotFound: + case FIRFirestoreErrorCodeAlreadyExists: + case FIRFirestoreErrorCodePermissionDenied: + case FIRFirestoreErrorCodeFailedPrecondition: + case FIRFirestoreErrorCodeAborted: + // Aborted might be retried in some scenarios, but that is dependant on + // the context and should handled individually by the calling code. + // See https://cloud.google.com/apis/design/errors + case FIRFirestoreErrorCodeOutOfRange: + case FIRFirestoreErrorCodeUnimplemented: + case FIRFirestoreErrorCodeDataLoss: + default: + return YES; + } +} + +/** Returns the string to be used as x-goog-api-client header value. */ ++ (NSString *)googAPIClientHeaderValue { + // TODO(dimond): This should ideally also include the grpc version, however, gRPC defines the + // version as a macro, so it would be hardcoded based on version we have at compile time of + // the Firestore library, rather than the version available at runtime/at compile time by the + // user of the library. + return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FirebaseFirestoreVersionString]; +} + +/** Returns the string to be used as google-cloud-resource-prefix header value. */ ++ (NSString *)googleCloudResourcePrefixForDatabaseID:(FSTDatabaseID *)databaseID { + return [NSString + stringWithFormat:@"projects/%@/databases/%@", databaseID.projectID, databaseID.databaseID]; +} +/** + * Takes a dictionary of (HTTP) response headers and returns the set of whitelisted headers + * (for logging purposes). + */ ++ (NSDictionary<NSString *, NSString *> *)extractWhiteListedHeaders: + (NSDictionary<NSString *, NSString *> *)headers { + NSMutableDictionary<NSString *, NSString *> *whiteListedHeaders = + [NSMutableDictionary dictionary]; + NSArray<NSString *> *whiteList = @[ + @"date", @"x-google-backends", @"x-google-netmon-label", @"x-google-service", + @"x-google-gfe-request-trace" + ]; + [headers + enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { + if ([whiteList containsObject:[headerName lowercaseString]]) { + whiteListedHeaders[headerName] = headerValue; + } + }]; + return whiteListedHeaders; +} + +/** Logs the (whitelisted) headers returned for an GRPCProtoCall RPC. */ ++ (void)logHeadersForRPC:(GRPCProtoCall *)rpc RPCName:(NSString *)rpcName { + if ([FIRFirestore isLoggingEnabled]) { + FSTLog(@"RPC %@ returned headers (whitelisted): %@", rpcName, + [FSTDatastore extractWhiteListedHeaders:rpc.responseHeaders]); + } +} + +- (void)commitMutations:(NSArray<FSTMutation *> *)mutations + completion:(FSTVoidErrorBlock)completion { + GCFSCommitRequest *request = [GCFSCommitRequest message]; + request.database = [self.serializer encodedDatabaseID]; + + NSMutableArray<GCFSWrite *> *mutationProtos = [NSMutableArray array]; + for (FSTMutation *mutation in mutations) { + [mutationProtos addObject:[self.serializer encodedMutation:mutation]]; + } + request.writesArray = mutationProtos; + + RPCFactory rpcFactory = ^GRPCProtoCall * { + __block GRPCProtoCall *rpc = [self.service + RPCToCommitWithRequest:request + handler:^(GCFSCommitResponse *response, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsync:^{ + FSTLog(@"RPC CommitRequest completed. Error: %@", error); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"CommitRequest"]; + completion(error); + }]; + }]; + return rpc; + }; + + [self invokeRPCWithFactory:rpcFactory errorHandler:completion]; +} + +- (void)lookupDocuments:(NSArray<FSTDocumentKey *> *)keys + completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { + GCFSBatchGetDocumentsRequest *request = [GCFSBatchGetDocumentsRequest message]; + request.database = [self.serializer encodedDatabaseID]; + for (FSTDocumentKey *key in keys) { + [request.documentsArray addObject:[self.serializer encodedDocumentKey:key]]; + } + + __block FSTMaybeDocumentDictionary *results = + [FSTMaybeDocumentDictionary maybeDocumentDictionary]; + + RPCFactory rpcFactory = ^GRPCProtoCall * { + __block GRPCProtoCall *rpc = [self.service + RPCToBatchGetDocumentsWithRequest:request + eventHandler:^(BOOL done, + GCFSBatchGetDocumentsResponse *_Nullable response, + NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsync:^{ + if (error) { + FSTLog(@"RPC BatchGetDocuments completed. Error: %@", error); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; + completion(nil, error); + return; + } + + if (!done) { + // Streaming response, accumulate result + FSTMaybeDocument *doc = + [self.serializer decodedMaybeDocumentFromBatch:response]; + results = [results dictionaryBySettingObject:doc forKey:doc.key]; + } else { + // Streaming response is done, call completion + FSTLog(@"RPC BatchGetDocuments completed successfully."); + [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; + FSTAssert(!response, @"Got response after done."); + NSMutableArray<FSTMaybeDocument *> *docs = + [NSMutableArray arrayWithCapacity:keys.count]; + for (FSTDocumentKey *key in keys) { + [docs addObject:results[key]]; + } + completion(docs, nil); + } + }]; + }]; + return rpc; + }; + + [self invokeRPCWithFactory:rpcFactory + errorHandler:^(NSError *_Nonnull error) { + error = [FSTDatastore firestoreErrorForError:error]; + completion(nil, error); + }]; +} + +- (void)invokeRPCWithFactory:(GRPCProtoCall * (^)())rpcFactory + errorHandler:(FSTVoidErrorBlock)errorHandler { + // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token, + // but I'm not sure how to detect that right now. http://b/32762461 + [self.credentials + getTokenForcingRefresh:NO + completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ + if (error) { + errorHandler(error); + } else { + GRPCProtoCall *rpc = rpcFactory(); + [FSTDatastore prepareHeadersForRPC:rpc + databaseID:self.databaseInfo.databaseID + token:result.token]; + [rpc start]; + } + }]; + }]; +} + +- (FSTWatchStream *)createWatchStreamWithDelegate:(id<FSTWatchStreamDelegate>)delegate { + return [[FSTBetaWatchStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + serializer:_serializer + delegate:delegate]; +} + +- (FSTWriteStream *)createWriteStreamWithDelegate:(id<FSTWriteStreamDelegate>)delegate { + return [[FSTBetaWriteStream alloc] initWithDatabase:_databaseInfo + workerDispatchQueue:_workerDispatchQueue + credentials:_credentials + serializer:_serializer + delegate:delegate]; +} + +/** Adds headers to the RPC including any OAuth access token if provided .*/ ++ (void)prepareHeadersForRPC:(GRPCCall *)rpc + databaseID:(FSTDatabaseID *)databaseID + token:(nullable NSString *)token { + rpc.oauth2AccessToken = token; + rpc.requestHeaders[kXGoogAPIClientHeader] = [FSTDatastore googAPIClientHeaderValue]; + // This header is used to improve routing and project isolation by the backend. + rpc.requestHeaders[kGoogleCloudResourcePrefix] = + [FSTDatastore googleCloudResourcePrefixForDatabaseID:databaseID]; +} + +@end + +#pragma mark - FSTStream + +@implementation FSTStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass { + if (self = [super init]) { + _databaseInfo = database; + _workerDispatchQueue = workerDispatchQueue; + _credentials = credentials; + _responseMessageClass = responseMessageClass; + + _backoff = [FSTExponentialBackoff exponentialBackoffWithDispatchQueue:workerDispatchQueue + initialDelay:kBackoffInitialDelay + backoffFactor:kBackoffFactor + maxDelay:kBackoffMaxDelay]; + _state = FSTStreamStateInitial; + } + return self; +} + +- (BOOL)isStarted { + [self.workerDispatchQueue verifyIsCurrentQueue]; + FSTStreamState state = self.state; + return state == FSTStreamStateBackoff || state == FSTStreamStateAuth || + state == FSTStreamStateOpen; +} + +- (BOOL)isOpen { + [self.workerDispatchQueue verifyIsCurrentQueue]; + return self.state == FSTStreamStateOpen; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)start { + [self.workerDispatchQueue verifyIsCurrentQueue]; + + if (self.state == FSTStreamStateError) { + [self performBackoff]; + return; + } + + FSTLog(@"%@ %p start", NSStringFromClass([self class]), (__bridge void *)self); + FSTAssert(self.state == FSTStreamStateInitial, @"Already started"); + + self.state = FSTStreamStateAuth; + + [self.credentials + getTokenForcingRefresh:NO + completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) { + error = [FSTDatastore firestoreErrorForError:error]; + [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ + [self resumeStartWithToken:result error:error]; + }]; + }]; +} + +/** Add an access token to our RPC, after obtaining one from the credentials provider. */ +- (void)resumeStartWithToken:(FSTGetTokenResult *)token error:(NSError *)error { + if (self.state == FSTStreamStateStopped) { + // Streams can be stopped while waiting for authorization. + return; + } + + [self.workerDispatchQueue verifyIsCurrentQueue]; + FSTAssert(self.state == FSTStreamStateAuth, @"State should still be auth (was %ld)", + (long)self.state); + + // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token, + // but I'm not sure how to detect that right now. http://b/32762461 + if (error) { + // RPC has not been started yet, so just invoke higher-level close handler. + [self handleStreamClose:error]; + return; + } + + self.requestsWriter = [[FSTBufferedWriter alloc] init]; + _rpc = [self createRPCWithRequestsWriter:self.requestsWriter]; + [FSTDatastore prepareHeadersForRPC:_rpc + databaseID:self.databaseInfo.databaseID + token:token.token]; + [_rpc startWithWriteable:self]; + + self.state = FSTStreamStateOpen; + [self handleStreamOpen]; +} + +/** Backs off after an error. */ +- (void)performBackoff { + FSTLog(@"%@ %p backoff", NSStringFromClass([self class]), (__bridge void *)self); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + FSTAssert(self.state == FSTStreamStateError, @"Should only perform backoff in an error case"); + self.state = FSTStreamStateBackoff; + + FSTWeakify(self); + [self.backoff backoffAndRunBlock:^{ + FSTStrongify(self); + [self resumeStartFromBackoff]; + }]; +} + +/** Resumes stream start after backing off. */ +- (void)resumeStartFromBackoff { + if (self.state == FSTStreamStateStopped) { + // Streams can be stopped while waiting for backoff to complete. + return; + } + + // In order to have performed a backoff the stream must have been in an error state just prior + // to entering the backoff state. If we weren't stopped we must be in the backoff state. + FSTAssert(self.state == FSTStreamStateBackoff, @"State should still be backoff (was %ld)", + (long)self.state); + + // Momentarily set state to FSTStreamStateInitial as `start` expects it. + self.state = FSTStreamStateInitial; + [self start]; + FSTAssert([self isStarted], @"Stream should have started."); +} + +- (void)stop { + FSTLog(@"%@ %p stop", NSStringFromClass([self class]), (__bridge void *)self); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Prevent any possible future restart of this stream. + self.state = FSTStreamStateStopped; + + // Close the stream client side. + FSTBufferedWriter *requestsWriter = self.requestsWriter; + @synchronized(requestsWriter) { + [requestsWriter finishWithError:nil]; + } +} + +- (void)inhibitBackoff { + FSTAssert(![self isStarted], @"Can only inhibit backoff after an error (was %ld)", + (long)self.state); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // Clear the error condition. + self.state = FSTStreamStateInitial; + [self.backoff reset]; +} + +/** + * Parses a protocol buffer response from the server. If the message fails to parse, generates + * an error and closes the stream. + * + * @param protoClass A protocol buffer message class object, that responds to parseFromData:error:. + * @param data The bytes in the response as returned from GRPC. + * @return An instance of the protocol buffer message, parsed from the data if parsing was + * successful, or nil otherwise. + */ +- (nullable id)parseProto:(Class)protoClass data:(NSData *)data error:(NSError **)error { + NSError *parseError; + id parsed = [protoClass parseFromData:data error:&parseError]; + if (parsed) { + *error = nil; + return parsed; + } else { + NSDictionary *info = @{ + NSLocalizedDescriptionKey : @"Unable to parse response from the server", + NSUnderlyingErrorKey : parseError, + @"Expected class" : protoClass, + @"Received value" : data, + }; + *error = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:FIRFirestoreErrorCodeInternal + userInfo:info]; + return nil; + } +} + +/** + * Writes a request proto into the stream. + */ +- (void)writeRequest:(GPBMessage *)request { + NSData *data = [request data]; + + FSTBufferedWriter *requestsWriter = self.requestsWriter; + @synchronized(requestsWriter) { + [requestsWriter writeValue:data]; + } +} + +#pragma mark Template methods for subclasses + +/** + * Called by the stream after the stream has been successfully connected, authenticated, and is now + * ready to accept messages. + * + * Subclasses should relay to their stream-specific delegate. Calling [super handleStreamOpen] is + * not required. + */ +- (void)handleStreamOpen { +} + +/** + * Called by the stream for each incoming protocol message coming from the server. + * + * Subclasses should implement this to deserialize the value and relay to their stream-specific + * delegate, if appropriate. Calling [super handleStreamMessage] is not required. + */ +- (void)handleStreamMessage:(id)value { +} + +/** + * Called by the stream when the underlying RPC has been closed for whatever reason. + * + * Subclasses should first call [super handleStreamClose:] and then call to their + * stream-specific delegate. + */ +- (void)handleStreamClose:(NSError *_Nullable)error { + FSTLog(@"%@ %p close: %@", NSStringFromClass([self class]), (__bridge void *)self, error); + FSTAssert([self isStarted], @"Can't handle server close in non-started state."); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + self.messageReceived = NO; + self.rpc = nil; + self.requestsWriter = nil; + + // In theory the stream could close cleanly, however, in our current model we never expect this + // to happen because if we stop a stream ourselves, this callback will never be called. To + // prevent cases where we retry without a backoff accidentally, we set the stream to error + // in all cases. + self.state = FSTStreamStateError; + + if (error.code == FIRFirestoreErrorCodeResourceExhausted) { + FSTLog(@"%@ %p Using maximum backoff delay to prevent overloading the backend.", [self class], + (__bridge void *)self); + [self.backoff resetToMax]; + } +} + +#pragma mark GRXWriteable implementation +// The GRXWriteable implementation defines the receive side of the RPC stream. + +/** + * Called by GRPC when it publishes a value. It is called from GRPC's own queue so we immediately + * redispatch back onto our own worker queue. + */ +- (void)writeValue:(id)value __used { + // TODO(mcg): remove the double-dispatch once GRPCCall at head is released. + // Once released we can set the responseDispatchQueue property on the GRPCCall and then this + // method can call handleStreamMessage directly. + FSTWeakify(self); + [self.workerDispatchQueue dispatchAsync:^{ + FSTStrongify(self); + if (!self || self.state == FSTStreamStateStopped) { + return; + } + if (!self.messageReceived) { + self.messageReceived = YES; + if ([FIRFirestore isLoggingEnabled]) { + FSTLog(@"%@ %p headers (whitelisted): %@", NSStringFromClass([self class]), + (__bridge void *)self, + [FSTDatastore extractWhiteListedHeaders:self.rpc.responseHeaders]); + } + } + NSError *error; + id proto = [self parseProto:self.responseMessageClass data:value error:&error]; + if (proto) { + [self handleStreamMessage:proto]; + } else { + [_rpc finishWithError:error]; + } + }]; +} + +/** + * Called by GRPC when it closed the stream with an error representing the final state of the + * stream. + * + * Do not call directly, since it dispatches via the worker queue. Call handleStreamClose to + * directly inform stream-specific logic, or call stop to tear down the stream. + */ +- (void)writesFinishedWithError:(NSError *_Nullable)error __used { + error = [FSTDatastore firestoreErrorForError:error]; + FSTWeakify(self); + [self.workerDispatchQueue dispatchAsync:^{ + FSTStrongify(self); + if (!self || self.state == FSTStreamStateStopped) { + return; + } + [self handleStreamClose:error]; + }]; +} + +@end + +#pragma mark - FSTWatchStream + +@implementation FSTWatchStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id<FSTWatchStreamDelegate>)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:responseMessageClass]; + if (self) { + _delegate = delegate; + } + return self; +} + +- (void)stop { + // Clear the delegate to avoid any possible bleed through of events from GRPC. + self.delegate = nil; + + [super stop]; +} + +- (void)watchQuery:(FSTQueryData *)query { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)unwatchTargetID:(FSTTargetID)targetID { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)handleStreamOpen { + [self.delegate watchStreamDidOpen]; +} + +- (void)handleStreamClose:(NSError *_Nullable)error { + [super handleStreamClose:error]; + [self.delegate watchStreamDidClose:error]; +} + +@end + +#pragma mark - FSTBetaWatchStream + +@implementation FSTBetaWatchStream { + FSTSerializerBeta *_serializer; +} + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + serializer:(FSTSerializerBeta *)serializer + delegate:(id<FSTWatchStreamDelegate>)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[GCFSListenResponse class] + delegate:delegate]; + if (self) { + _serializer = serializer; + } + return self; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + return [[GRPCCall alloc] initWithHost:self.databaseInfo.host + path:@"/google.firestore.v1beta1.Firestore/Listen" + requestsWriter:requestsWriter]; +} + +- (void)watchQuery:(FSTQueryData *)query { + FSTAssert([self isOpen], @"Not yet open"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSListenRequest *request = [GCFSListenRequest message]; + request.database = [_serializer encodedDatabaseID]; + request.addTarget = [_serializer encodedTarget:query]; + request.labels = [_serializer encodedListenRequestLabelsForQueryData:query]; + + FSTLog(@"FSTWatchStream %p watch: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +- (void)unwatchTargetID:(FSTTargetID)targetID { + FSTAssert([self isOpen], @"Not yet open"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSListenRequest *request = [GCFSListenRequest message]; + request.database = [_serializer encodedDatabaseID]; + request.removeTarget = targetID; + + FSTLog(@"FSTWatchStream %p unwatch: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +/** + * Receives an inbound message from GRPC, deserializes, and then passes that on to the delegate's + * watchStreamDidChange:snapshotVersion: callback. + */ +- (void)handleStreamMessage:(GCFSListenResponse *)proto { + FSTLog(@"FSTWatchStream %p response: %@", (__bridge void *)self, proto); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // A successful response means the stream is healthy. + [self.backoff reset]; + + FSTWatchChange *change = [_serializer decodedWatchChange:proto]; + FSTSnapshotVersion *snap = [_serializer versionFromListenResponse:proto]; + [self.delegate watchStreamDidChange:change snapshotVersion:snap]; +} + +@end + +#pragma mark - FSTWriteStream + +@implementation FSTWriteStream + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + responseMessageClass:(Class)responseMessageClass + delegate:(id<FSTWriteStreamDelegate>)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:responseMessageClass]; + if (self) { + _delegate = delegate; + } + return self; +} + +- (void)start { + self.handshakeComplete = NO; + [super start]; +} + +- (void)stop { + // Clear the delegate to avoid any possible bleed through of events from GRPC. + self.delegate = nil; + + [super stop]; +} + +- (void)writeHandshake { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)handleStreamOpen { + [self.delegate writeStreamDidOpen]; +} + +- (void)handleStreamClose:(NSError *_Nullable)error { + [super handleStreamClose:error]; + + [self.delegate writeStreamDidClose:error]; +} + +@end + +#pragma mark - FSTBetaWriteStream + +@implementation FSTBetaWriteStream { + FSTSerializerBeta *_serializer; +} + +- (instancetype)initWithDatabase:(FSTDatabaseInfo *)database + workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue + credentials:(id<FSTCredentialsProvider>)credentials + serializer:(FSTSerializerBeta *)serializer + delegate:(id<FSTWriteStreamDelegate>)delegate { + self = [super initWithDatabase:database + workerDispatchQueue:workerDispatchQueue + credentials:credentials + responseMessageClass:[GCFSWriteResponse class] + delegate:delegate]; + if (self) { + _serializer = serializer; + } + return self; +} + +- (GRPCCall *)createRPCWithRequestsWriter:(GRXWriter *)requestsWriter { + return [[GRPCCall alloc] initWithHost:self.databaseInfo.host + path:@"/google.firestore.v1beta1.Firestore/Write" + requestsWriter:requestsWriter]; +} + +- (void)writeHandshake { + // The initial request cannot contain mutations, but must contain a projectID. + FSTAssert([self isOpen], @"Not yet open"); + FSTAssert(!self.handshakeComplete, @"Handshake sent out of turn"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + GCFSWriteRequest *request = [GCFSWriteRequest message]; + request.database = [_serializer encodedDatabaseID]; + // TODO(dimond): Support stream resumption. We intentionally do not set the stream token on the + // handshake, ignoring any stream token we might have. + + FSTLog(@"FSTWriteStream %p initial request: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +- (void)writeMutations:(NSArray<FSTMutation *> *)mutations { + FSTAssert([self isOpen], @"Not yet open"); + FSTAssert(self.handshakeComplete, @"Mutations sent out of turn"); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + NSMutableArray<GCFSWrite *> *protos = [NSMutableArray arrayWithCapacity:mutations.count]; + for (FSTMutation *mutation in mutations) { + [protos addObject:[_serializer encodedMutation:mutation]]; + }; + + GCFSWriteRequest *request = [GCFSWriteRequest message]; + request.writesArray = protos; + request.streamToken = self.lastStreamToken; + + FSTLog(@"FSTWriteStream %p mutation request: %@", (__bridge void *)self, request); + [self writeRequest:request]; +} + +/** + * Implements GRXWriteable to receive an inbound message from GRPC, deserialize, and then pass + * that on to the mutationResultsHandler. + */ +- (void)handleStreamMessage:(GCFSWriteResponse *)response { + FSTLog(@"FSTWriteStream %p response: %@", (__bridge void *)self, response); + [self.workerDispatchQueue verifyIsCurrentQueue]; + + // A successful response means the stream is healthy. + [self.backoff reset]; + + // Always capture the last stream token. + self.lastStreamToken = response.streamToken; + + if (!self.handshakeComplete) { + // The first response is the handshake response + self.handshakeComplete = YES; + + [self.delegate writeStreamDidCompleteHandshake]; + } else { + FSTSnapshotVersion *commitVersion = [_serializer decodedVersion:response.commitTime]; + NSMutableArray<GCFSWriteResult *> *protos = response.writeResultsArray; + NSMutableArray<FSTMutationResult *> *results = [NSMutableArray arrayWithCapacity:protos.count]; + for (GCFSWriteResult *proto in protos) { + [results addObject:[_serializer decodedMutationResult:proto]]; + }; + + [self.delegate writeStreamDidReceiveResponseWithVersion:commitVersion mutationResults:results]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTExistenceFilter.h b/Firestore/Source/Remote/FSTExistenceFilter.h new file mode 100644 index 0000000..df95950 --- /dev/null +++ b/Firestore/Source/Remote/FSTExistenceFilter.h @@ -0,0 +1,31 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTExistenceFilter : NSObject + ++ (instancetype)filterWithCount:(int32_t)count; + +- (instancetype)init __attribute__((unavailable("Use a static constructor"))); + +@property(nonatomic, assign, readonly) int32_t count; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTExistenceFilter.m b/Firestore/Source/Remote/FSTExistenceFilter.m new file mode 100644 index 0000000..7c0ded2 --- /dev/null +++ b/Firestore/Source/Remote/FSTExistenceFilter.m @@ -0,0 +1,53 @@ +/* + * 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 "FSTExistenceFilter.h" + +@interface FSTExistenceFilter () + +- (instancetype)initWithCount:(int32_t)count NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTExistenceFilter + ++ (instancetype)filterWithCount:(int32_t)count { + return [[FSTExistenceFilter alloc] initWithCount:count]; +} + +- (instancetype)initWithCount:(int32_t)count { + if (self = [super init]) { + _count = count; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTExistenceFilter class]]) { + return NO; + } + + return _count == ((FSTExistenceFilter *)other).count; +} + +- (NSUInteger)hash { + return _count; +} + +@end diff --git a/Firestore/Source/Remote/FSTExponentialBackoff.h b/Firestore/Source/Remote/FSTExponentialBackoff.h new file mode 100644 index 0000000..0bee2bd --- /dev/null +++ b/Firestore/Source/Remote/FSTExponentialBackoff.h @@ -0,0 +1,79 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTDispatchQueue; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Helper to implement exponential backoff. + * + * In general, call -reset after each successful round-trip. Call -backoffAndRunBlock before + * retrying after an error. Each backoffAndRunBlock will increase the delay between retries. + */ +@interface FSTExponentialBackoff : NSObject + +/** + * Creates and returns a helper for running delayed tasks following an exponential backoff curve + * between attempts. + * + * Each delay is made up of a "base" delay which follows the exponential backoff curve, and a + * +/- 50% "jitter" that is calculated and added to the base delay. This prevents clients from + * accidentally synchronizing their delays causing spikes of load to the backend. + * + * @param dispatchQueue The dispatch queue to run tasks on. + * @param initialDelay The initial delay (used as the base delay on the first retry attempt). + * Note that jitter will still be applied, so the actual delay could be as little as + * 0.5*initialDelay. + * @param backoffFactor The multiplier to use to determine the extended base delay after each + * attempt. + * @param maxDelay The maximum base delay after which no further backoff is performed. Note that + * jitter will still be applied, so the actual delay could be as much as 1.5*maxDelay. + */ ++ (instancetype)exponentialBackoffWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + initialDelay:(NSTimeInterval)initialDelay + backoffFactor:(double)backoffFactor + maxDelay:(NSTimeInterval)maxDelay; + +- (instancetype)init + __attribute__((unavailable("Use exponentialBackoffWithDispatchQueue constructor method."))); + +/** + * Resets the backoff delay. + * + * The very next backoffAndRunBlock: will have no delay. If it is called again (i.e. due to an + * error), initialDelay (plus jitter) will be used, and subsequent ones will increase according + * to the backoffFactor. + */ +- (void)reset; + +/** + * Resets the backoff to the maximum delay (e.g. for use after a RESOURCE_EXHAUSTED error). + */ +- (void)resetToMax; + +/** + * Waits for currentDelay seconds, increases the delay and runs the specified block. + * + * @param block The block to run. + */ +- (void)backoffAndRunBlock:(void (^)())block; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTExponentialBackoff.m b/Firestore/Source/Remote/FSTExponentialBackoff.m new file mode 100644 index 0000000..ec21282 --- /dev/null +++ b/Firestore/Source/Remote/FSTExponentialBackoff.m @@ -0,0 +1,97 @@ +/* + * 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 "FSTExponentialBackoff.h" + +#import "FSTDispatchQueue.h" +#import "FSTLogger.h" +#import "FSTUtil.h" + +@interface FSTExponentialBackoff () +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + initialDelay:(NSTimeInterval)initialDelay + backoffFactor:(double)backoffFactor + maxDelay:(NSTimeInterval)maxDelay NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong) FSTDispatchQueue *dispatchQueue; +@property(nonatomic) double backoffFactor; +@property(nonatomic) NSTimeInterval initialDelay; +@property(nonatomic) NSTimeInterval maxDelay; +@property(nonatomic) NSTimeInterval currentBase; +@end + +@implementation FSTExponentialBackoff + +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + initialDelay:(NSTimeInterval)initialDelay + backoffFactor:(double)backoffFactor + maxDelay:(NSTimeInterval)maxDelay { + if (self = [super init]) { + _dispatchQueue = dispatchQueue; + _initialDelay = initialDelay; + _backoffFactor = backoffFactor; + _maxDelay = maxDelay; + + [self reset]; + } + return self; +} + ++ (instancetype)exponentialBackoffWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + initialDelay:(NSTimeInterval)initialDelay + backoffFactor:(double)backoffFactor + maxDelay:(NSTimeInterval)maxDelay { + return [[FSTExponentialBackoff alloc] initWithDispatchQueue:dispatchQueue + initialDelay:initialDelay + backoffFactor:backoffFactor + maxDelay:maxDelay]; +} + +- (void)reset { + _currentBase = 0; +} + +- (void)resetToMax { + _currentBase = _maxDelay; +} + +- (void)backoffAndRunBlock:(void (^)())block { + // First schedule the block using the current base (which may be 0 and should be honored as such). + NSTimeInterval delayWithJitter = _currentBase + [self jitterDelay]; + if (_currentBase > 0) { + FSTLog(@"Backing off for %.2f seconds (base delay: %.2f seconds)", delayWithJitter, + _currentBase); + } + dispatch_time_t delay = + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayWithJitter * NSEC_PER_SEC)); + dispatch_after(delay, self.dispatchQueue.queue, block); + + // Apply backoff factor to determine next delay and ensure it is within bounds. + _currentBase *= _backoffFactor; + if (_currentBase < _initialDelay) { + _currentBase = _initialDelay; + } + if (_currentBase > _maxDelay) { + _currentBase = _maxDelay; + } +} + +/** Returns a random value in the range [-currentBase/2, currentBase/2] */ +- (NSTimeInterval)jitterDelay { + return ([FSTUtil randomDouble] - 0.5) * _currentBase; +} + +@end diff --git a/Firestore/Source/Remote/FSTRemoteEvent.h b/Firestore/Source/Remote/FSTRemoteEvent.h new file mode 100644 index 0000000..939a027 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteEvent.h @@ -0,0 +1,213 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentDictionary.h" +#import "FSTDocumentKeySet.h" +#import "FSTTypes.h" + +@class FSTDocument; +@class FSTDocumentKey; +@class FSTExistenceFilter; +@class FSTMaybeDocument; +@class FSTSnapshotVersion; +@class FSTWatchChange; +@class FSTQueryData; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTargetMapping + +/** + * TargetMapping represents a change to the documents in a query from the server. This can either + * be an incremental Update or a full Reset. + * + * <p>This is an empty abstract class so that all the different kinds of changes can have a common + * base class. + */ +@interface FSTTargetMapping : NSObject +@end + +#pragma mark - FSTResetMapping + +/** The new set of documents to replace the current documents for a target. */ +@interface FSTResetMapping : FSTTargetMapping + +/** + * Creates a new mapping with the keys for the given documents added. This is intended primarily + * for testing. + */ ++ (FSTResetMapping *)mappingWithDocuments:(NSArray<FSTDocument *> *)documents; + +/** The new set of documents for the target. */ +@property(nonatomic, strong, readonly) FSTDocumentKeySet *documents; +@end + +#pragma mark - FSTUpdateMapping + +/** + * A target should update its set of documents with the given added/removed set of documents. + */ +@interface FSTUpdateMapping : FSTTargetMapping + +/** + * Creates a new mapping with the keys for the given documents added. This is intended primarily + * for testing. + */ ++ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added + removedDocuments:(NSArray<FSTDocument *> *)removed; + +- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys; + +/** The documents added to the target. */ +@property(nonatomic, strong, readonly) FSTDocumentKeySet *addedDocuments; +/** The documents removed from the target. */ +@property(nonatomic, strong, readonly) FSTDocumentKeySet *removedDocuments; +@end + +#pragma mark - FSTTargetChange + +/** + * Represents an update to the current status of a target, either explicitly having no new state, or + * the new value to set. Note "current" has special meaning in the RPC protocol that implies that a + * target is both up-to-date and consistent with the rest of the watch stream. + */ +typedef NS_ENUM(NSUInteger, FSTCurrentStatusUpdate) { + /** The current status is not affected and should not be modified */ + FSTCurrentStatusUpdateNone, + /** The target must be marked as no longer "current" */ + FSTCurrentStatusUpdateMarkNotCurrent, + /** The target must be marked as "current" */ + FSTCurrentStatusUpdateMarkCurrent, +}; + +/** + * A part of an FSTRemoteEvent specifying set of changes to a specific target. These changes track + * what documents are currently included in the target as well as the current snapshot version and + * resume token but the actual changes *to* documents are not part of the FSTTargetChange since + * documents may be part of multiple targets. + */ +@interface FSTTargetChange : NSObject + +/** + * Creates a new target change with the given documents. Instances of FSTDocument are considered + * added. Instance of FSTDeletedDocument are considered removed. This is intended primarily for + * testing. + */ ++ (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs + currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate; + +/** + * The new "current" (synced) status of this target. Set to CurrentStatusUpdateNone if the status + * should not be updated. Note "current" has special meaning for in the RPC protocol that implies + * that a target is both up-to-date and consistent with the rest of the watch stream. + */ +@property(nonatomic, assign, readonly) FSTCurrentStatusUpdate currentStatusUpdate; + +/** A set of changes to documents in this target. */ +@property(nonatomic, strong, readonly) FSTTargetMapping *mapping; + +/** + * The snapshot version representing the last state at which this target received a consistent + * snapshot from the backend. + */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** + * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting + * without retransmitting all the data that matches the query. The resume token essentially + * identifies a point in time from which the server should resume sending results. + */ +@property(nonatomic, strong, readonly) NSData *resumeToken; + +@end + +#pragma mark - FSTRemoteEvent + +/** + * An event from the RemoteStore. It is split into targetChanges (changes to the state or the set + * of documents in our watched targets) and documentUpdates (changes to the actual documents). + */ +@interface FSTRemoteEvent : NSObject + ++ (instancetype) +eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges + documentUpdates: + (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates; + +/** The snapshot version this event brings us up to. */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** A map from target to changes to the target. See TargetChange. */ +@property(nonatomic, strong, readonly) + NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges; + +/** + * A set of which documents have changed or been deleted, along with the doc's new values + * (if not deleted). + */ +@property(nonatomic, strong, readonly) + NSDictionary<FSTDocumentKey *, FSTMaybeDocument *> *documentUpdates; + +/** Adds a document update to this remote event */ +- (void)addDocumentUpdate:(FSTMaybeDocument *)document; + +/** Handles an existence filter mismatch */ +- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID; + +@end + +#pragma mark - FSTWatchChangeAggregator + +/** + * A helper class to accumulate watch changes into a FSTRemoteEvent and other target + * information. + */ +@interface FSTWatchChangeAggregator : NSObject + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets + pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The number of pending responses that are being waited on from watch */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingTargetResponses; + +/** Aggregates a watch change into the current state */ +- (void)addWatchChange:(FSTWatchChange *)watchChange; + +/** Aggregates all provided watch changes to the current state in order */ +- (void)addWatchChanges:(NSArray<FSTWatchChange *> *)watchChanges; + +/** + * Converts the current state into a remote event with the snapshot version taken from the + * initializer. + */ +- (FSTRemoteEvent *)remoteEvent; + +/** The existence filters - if any - for the given target IDs. */ +@property(nonatomic, strong, readonly) + NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *existenceFilters; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteEvent.m b/Firestore/Source/Remote/FSTRemoteEvent.m new file mode 100644 index 0000000..5c75998 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteEvent.m @@ -0,0 +1,516 @@ +/* + * 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 "FSTRemoteEvent.h" + +#import "FSTAssert.h" +#import "FSTClasses.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTLogger.h" +#import "FSTSnapshotVersion.h" +#import "FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTTargetMapping + +@interface FSTTargetMapping () + +/** Private mutator method to add a document key to the mapping */ +- (void)addDocumentKey:(FSTDocumentKey *)documentKey; + +/** Private mutator method to remove a document key from the mapping */ +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey; + +@end + +@implementation FSTTargetMapping + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + @throw FSTAbstractMethodException(); // NOLINT +} + +@end + +#pragma mark - FSTResetMapping + +@interface FSTResetMapping () +@property(nonatomic, strong) FSTDocumentKeySet *documents; +@end + +@implementation FSTResetMapping + ++ (instancetype)mappingWithDocuments:(NSArray<FSTDocument *> *)documents { + FSTResetMapping *mapping = [[FSTResetMapping alloc] init]; + for (FSTDocument *doc in documents) { + mapping.documents = [mapping.documents setByAddingObject:doc.key]; + } + return mapping; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _documents = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTResetMapping class]]) { + return NO; + } + + FSTResetMapping *otherMapping = (FSTResetMapping *)other; + return [self.documents isEqual:otherMapping.documents]; +} + +- (NSUInteger)hash { + return self.documents.hash; +} + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + self.documents = [self.documents setByAddingObject:documentKey]; +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + self.documents = [self.documents setByRemovingObject:documentKey]; +} + +@end + +#pragma mark - FSTUpdateMapping + +@interface FSTUpdateMapping () +@property(nonatomic, strong) FSTDocumentKeySet *addedDocuments; +@property(nonatomic, strong) FSTDocumentKeySet *removedDocuments; +@end + +@implementation FSTUpdateMapping + ++ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added + removedDocuments:(NSArray<FSTDocument *> *)removed { + FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + for (FSTDocument *doc in added) { + mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + } + for (FSTDocument *doc in removed) { + mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + } + return mapping; +} + +- (instancetype)init { + self = [super init]; + if (self) { + _addedDocuments = [FSTDocumentKeySet keySet]; + _removedDocuments = [FSTDocumentKeySet keySet]; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTUpdateMapping class]]) { + return NO; + } + + FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other; + return [self.addedDocuments isEqual:otherMapping.addedDocuments] && + [self.removedDocuments isEqual:otherMapping.removedDocuments]; +} + +- (NSUInteger)hash { + return self.addedDocuments.hash * 31 + self.removedDocuments.hash; +} + +- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys { + __block FSTDocumentKeySet *result = keys; + [self.addedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + result = [result setByAddingObject:key]; + }]; + [self.removedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) { + result = [result setByRemovingObject:key]; + }]; + return result; +} + +- (void)addDocumentKey:(FSTDocumentKey *)documentKey { + self.addedDocuments = [self.addedDocuments setByAddingObject:documentKey]; + self.removedDocuments = [self.removedDocuments setByRemovingObject:documentKey]; +} + +- (void)removeDocumentKey:(FSTDocumentKey *)documentKey { + self.addedDocuments = [self.addedDocuments setByRemovingObject:documentKey]; + self.removedDocuments = [self.removedDocuments setByAddingObject:documentKey]; +} + +@end + +#pragma mark - FSTTargetChange + +@interface FSTTargetChange () +@property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate; +@property(nonatomic, strong, nullable) FSTTargetMapping *mapping; +@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; +@property(nonatomic, strong) NSData *resumeToken; +@end + +@implementation FSTTargetChange + +- (instancetype)init { + if (self = [super init]) { + _currentStatusUpdate = FSTCurrentStatusUpdateNone; + _resumeToken = [NSData data]; + } + return self; +} + ++ (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs + currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { + FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init]; + for (FSTMaybeDocument *doc in docs) { + if ([doc isKindOfClass:[FSTDeletedDocument class]]) { + mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key]; + } else { + mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key]; + } + } + FSTTargetChange *change = [[FSTTargetChange alloc] init]; + change.mapping = mapping; + change.currentStatusUpdate = currentStatusUpdate; + return change; +} + ++ (instancetype)changeWithMapping:(FSTTargetMapping *)mapping + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion + currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate { + FSTTargetChange *change = [[FSTTargetChange alloc] init]; + change.mapping = mapping; + change.snapshotVersion = snapshotVersion; + change.currentStatusUpdate = currentStatusUpdate; + return change; +} + +- (FSTTargetMapping *)mapping { + if (!_mapping) { + // Create an FSTUpdateMapping by default, since resets are always explicit + _mapping = [[FSTUpdateMapping alloc] init]; + } + return _mapping; +} + +/** + * Sets the resume token but only when it has a new value. Empty resumeTokens are + * discarded. + */ +- (void)setResumeToken:(NSData *)resumeToken { + if (resumeToken.length > 0) { + _resumeToken = resumeToken; + } +} + +@end + +#pragma mark - FSTRemoteEvent + +@interface FSTRemoteEvent () { + NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *_documentUpdates; + NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges + documentUpdates: + (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates; + +@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion; + +@end + +@implementation FSTRemoteEvent + ++ (instancetype) +eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges + documentUpdates: + (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates { + return [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion + targetChanges:targetChanges + documentUpdates:documentUpdates]; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges + documentUpdates: + (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates { + self = [super init]; + if (self) { + _snapshotVersion = snapshotVersion; + _targetChanges = targetChanges; + _documentUpdates = documentUpdates; + } + return self; +} + +/** Adds a document update to this remote event */ +- (void)addDocumentUpdate:(FSTMaybeDocument *)document { + _documentUpdates[document.key] = document; +} + +/** Handles an existence filter mismatch */ +- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID { + // An existence filter mismatch will reset the query and we need to reset the mapping to contain + // no documents and an empty resume token. + // + // Note: + // * The reset mapping is empty, specifically forcing the consumer of the change to + // forget all keys for this targetID; + // * The resume snapshot for this target must be reset + // * The target must be unacked because unwatching and rewatching introduces a race for + // changes. + // + // TODO(dimond): keep track of reset targets not to raise. + FSTTargetChange *targetChange = + [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init] + snapshotVersion:[FSTSnapshotVersion noVersion] + currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent]; + _targetChanges[targetID] = targetChange; +} + +@end + +#pragma mark - FSTWatchChangeAggregator + +@interface FSTWatchChangeAggregator () + +/** The snapshot version for every target change this creates. */ +@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion; + +/** Keeps track of the current target mappings */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges; + +/** Keeps track of document to update */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *documentUpdates; + +/** The set of open listens on the client */ +@property(nonatomic, strong, readonly) + NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *listenTargets; + +/** Whether this aggregator was frozen and can no longer be modified */ +@property(nonatomic, assign) BOOL frozen; + +@end + +@implementation FSTWatchChangeAggregator { + NSMutableDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *_existenceFilters; +} + +- (instancetype) +initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion + listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets + pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses { + self = [super init]; + if (self) { + _snapshotVersion = snapshotVersion; + + _frozen = NO; + _targetChanges = [NSMutableDictionary dictionary]; + _listenTargets = listenTargets; + _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses]; + + _existenceFilters = [NSMutableDictionary dictionary]; + _documentUpdates = [NSMutableDictionary dictionary]; + } + return self; +} + +- (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID { + FSTTargetChange *change = self.targetChanges[targetID]; + if (!change) { + change = [[FSTTargetChange alloc] init]; + change.snapshotVersion = self.snapshotVersion; + self.targetChanges[targetID] = change; + } + return change; +} + +- (void)addWatchChanges:(NSArray<FSTWatchChange *> *)watchChanges { + FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator"); + for (FSTWatchChange *watchChange in watchChanges) { + [self addWatchChange:watchChange]; + } +} + +- (void)addWatchChange:(FSTWatchChange *)watchChange { + FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator"); + if ([watchChange isKindOfClass:[FSTDocumentWatchChange class]]) { + [self addDocumentChange:(FSTDocumentWatchChange *)watchChange]; + } else if ([watchChange isKindOfClass:[FSTWatchTargetChange class]]) { + [self addTargetChange:(FSTWatchTargetChange *)watchChange]; + } else if ([watchChange isKindOfClass:[FSTExistenceFilterWatchChange class]]) { + [self addExistenceFilterChange:(FSTExistenceFilterWatchChange *)watchChange]; + } else { + FSTFail(@"Unknown watch change: %@", watchChange); + } +} + +- (void)addDocumentChange:(FSTDocumentWatchChange *)docChange { + BOOL relevant = NO; + + for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) { + if ([self isActiveTarget:targetID]) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + [change.mapping addDocumentKey:docChange.documentKey]; + relevant = YES; + } + } + + for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) { + if ([self isActiveTarget:targetID]) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + [change.mapping removeDocumentKey:docChange.documentKey]; + relevant = YES; + } + } + + // Only update the document if there is a new document to replace, this might be just a target + // update instead. + if (docChange.document && relevant) { + self.documentUpdates[docChange.documentKey] = docChange.document; + } +} + +- (void)addTargetChange:(FSTWatchTargetChange *)targetChange { + for (FSTBoxedTargetID *targetID in targetChange.targetIDs) { + FSTTargetChange *change = [self targetChangeForTargetID:targetID]; + switch (targetChange.state) { + case FSTWatchTargetChangeStateNoChange: + if ([self isActiveTarget:targetID]) { + // Creating the change above satisfies the semantics of no-change. + change.resumeToken = targetChange.resumeToken; + } + break; + case FSTWatchTargetChangeStateAdded: + [self recordResponseForTargetID:targetID]; + if (![self.pendingTargetResponses objectForKey:targetID]) { + // We have a freshly added target, so we need to reset any state that we had previously + // This can happen e.g. when remove and add back a target for existence filter + // mismatches. + change.mapping = nil; + change.currentStatusUpdate = FSTCurrentStatusUpdateNone; + [_existenceFilters removeObjectForKey:targetID]; + } + change.resumeToken = targetChange.resumeToken; + break; + case FSTWatchTargetChangeStateRemoved: + // We need to keep track of removed targets to we can post-filter and remove any target + // changes. + [self recordResponseForTargetID:targetID]; + FSTAssert(!targetChange.cause, @"WatchChangeAggregator does not handle errored targets."); + break; + case FSTWatchTargetChangeStateCurrent: + if ([self isActiveTarget:targetID]) { + change.currentStatusUpdate = FSTCurrentStatusUpdateMarkCurrent; + change.resumeToken = targetChange.resumeToken; + } + break; + case FSTWatchTargetChangeStateReset: + if ([self isActiveTarget:targetID]) { + // Overwrite any existing target mapping with a reset mapping. Every subsequent update + // will modify the reset mapping, not an update mapping. + change.mapping = [[FSTResetMapping alloc] init]; + change.resumeToken = targetChange.resumeToken; + } + break; + default: + FSTWarn(@"Unknown target watch change type: %ld", (long)targetChange.state); + } + } +} + +/** + * Records that we got a watch target add/remove by decrementing the number of pending target + * responses that we have. + */ +- (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID { + NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; + int newCount = count ? [count intValue] - 1 : -1; + if (newCount == 0) { + [self.pendingTargetResponses removeObjectForKey:targetID]; + } else { + [self.pendingTargetResponses setObject:[NSNumber numberWithInt:newCount] forKey:targetID]; + } +} + +/** + * Returns true if the given targetId is active. Active targets are those for which there are no + * pending requests to add a listen and are in the current list of targets the client cares about. + * + * Clients can repeatedly listen and stop listening to targets, so this check is useful in + * preventing in preventing race conditions for a target where events arrive but the server hasn't + * yet acknowledged the intended change in state. + */ +- (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID { + return [self.listenTargets objectForKey:targetID] && + ![self.pendingTargetResponses objectForKey:targetID]; +} + +- (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange { + FSTBoxedTargetID *targetID = @(existenceFilterChange.targetID); + if ([self isActiveTarget:targetID]) { + _existenceFilters[targetID] = existenceFilterChange.filter; + } +} + +- (FSTRemoteEvent *)remoteEvent { + NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges = self.targetChanges; + + NSMutableArray *targetsToRemove = [NSMutableArray array]; + + // Apply any inactive targets. + for (FSTBoxedTargetID *targetID in [targetChanges keyEnumerator]) { + if (![self isActiveTarget:targetID]) { + [targetsToRemove addObject:targetID]; + } + } + + [targetChanges removeObjectsForKeys:targetsToRemove]; + + // Mark this aggregator as frozen so no further modifications are made. + self.frozen = YES; + return [FSTRemoteEvent eventWithSnapshotVersion:self.snapshotVersion + targetChanges:targetChanges + documentUpdates:self.documentUpdates]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.h b/Firestore/Source/Remote/FSTRemoteStore.h new file mode 100644 index 0000000..94208e1 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteStore.h @@ -0,0 +1,143 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTDocumentVersionDictionary.h" +#import "FSTTypes.h" + +@class FSTDatabaseInfo; +@class FSTDatastore; +@class FSTDocumentKey; +@class FSTLocalStore; +@class FSTMutationBatch; +@class FSTMutationBatchResult; +@class FSTQuery; +@class FSTQueryData; +@class FSTRemoteEvent; +@class FSTTransaction; +@class FSTUser; + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - FSTRemoteSyncer + +/** + * A protocol that describes the actions the FSTRemoteStore needs to perform on a cooperating + * synchronization engine. + */ +@protocol FSTRemoteSyncer + +/** + * Applies one remote event to the sync engine, notifying any views of the changes, and releasing + * any pending mutation batches that would become visible because of the snapshot version the + * remote event contains. + */ +- (void)applyRemoteEvent:(FSTRemoteEvent *)remoteEvent; + +/** + * Rejects the listen for the given targetID. This can be triggered by the backend for any active + * target. + * + * @param targetID The targetID corresponding to a listen initiated via + * -listenToTargetWithQueryData: on FSTRemoteStore. + * @param error A description of the condition that has forced the rejection. Nearly always this + * will be an indication that the user is no longer authorized to see the data matching the + * target. + */ +- (void)rejectListenWithTargetID:(FSTBoxedTargetID *)targetID error:(NSError *)error; + +/** + * Applies the result of a successful write of a mutation batch to the sync engine, emitting + * snapshots in any views that the mutation applies to, and removing the batch from the mutation + * queue. + */ +- (void)applySuccessfulWriteWithResult:(FSTMutationBatchResult *)batchResult; + +/** + * Rejects the batch, removing the batch from the mutation queue, recomputing the local view of + * any documents affected by the batch and then, emitting snapshots with the reverted value. + */ +- (void)rejectFailedWriteWithBatchID:(FSTBatchID)batchID error:(NSError *)error; + +@end + +/** + * A protocol for the FSTRemoteStore online state delegate, called whenever the state of the + * online streams of the FSTRemoteStore changes. + * Note that this protocol only supports the watch stream for now. + */ +@protocol FSTOnlineStateDelegate <NSObject> + +/** Called whenever the online state of the watch stream changes */ +- (void)watchStreamDidChangeOnlineState:(FSTOnlineState)onlineState; + +@end + +#pragma mark - FSTRemoteStore + +/** + * FSTRemoteStore handles all interaction with the backend through a simple, clean interface. This + * class is not thread safe and should be only called from the worker dispatch queue. + */ +@interface FSTRemoteStore : NSObject + ++ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore; + +- (instancetype)init __attribute__((unavailable("Use static constructor method."))); + +@property(nonatomic, weak) id<FSTRemoteSyncer> syncEngine; + +@property(nonatomic, weak) id<FSTOnlineStateDelegate> onlineStateDelegate; + +/** Starts up the remote store, creating streams, restoring state from LocalStore, etc. */ +- (void)start; + +/** Shuts down the remote store, tearing down connections and otherwise cleaning up. */ +- (void)shutdown; + +/** + * Tells the FSTRemoteStore that the currently authenticated user has changed. + * + * In response the remote store tears down streams and clears up any tracked operations that should + * not persist across users. Restarts the streams if appropriate. + */ +- (void)userDidChange:(FSTUser *)user; + +/** Listens to the target identified by the given FSTQueryData. */ +- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData; + +/** Stops listening to the target with the given target ID. */ +- (void)stopListeningToTargetID:(FSTTargetID)targetID; + +/** + * Tells the FSTRemoteStore that there are new mutations to process in the queue. This is typically + * called by FSTSyncEngine after it has sent mutations to FSTLocalStore. + * + * In response the remote store will pull mutations from the local store until the datastore + * instance reports that it cannot accept further in-progress writes. This mechanism serves to + * maintain a pipeline of in-flight requests between the FSTDatastore and the server that + * applies them. + */ +- (void)fillWritePipeline; + +/** Returns a new transaction backed by this remote store. */ +- (FSTTransaction *)transaction; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTRemoteStore.m b/Firestore/Source/Remote/FSTRemoteStore.m new file mode 100644 index 0000000..cea2ce8 --- /dev/null +++ b/Firestore/Source/Remote/FSTRemoteStore.m @@ -0,0 +1,599 @@ +/* + * 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 "FSTRemoteStore.h" + +#import "FSTAssert.h" +#import "FSTDatastore.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTExistenceFilter.h" +#import "FSTLocalStore.h" +#import "FSTLogger.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTRemoteEvent.h" +#import "FSTSnapshotVersion.h" +#import "FSTTransaction.h" +#import "FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + * The maximum number of pending writes to allow. + * TODO(bjornick): Negotiate this value with the backend. + */ +static const NSUInteger kMaxPendingWrites = 10; + +#pragma mark - FSTRemoteStore + +@interface FSTRemoteStore () <FSTWatchStreamDelegate, FSTWriteStreamDelegate> + +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore NS_DESIGNATED_INITIALIZER; + +/** + * The local store, used to fill the write pipeline with outbound mutations and resolve existence + * filter mismatches. Immutable after initialization. + */ +@property(nonatomic, strong, readonly) FSTLocalStore *localStore; + +/** The client-side proxy for interacting with the backend. Immutable after initialization. */ +@property(nonatomic, strong, readonly) FSTDatastore *datastore; + +#pragma mark Watch Stream +@property(nonatomic, strong, nullable) FSTWatchStream *watchStream; + +/** + * A mapping of watched targets that the client cares about tracking and the + * user has explicitly called a 'listen' for this target. + * + * These targets may or may not have been sent to or acknowledged by the + * server. On re-establishing the listen stream, these targets should be sent + * to the server. The targets removed with unlistens are removed eagerly + * without waiting for confirmation from the listen stream. */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTBoxedTargetID *, FSTQueryData *> *listenTargets; + +/** + * A mapping of targetId to pending acks needed. + * + * If a targetId is present in this map, then we're waiting for watch to + * acknowledge a removal or addition of the target. If a target is not in this + * mapping, and it's in the listenTargets map, then we consider the target to + * be active. + * + * We increment the count here everytime we issue a request over the stream to + * watch or unwatch. We then decrement the count everytime we get a target + * added or target removed message from the server. Once the count is equal to + * 0 we know that the client and server are in the same state (once this state + * is reached the targetId is removed from the map to free the memory). + */ +@property(nonatomic, strong, readonly) + NSMutableDictionary<FSTBoxedTargetID *, NSNumber *> *pendingTargetResponses; + +@property(nonatomic, strong) NSMutableArray<FSTWatchChange *> *accumulatedChanges; +@property(nonatomic, assign) FSTBatchID lastBatchSeen; + +/** + * The online state of the watch stream. The state is set to healthy if and only if there are + * messages received by the backend. + */ +@property(nonatomic, assign) FSTOnlineState watchStreamOnlineState; + +#pragma mark Write Stream +@property(nonatomic, strong, nullable) FSTWriteStream *writeStream; + +/** + * The approximate time the StreamingWrite stream was opened. Used to estimate if stream was + * closed due to an auth expiration (a recoverable error) or some other more permanent error. + */ +@property(nonatomic, strong, nullable) NSDate *writeStreamOpenTime; + +/** + * A FIFO queue of in-flight writes. This is in-flight from the point of view of the caller of + * writeMutations, not from the point of view from the Datastore itself. In particular, these + * requests may not have been sent to the Datastore server if the write stream is not yet running. + */ +@property(nonatomic, strong, readonly) NSMutableArray<FSTMutationBatch *> *pendingWrites; +@end + +@implementation FSTRemoteStore + ++ (instancetype)remoteStoreWithLocalStore:(FSTLocalStore *)localStore + datastore:(FSTDatastore *)datastore { + return [[FSTRemoteStore alloc] initWithLocalStore:localStore datastore:datastore]; +} + +- (instancetype)initWithLocalStore:(FSTLocalStore *)localStore datastore:(FSTDatastore *)datastore { + if (self = [super init]) { + _localStore = localStore; + _datastore = datastore; + _listenTargets = [NSMutableDictionary dictionary]; + _pendingTargetResponses = [NSMutableDictionary dictionary]; + _accumulatedChanges = [NSMutableArray array]; + + _lastBatchSeen = kFSTBatchIDUnknown; + _watchStreamOnlineState = FSTOnlineStateUnknown; + _pendingWrites = [NSMutableArray array]; + } + return self; +} + +- (void)start { + [self setupStreams]; + + // Resume any writes + [self fillWritePipeline]; +} + +- (void)updateAndNotifyAboutOnlineState:(FSTOnlineState)watchStreamOnlineState { + BOOL didChange = (watchStreamOnlineState != self.watchStreamOnlineState); + self.watchStreamOnlineState = watchStreamOnlineState; + if (didChange) { + [self.onlineStateDelegate watchStreamDidChangeOnlineState:watchStreamOnlineState]; + } +} + +- (void)setupStreams { + self.watchStream = [self.datastore createWatchStreamWithDelegate:self]; + self.writeStream = [self.datastore createWriteStreamWithDelegate:self]; + + // Load any saved stream token from persistent storage + self.writeStream.lastStreamToken = [self.localStore lastStreamToken]; +} + +#pragma mark Shutdown + +- (void)shutdown { + FSTLog(@"FSTRemoteStore %p shutting down", (__bridge void *)self); + + self.watchStreamOnlineState = FSTOnlineStateUnknown; + [self cleanupWatchStreamState]; + [self.watchStream stop]; + [self.writeStream stop]; +} + +- (void)userDidChange:(FSTUser *)user { + FSTLog(@"FSTRemoteStore %p changing users: %@", (__bridge void *)self, user); + + // Clear pending writes because those are per-user. Watched targets persist across users so + // don't clear those. + _lastBatchSeen = kFSTBatchIDUnknown; + [self.pendingWrites removeAllObjects]; + + // Stop the streams. They promise not to call us back. + [self.watchStream stop]; + [self.writeStream stop]; + + [self cleanupWatchStreamState]; + + // Create new streams (but note they're not started yet). + [self setupStreams]; + + // If there are any watchedTargets properly handle the stream restart now that FSTRemoteStore + // is ready to handle them. + if ([self shouldStartWatchStream]) { + [self.watchStream start]; + } + + // Resume any writes + [self fillWritePipeline]; + + // User change moves us back to the unknown state because we might not + // want to re-open the stream + [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown]; +} + +#pragma mark Watch Stream + +- (void)listenToTargetWithQueryData:(FSTQueryData *)queryData { + NSNumber *targetKey = @(queryData.targetID); + FSTAssert(!self.listenTargets[targetKey], @"listenToQuery called with duplicate target id: %@", + targetKey); + + self.listenTargets[targetKey] = queryData; + if ([self.watchStream isOpen]) { + [self sendWatchRequestWithQueryData:queryData]; + } else if (![self.watchStream isStarted]) { + [self.watchStream start]; + } +} + +- (void)sendWatchRequestWithQueryData:(FSTQueryData *)queryData { + [self recordPendingRequestForTargetID:@(queryData.targetID)]; + [self.watchStream watchQuery:queryData]; +} + +- (void)stopListeningToTargetID:(FSTTargetID)targetID { + FSTBoxedTargetID *targetKey = @(targetID); + FSTQueryData *queryData = self.listenTargets[targetKey]; + FSTAssert(queryData, @"unlistenToTarget: target not currently watched: %@", targetKey); + + [self.listenTargets removeObjectForKey:targetKey]; + if ([self.watchStream isOpen]) { + [self sendUnwatchRequestForTargetID:targetKey]; + } +} + +- (void)sendUnwatchRequestForTargetID:(FSTBoxedTargetID *)targetID { + [self recordPendingRequestForTargetID:targetID]; + [self.watchStream unwatchTargetID:[targetID intValue]]; +} + +- (void)recordPendingRequestForTargetID:(FSTBoxedTargetID *)targetID { + NSNumber *count = [self.pendingTargetResponses objectForKey:targetID]; + count = @([count intValue] + 1); + [self.pendingTargetResponses setObject:count forKey:targetID]; +} + +/** + * Returns whether the watch stream should be started because there are active targets trying to + * be listened to. + */ +- (BOOL)shouldStartWatchStream { + return self.listenTargets.count > 0; +} + +- (void)cleanupWatchStreamState { + // If the connection is closed then we'll never get a snapshot version for the accumulated + // changes and so we'll never be able to complete the batch. When we start up again the server + // is going to resend these changes anyway, so just toss the accumulated state. + [self.accumulatedChanges removeAllObjects]; + [self.pendingTargetResponses removeAllObjects]; +} + +- (void)watchStreamDidOpen { + // Restore any existing watches. + for (FSTQueryData *queryData in [self.listenTargets objectEnumerator]) { + [self sendWatchRequestWithQueryData:queryData]; + } +} + +- (void)watchStreamDidChange:(FSTWatchChange *)change + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + // Mark the connection as healthy because we got a message from the server. + [self updateAndNotifyAboutOnlineState:FSTOnlineStateHealthy]; + + FSTWatchTargetChange *watchTargetChange = + [change isKindOfClass:[FSTWatchTargetChange class]] ? (FSTWatchTargetChange *)change : nil; + + if (watchTargetChange && watchTargetChange.state == FSTWatchTargetChangeStateRemoved && + watchTargetChange.cause) { + // There was an error on a target, don't wait for a consistent snapshot to raise events + [self processTargetErrorForWatchChange:(FSTWatchTargetChange *)change]; + } else { + // Accumulate watch changes but don't process them if there's no snapshotVersion or it's + // older than a previous snapshot we've processed (can happen after we resume a target + // using a resume token). + [self.accumulatedChanges addObject:change]; + FSTAssert(snapshotVersion, @"snapshotVersion must not be nil."); + if ([snapshotVersion isEqual:[FSTSnapshotVersion noVersion]] || + [snapshotVersion compare:[self.localStore lastRemoteSnapshotVersion]] == + NSOrderedAscending) { + return; + } + + // Create a batch, giving it the accumulatedChanges array. + NSArray<FSTWatchChange *> *changes = self.accumulatedChanges; + self.accumulatedChanges = [NSMutableArray array]; + + [self processBatchedWatchChanges:changes snapshotVersion:snapshotVersion]; + } +} + +- (void)watchStreamDidClose:(NSError *_Nullable)error { + [self cleanupWatchStreamState]; + + // If there was an error, retry the connection. + if ([self shouldStartWatchStream]) { + // If the connection fails before the stream has become healthy, consider the online state + // failed. Otherwise consider the online state unknown and the next connection attempt will + // resolve the online state. For example, if a healthy stream is closed due to an expired token + // we want to have one more try at reconnecting before we consider the connection unhealthy. + if (self.watchStreamOnlineState == FSTOnlineStateHealthy) { + [self updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown]; + } else { + [self updateAndNotifyAboutOnlineState:FSTOnlineStateFailed]; + } + [self.watchStream start]; + } else { + // No need to restart 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 updateAndNotifyAboutOnlineState:FSTOnlineStateUnknown]; + } +} + +/** + * Takes a batch of changes from the Datastore, repackages them as a RemoteEvent, and passes that + * on to the SyncEngine. + */ +- (void)processBatchedWatchChanges:(NSArray<FSTWatchChange *> *)changes + snapshotVersion:(FSTSnapshotVersion *)snapshotVersion { + FSTWatchChangeAggregator *aggregator = + [[FSTWatchChangeAggregator alloc] initWithSnapshotVersion:snapshotVersion + listenTargets:self.listenTargets + pendingTargetResponses:self.pendingTargetResponses]; + [aggregator addWatchChanges:changes]; + FSTRemoteEvent *remoteEvent = [aggregator remoteEvent]; + [self.pendingTargetResponses removeAllObjects]; + [self.pendingTargetResponses setDictionary:aggregator.pendingTargetResponses]; + + // Handle existence filters and existence filter mismatches + [aggregator.existenceFilters enumerateKeysAndObjectsUsingBlock:^(FSTBoxedTargetID *target, + FSTExistenceFilter *filter, + BOOL *stop) { + FSTTargetID targetID = target.intValue; + + FSTQueryData *queryData = self.listenTargets[target]; + FSTQuery *query = queryData.query; + if (!queryData) { + // A watched target might have been removed already. + return; + + } else if ([query isDocumentQuery]) { + if (filter.count == 0) { + // The existence filter told us the document does not exist. + // We need to deduce that this document does not exist and apply a deleted document to our + // updates. Without applying a deleted document there might be another query that will + // raise this document as part of a snapshot until it is resolved, essentially exposing + // inconsistency between queries + FSTDocumentKey *key = [FSTDocumentKey keyWithPath:query.path]; + FSTDeletedDocument *deletedDoc = + [FSTDeletedDocument documentWithKey:key version:snapshotVersion]; + [remoteEvent addDocumentUpdate:deletedDoc]; + } else { + FSTAssert(filter.count == 1, @"Single document existence filter with count: %" PRId32, + filter.count); + } + + } else { + // Not a document query. + FSTDocumentKeySet *trackedRemote = [self.localStore remoteDocumentKeysForTarget:targetID]; + FSTTargetMapping *mapping = remoteEvent.targetChanges[target].mapping; + if (mapping) { + if ([mapping isKindOfClass:[FSTUpdateMapping class]]) { + FSTUpdateMapping *update = (FSTUpdateMapping *)mapping; + trackedRemote = [update applyTo:trackedRemote]; + } else { + FSTAssert([mapping isKindOfClass:[FSTResetMapping class]], + @"Expected either reset or update mapping but got something else %@", mapping); + trackedRemote = ((FSTResetMapping *)mapping).documents; + } + } + + if (trackedRemote.count != (NSUInteger)filter.count) { + FSTLog(@"Existence filter mismatch, resetting mapping"); + + // Make sure the mismatch is exposed in the remote event + [remoteEvent handleExistenceFilterMismatchForTargetID:target]; + + // Clear the resume token for the query, since we're in a known mismatch state. + queryData = + [[FSTQueryData alloc] initWithQuery:query targetID:targetID purpose:queryData.purpose]; + self.listenTargets[target] = queryData; + + // Cause a hard reset by unwatching and rewatching immediately, but deliberately don't + // send a resume token so that we get a full update. + [self sendUnwatchRequestForTargetID:@(targetID)]; + + // Mark the query we send as being on behalf of an existence filter mismatch, but don't + // actually retain that in listenTargets. This ensures that we flag the first re-listen + // this way without impacting future listens of this target (that might happen e.g. on + // reconnect). + FSTQueryData *requestQueryData = + [[FSTQueryData alloc] initWithQuery:query + targetID:targetID + purpose:FSTQueryPurposeExistenceFilterMismatch]; + [self sendWatchRequestWithQueryData:requestQueryData]; + } + } + }]; + + // Update in-memory resume tokens. FSTLocalStore will update the persistent view of these when + // applying the completed FSTRemoteEvent. + [remoteEvent.targetChanges enumerateKeysAndObjectsUsingBlock:^( + FSTBoxedTargetID *target, FSTTargetChange *change, BOOL *stop) { + NSData *resumeToken = change.resumeToken; + if (resumeToken.length > 0) { + FSTQueryData *queryData = _listenTargets[target]; + // A watched target might have been removed already. + if (queryData) { + _listenTargets[target] = + [queryData queryDataByReplacingSnapshotVersion:change.snapshotVersion + resumeToken:resumeToken]; + } + } + }]; + + // Finally handle remote event + [self.syncEngine applyRemoteEvent:remoteEvent]; +} + +/** Process a target error and passes the error along to SyncEngine. */ +- (void)processTargetErrorForWatchChange:(FSTWatchTargetChange *)change { + FSTAssert(change.cause, @"Handling target error without a cause"); + // Ignore targets that have been removed already. + for (FSTBoxedTargetID *targetID in change.targetIDs) { + if (self.listenTargets[targetID]) { + [self.listenTargets removeObjectForKey:targetID]; + [self.syncEngine rejectListenWithTargetID:targetID error:change.cause]; + } + } +} + +#pragma mark Write Stream + +- (void)fillWritePipeline { + while ([self canWriteMutations]) { + FSTMutationBatch *batch = [self.localStore nextMutationBatchAfterBatchID:self.lastBatchSeen]; + if (!batch) { + break; + } + [self commitBatch:batch]; + } +} + +/** + * Returns YES if the backend can accept additional write requests. + * + * When sending mutations to the write stream (e.g. in -fillWritePipeline), call this method first + * to check if more mutations can be sent. + * + * Currently the only thing that can prevent the backend from accepting write requests is if + * there are too many requests already outstanding. As writes complete the backend will be able + * to accept more. + */ +- (BOOL)canWriteMutations { + return self.pendingWrites.count < kMaxPendingWrites; +} + +/** Given mutations to commit, actually commits them to the backend. */ +- (void)commitBatch:(FSTMutationBatch *)batch { + FSTAssert([self canWriteMutations], @"commitBatch called when mutations can't be written"); + self.lastBatchSeen = batch.batchID; + + if (!self.writeStream.isStarted) { + [self.writeStream start]; + } + + [self.pendingWrites addObject:batch]; + + if (self.writeStream.handshakeComplete) { + [self.writeStream writeMutations:batch.mutations]; + } +} + +- (void)writeStreamDidOpen { + self.writeStreamOpenTime = [NSDate date]; + + [self.writeStream writeHandshake]; +} + +/** + * Handles a successful handshake response from the server, which is our cue to send any pending + * writes. + */ +- (void)writeStreamDidCompleteHandshake { + // Record the stream token. + [self.localStore setLastStreamToken:self.writeStream.lastStreamToken]; + + // Drain any pending writes. + // + // Note that at this point pendingWrites contains mutations that have already been accepted by + // fillWritePipeline/commitBatch. If the pipeline is full, canWriteMutations will be NO, despite + // the fact that we actually need to send mutations over. + // + // This also means that this method indirectly respects the limits imposed by canWriteMutations + // since writes can't be added to the pendingWrites array when canWriteMutations is NO. If the + // limits imposed by canWriteMutations actually protect us from DOSing ourselves then those limits + // won't be exceeded here and we'll continue to make progress. + for (FSTMutationBatch *write in self.pendingWrites) { + [self.writeStream writeMutations:write.mutations]; + } +} + +/** Handles a successful StreamingWriteResponse from the server that contains a mutation result. */ +- (void)writeStreamDidReceiveResponseWithVersion:(FSTSnapshotVersion *)commitVersion + mutationResults:(NSArray<FSTMutationResult *> *)results { + // This is a response to a write containing mutations and should be correlated to the first + // pending write. + NSMutableArray *pendingWrites = self.pendingWrites; + FSTMutationBatch *batch = pendingWrites[0]; + [pendingWrites removeObjectAtIndex:0]; + + FSTMutationBatchResult *batchResult = + [FSTMutationBatchResult resultWithBatch:batch + commitVersion:commitVersion + mutationResults:results + streamToken:self.writeStream.lastStreamToken]; + [self.syncEngine applySuccessfulWriteWithResult:batchResult]; + + // It's possible that with the completion of this mutation another slot has freed up. + [self fillWritePipeline]; +} + +/** + * Handles the closing of the StreamingWrite RPC, either because of an error or because the RPC + * has been terminated by the client or the server. + */ +- (void)writeStreamDidClose:(NSError *_Nullable)error { + NSMutableArray *pendingWrites = self.pendingWrites; + // Ignore close if there are no pending writes. + if (pendingWrites.count == 0) { + return; + } + + FSTAssert(error, @"There are pending writes, but the write stream closed without an error."); + if ([FSTDatastore isPermanentWriteError:error]) { + if (self.writeStream.handshakeComplete) { + // This error affects the actual writes. + [self handleWriteError:error]; + } else { + // If there was an error before the handshake finished, it's possible that the server is + // unable to process the stream token we're sending. (Perhaps it's too old?) + [self handleHandshakeError:error]; + } + } + + // The write stream might have been started by refilling the write pipeline for failed writes + if (pendingWrites.count > 0 && !self.writeStream.isStarted) { + [self.writeStream start]; + } +} + +- (void)handleHandshakeError:(NSError *)error { + // Reset the token if it's a permanent error or the error code is ABORTED, signaling the write + // stream is no longer valid. + if ([FSTDatastore isPermanentWriteError:error] || [FSTDatastore isAbortedError:error]) { + NSString *token = [self.writeStream.lastStreamToken base64EncodedStringWithOptions:0]; + FSTLog(@"FSTRemoteStore %p error before completed handshake; resetting stream token %@: %@", + (__bridge void *)self, token, error); + self.writeStream.lastStreamToken = nil; + [self.localStore setLastStreamToken:nil]; + } +} + +- (void)handleWriteError:(NSError *)error { + // Only handle permanent error. If it's transient, just let the retry logic kick in. + if (![FSTDatastore isPermanentWriteError:error]) { + return; + } + + // If this was a permanent error, the request itself was the problem so it's not going to + // succeed if we resend it. + FSTMutationBatch *batch = self.pendingWrites[0]; + [self.pendingWrites removeObjectAtIndex:0]; + + // In this case it's also unlikely that the server itself is melting down--this was just a + // bad request so inhibit backoff on the next restart. + [self.writeStream inhibitBackoff]; + + [self.syncEngine rejectFailedWriteWithBatchID:batch.batchID error:error]; + + // It's possible that with the completion of this mutation another slot has freed up. + [self fillWritePipeline]; +} + +- (FSTTransaction *)transaction { + return [FSTTransaction transactionWithDatastore:self.datastore]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTSerializerBeta.h b/Firestore/Source/Remote/FSTSerializerBeta.h new file mode 100644 index 0000000..973f866 --- /dev/null +++ b/Firestore/Source/Remote/FSTSerializerBeta.h @@ -0,0 +1,110 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FSTDatabaseID; +@class FSTDocumentKey; +@class FSTFieldValue; +@class FSTMaybeDocument; +@class FSTMutation; +@class FSTMutationBatch; +@class FSTMutationResult; +@class FSTObjectValue; +@class FSTQuery; +@class FSTQueryData; +@class FSTSnapshotVersion; +@class FSTTimestamp; +@class FSTWatchChange; + +@class GCFSBatchGetDocumentsResponse; +@class GCFSDocument; +@class GCFSDocumentMask; +@class GCFSListenResponse; +@class GCFSTarget; +@class GCFSTarget_DocumentsTarget; +@class GCFSTarget_QueryTarget; +@class GCFSValue; +@class GCFSWrite; +@class GCFSWriteResult; + +@class GPBTimestamp; + +NS_ASSUME_NONNULL_BEGIN + +/** + * Converts internal model objects to their equivalent protocol buffer form. Methods starting with + * "encoded" convert to a protocol buffer and methods starting with "decoded" convert from a + * protocol buffer. + * + * Throws an exception if a protocol buffer is missing a critical field or has a value we can't + * interpret. + */ +@interface FSTSerializerBeta : NSObject + +- (instancetype)init NS_UNAVAILABLE; + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID NS_DESIGNATED_INITIALIZER; + +- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp; +- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp; + +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version; +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version; + +/** Returns the database ID, such as `projects/{project id}/databases/{database_id}`. */ +- (NSString *)encodedDatabaseID; + +- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key; +- (FSTDocumentKey *)decodedDocumentKey:(NSString *)key; + +- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue; +- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto; + +- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation; +- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation; + +- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation; + +- (nullable NSMutableDictionary<NSString *, NSString *> *)encodedListenRequestLabelsForQueryData: + (FSTQueryData *)queryData; + +- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData; + +- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query; +- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target; + +- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query; +- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target; + +- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange; +- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange; + +- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue key:(FSTDocumentKey *)key; + +/** + * Encodes an FSTObjectValue into a dictionary. + * @return a new dictionary that can be assigned to a field in another proto. + */ +- (NSMutableDictionary<NSString *, GCFSValue *> *)encodedFields:(FSTObjectValue *)value; + +- (FSTObjectValue *)decodedFields:(NSDictionary<NSString *, GCFSValue *> *)fields; + +- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTSerializerBeta.m b/Firestore/Source/Remote/FSTSerializerBeta.m new file mode 100644 index 0000000..418dabd --- /dev/null +++ b/Firestore/Source/Remote/FSTSerializerBeta.m @@ -0,0 +1,1084 @@ +/* + * 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 "FSTSerializerBeta.h" + +#import <GRPCClient/GRPCCall.h> + +#import "Common.pbobjc.h" +#import "Document.pbobjc.h" +#import "Firestore.pbobjc.h" +#import "Latlng.pbobjc.h" +#import "Query.pbobjc.h" +#import "Status.pbobjc.h" +#import "Write.pbobjc.h" + +#import "FIRFirestoreErrors.h" +#import "FIRGeoPoint.h" +#import "FSTAssert.h" +#import "FSTDatabaseID.h" +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTExistenceFilter.h" +#import "FSTFieldValue.h" +#import "FSTMutation.h" +#import "FSTMutationBatch.h" +#import "FSTPath.h" +#import "FSTQuery.h" +#import "FSTQueryData.h" +#import "FSTSnapshotVersion.h" +#import "FSTTimestamp.h" +#import "FSTWatchChange.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTSerializerBeta () +@property(nonatomic, strong, readonly) FSTDatabaseID *databaseID; +@end + +@implementation FSTSerializerBeta + +- (instancetype)initWithDatabaseID:(FSTDatabaseID *)databaseID { + self = [super init]; + if (self) { + _databaseID = databaseID; + } + return self; +} + +#pragma mark - FSTSnapshotVersion <=> GPBTimestamp + +- (GPBTimestamp *)encodedTimestamp:(FSTTimestamp *)timestamp { + GPBTimestamp *result = [GPBTimestamp message]; + result.seconds = timestamp.seconds; + result.nanos = timestamp.nanos; + return result; +} + +- (FSTTimestamp *)decodedTimestamp:(GPBTimestamp *)timestamp { + return [[FSTTimestamp alloc] initWithSeconds:timestamp.seconds nanos:timestamp.nanos]; +} + +- (GPBTimestamp *)encodedVersion:(FSTSnapshotVersion *)version { + return [self encodedTimestamp:version.timestamp]; +} + +- (FSTSnapshotVersion *)decodedVersion:(GPBTimestamp *)version { + return [FSTSnapshotVersion versionWithTimestamp:[self decodedTimestamp:version]]; +} + +#pragma mark - FIRGeoPoint <=> GTPLatLng + +- (GTPLatLng *)encodedGeoPoint:(FIRGeoPoint *)geoPoint { + GTPLatLng *latLng = [GTPLatLng message]; + latLng.latitude = geoPoint.latitude; + latLng.longitude = geoPoint.longitude; + return latLng; +} + +- (FIRGeoPoint *)decodedGeoPoint:(GTPLatLng *)latLng { + return [[FIRGeoPoint alloc] initWithLatitude:latLng.latitude longitude:latLng.longitude]; +} + +#pragma mark - FSTDocumentKey <=> Key proto + +- (NSString *)encodedDocumentKey:(FSTDocumentKey *)key { + return [self encodedResourcePathForDatabaseID:self.databaseID path:key.path]; +} + +- (FSTDocumentKey *)decodedDocumentKey:(NSString *)name { + FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:name]; + FSTAssert([[path segmentAtIndex:1] isEqualToString:self.databaseID.projectID], + @"Tried to deserialize key from different project."); + FSTAssert([[path segmentAtIndex:3] isEqualToString:self.databaseID.databaseID], + @"Tried to deserialize key from different datbase."); + return [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]]; +} + +- (NSString *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID + path:(FSTResourcePath *)path { + return [[[[self encodedResourcePathForDatabaseID:databaseID] pathByAppendingSegment:@"documents"] + pathByAppendingPath:path] canonicalString]; +} + +- (FSTResourcePath *)decodedResourcePathWithDatabaseID:(NSString *)name { + FSTResourcePath *path = [FSTResourcePath pathWithString:name]; + FSTAssert([self validQualifiedResourcePath:path], @"Tried to deserialize invalid key %@", path); + return path; +} + +- (NSString *)encodedQueryPath:(FSTResourcePath *)path { + if (path.length == 0) { + // If the path is empty, the backend requires we leave off the /documents at the end. + return [self encodedDatabaseID]; + } + return [self encodedResourcePathForDatabaseID:self.databaseID path:path]; +} + +- (FSTResourcePath *)decodedQueryPath:(NSString *)name { + FSTResourcePath *resource = [self decodedResourcePathWithDatabaseID:name]; + if (resource.length == 4) { + return [FSTResourcePath pathWithSegments:@[]]; + } else { + return [self localResourcePathForQualifiedResourcePath:resource]; + } +} + +- (FSTResourcePath *)encodedResourcePathForDatabaseID:(FSTDatabaseID *)databaseID { + return [FSTResourcePath + pathWithSegments:@[ @"projects", databaseID.projectID, @"databases", databaseID.databaseID ]]; +} + +- (FSTResourcePath *)localResourcePathForQualifiedResourcePath:(FSTResourcePath *)resourceName { + FSTAssert( + resourceName.length > 4 && [[resourceName segmentAtIndex:4] isEqualToString:@"documents"], + @"Tried to deserialize invalid key %@", resourceName); + return [resourceName pathByRemovingFirstSegments:5]; +} + +- (BOOL)validQualifiedResourcePath:(FSTResourcePath *)path { + return path.length >= 4 && [[path segmentAtIndex:0] isEqualToString:@"projects"] && + [[path segmentAtIndex:2] isEqualToString:@"databases"]; +} + +- (NSString *)encodedDatabaseID { + return [[self encodedResourcePathForDatabaseID:self.databaseID] canonicalString]; +} + +#pragma mark - FSTFieldValue <=> Value proto + +- (GCFSValue *)encodedFieldValue:(FSTFieldValue *)fieldValue { + Class class = [fieldValue class]; + if (class == [FSTNullValue class]) { + return [self encodedNull]; + + } else if (class == [FSTBooleanValue class]) { + return [self encodedBool:[[fieldValue value] boolValue]]; + + } else if (class == [FSTIntegerValue class]) { + return [self encodedInteger:[[fieldValue value] longLongValue]]; + + } else if (class == [FSTDoubleValue class]) { + return [self encodedDouble:[[fieldValue value] doubleValue]]; + + } else if (class == [FSTStringValue class]) { + return [self encodedString:[fieldValue value]]; + + } else if (class == [FSTTimestampValue class]) { + return [self encodedTimestampValue:((FSTTimestampValue *)fieldValue).internalValue]; + + } else if (class == [FSTGeoPointValue class]) { + return [self encodedGeoPointValue:[fieldValue value]]; + + } else if (class == [FSTBlobValue class]) { + return [self encodedBlobValue:[fieldValue value]]; + + } else if (class == [FSTReferenceValue class]) { + FSTReferenceValue *ref = (FSTReferenceValue *)fieldValue; + return [self encodedReferenceValueForDatabaseID:[ref databaseID] key:[ref value]]; + + } else if (class == [FSTObjectValue class]) { + GCFSValue *result = [GCFSValue message]; + result.mapValue = [self encodedMapValue:(FSTObjectValue *)fieldValue]; + return result; + + } else if (class == [FSTArrayValue class]) { + GCFSValue *result = [GCFSValue message]; + result.arrayValue = [self encodedArrayValue:(FSTArrayValue *)fieldValue]; + return result; + + } else { + FSTFail(@"Unhandled type %@ on %@", NSStringFromClass([fieldValue class]), fieldValue); + } +} + +- (FSTFieldValue *)decodedFieldValue:(GCFSValue *)valueProto { + switch (valueProto.valueTypeOneOfCase) { + case GCFSValue_ValueType_OneOfCase_NullValue: + return [FSTNullValue nullValue]; + + case GCFSValue_ValueType_OneOfCase_BooleanValue: + return [FSTBooleanValue booleanValue:valueProto.booleanValue]; + + case GCFSValue_ValueType_OneOfCase_IntegerValue: + return [FSTIntegerValue integerValue:valueProto.integerValue]; + + case GCFSValue_ValueType_OneOfCase_DoubleValue: + return [FSTDoubleValue doubleValue:valueProto.doubleValue]; + + case GCFSValue_ValueType_OneOfCase_StringValue: + return [FSTStringValue stringValue:valueProto.stringValue]; + + case GCFSValue_ValueType_OneOfCase_TimestampValue: + return [FSTTimestampValue timestampValue:[self decodedTimestamp:valueProto.timestampValue]]; + + case GCFSValue_ValueType_OneOfCase_GeoPointValue: + return [FSTGeoPointValue geoPointValue:[self decodedGeoPoint:valueProto.geoPointValue]]; + + case GCFSValue_ValueType_OneOfCase_BytesValue: + return [FSTBlobValue blobValue:valueProto.bytesValue]; + + case GCFSValue_ValueType_OneOfCase_ReferenceValue: + return [self decodedReferenceValue:valueProto.referenceValue]; + + case GCFSValue_ValueType_OneOfCase_ArrayValue: + return [self decodedArrayValue:valueProto.arrayValue]; + + case GCFSValue_ValueType_OneOfCase_MapValue: + return [self decodedMapValue:valueProto.mapValue]; + + default: + FSTFail(@"Unhandled type %d on %@", valueProto.valueTypeOneOfCase, valueProto); + } +} + +- (GCFSValue *)encodedNull { + GCFSValue *result = [GCFSValue message]; + result.nullValue = GPBNullValue_NullValue; + return result; +} + +- (GCFSValue *)encodedBool:(BOOL)value { + GCFSValue *result = [GCFSValue message]; + result.booleanValue = value; + return result; +} + +- (GCFSValue *)encodedDouble:(double)value { + GCFSValue *result = [GCFSValue message]; + result.doubleValue = value; + return result; +} + +- (GCFSValue *)encodedInteger:(int64_t)value { + GCFSValue *result = [GCFSValue message]; + result.integerValue = value; + return result; +} + +- (GCFSValue *)encodedString:(NSString *)value { + GCFSValue *result = [GCFSValue message]; + result.stringValue = value; + return result; +} + +- (GCFSValue *)encodedTimestampValue:(FSTTimestamp *)value { + GCFSValue *result = [GCFSValue message]; + result.timestampValue = [self encodedTimestamp:value]; + return result; +} + +- (GCFSValue *)encodedGeoPointValue:(FIRGeoPoint *)value { + GCFSValue *result = [GCFSValue message]; + result.geoPointValue = [self encodedGeoPoint:value]; + return result; +} + +- (GCFSValue *)encodedBlobValue:(NSData *)value { + GCFSValue *result = [GCFSValue message]; + result.bytesValue = value; + return result; +} + +- (GCFSValue *)encodedReferenceValueForDatabaseID:(FSTDatabaseID *)databaseID + key:(FSTDocumentKey *)key { + GCFSValue *result = [GCFSValue message]; + result.referenceValue = [self encodedResourcePathForDatabaseID:databaseID path:key.path]; + return result; +} + +- (FSTReferenceValue *)decodedReferenceValue:(NSString *)resourceName { + FSTResourcePath *path = [self decodedResourcePathWithDatabaseID:resourceName]; + NSString *project = [path segmentAtIndex:1]; + NSString *database = [path segmentAtIndex:3]; + FSTDatabaseID *databaseID = [FSTDatabaseID databaseIDWithProject:project database:database]; + FSTDocumentKey *key = + [FSTDocumentKey keyWithPath:[self localResourcePathForQualifiedResourcePath:path]]; + return [FSTReferenceValue referenceValue:key databaseID:databaseID]; +} + +- (GCFSArrayValue *)encodedArrayValue:(FSTArrayValue *)arrayValue { + GCFSArrayValue *proto = [GCFSArrayValue message]; + NSMutableArray<GCFSValue *> *protoContents = [proto valuesArray]; + + [[arrayValue internalValue] + enumerateObjectsUsingBlock:^(FSTFieldValue *value, NSUInteger idx, BOOL *stop) { + GCFSValue *converted = [self encodedFieldValue:value]; + [protoContents addObject:converted]; + }]; + return proto; +} + +- (FSTArrayValue *)decodedArrayValue:(GCFSArrayValue *)arrayValue { + NSMutableArray<FSTFieldValue *> *contents = + [NSMutableArray arrayWithCapacity:arrayValue.valuesArray_Count]; + + [arrayValue.valuesArray + enumerateObjectsUsingBlock:^(GCFSValue *value, NSUInteger idx, BOOL *stop) { + [contents addObject:[self decodedFieldValue:value]]; + }]; + return [[FSTArrayValue alloc] initWithValueNoCopy:contents]; +} + +- (GCFSMapValue *)encodedMapValue:(FSTObjectValue *)value { + GCFSMapValue *result = [GCFSMapValue message]; + result.fields = [self encodedFields:value]; + return result; +} + +- (FSTObjectValue *)decodedMapValue:(GCFSMapValue *)map { + return [self decodedFields:map.fields]; +} + +/** + * Encodes an FSTObjectValue into a dictionary. + * @return a new dictionary that can be assigned to a field in another proto. + */ +- (NSMutableDictionary<NSString *, GCFSValue *> *)encodedFields:(FSTObjectValue *)value { + FSTImmutableSortedDictionary<NSString *, FSTFieldValue *> *fields = value.internalValue; + NSMutableDictionary<NSString *, GCFSValue *> *result = [NSMutableDictionary dictionary]; + [fields enumerateKeysAndObjectsUsingBlock:^(NSString *key, FSTFieldValue *obj, BOOL *stop) { + GCFSValue *converted = [self encodedFieldValue:obj]; + result[key] = converted; + }]; + return result; +} + +- (FSTObjectValue *)decodedFields:(NSDictionary<NSString *, GCFSValue *> *)fields { + __block FSTObjectValue *result = [FSTObjectValue objectValue]; + [fields enumerateKeysAndObjectsUsingBlock:^(NSString *_Nonnull key, GCFSValue *_Nonnull obj, + BOOL *_Nonnull stop) { + FSTFieldPath *path = [FSTFieldPath pathWithSegments:@[ key ]]; + FSTFieldValue *value = [self decodedFieldValue:obj]; + result = [result objectBySettingValue:value forPath:path]; + }]; + return result; +} + +#pragma mark - FSTObjectValue <=> Document proto + +- (GCFSDocument *)encodedDocumentWithFields:(FSTObjectValue *)objectValue + key:(FSTDocumentKey *)key { + GCFSDocument *proto = [GCFSDocument message]; + proto.name = [self encodedDocumentKey:key]; + proto.fields = [self encodedFields:objectValue]; + return proto; +} + +#pragma mark - FSTMaybeDocument <= BatchGetDocumentsResponse proto + +- (FSTMaybeDocument *)decodedMaybeDocumentFromBatch:(GCFSBatchGetDocumentsResponse *)response { + switch (response.resultOneOfCase) { + case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Found: + return [self decodedFoundDocument:response]; + case GCFSBatchGetDocumentsResponse_Result_OneOfCase_Missing: + return [self decodedDeletedDocument:response]; + default: + FSTFail(@"Unknown document type: %@", response); + } +} + +- (FSTDocument *)decodedFoundDocument:(GCFSBatchGetDocumentsResponse *)response { + FSTAssert(!!response.found, @"Tried to deserialize a found document from a deleted document."); + FSTDocumentKey *key = [self decodedDocumentKey:response.found.name]; + FSTObjectValue *value = [self decodedFields:response.found.fields]; + FSTSnapshotVersion *version = [self decodedVersion:response.found.updateTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a document response with no snapshot version"); + + return [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; +} + +- (FSTDeletedDocument *)decodedDeletedDocument:(GCFSBatchGetDocumentsResponse *)response { + FSTAssert(!!response.missing, @"Tried to deserialize a deleted document from a found document."); + FSTDocumentKey *key = [self decodedDocumentKey:response.missing]; + FSTSnapshotVersion *version = [self decodedVersion:response.readTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a no document response with no snapshot version"); + return [FSTDeletedDocument documentWithKey:key version:version]; +} + +#pragma mark - FSTMutation => GCFSWrite proto + +- (GCFSWrite *)encodedMutation:(FSTMutation *)mutation { + GCFSWrite *proto = [GCFSWrite message]; + + Class mutationClass = [mutation class]; + if (mutationClass == [FSTSetMutation class]) { + FSTSetMutation *set = (FSTSetMutation *)mutation; + proto.update = [self encodedDocumentWithFields:set.value key:set.key]; + + } else if (mutationClass == [FSTPatchMutation class]) { + FSTPatchMutation *patch = (FSTPatchMutation *)mutation; + proto.update = [self encodedDocumentWithFields:patch.value key:patch.key]; + proto.updateMask = [self encodedFieldMask:patch.fieldMask]; + + } else if (mutationClass == [FSTTransformMutation class]) { + FSTTransformMutation *transform = (FSTTransformMutation *)mutation; + + proto.transform = [GCFSDocumentTransform message]; + proto.transform.document = [self encodedDocumentKey:transform.key]; + proto.transform.fieldTransformsArray = [self encodedFieldTransforms:transform.fieldTransforms]; + // NOTE: We set a precondition of exists: true as a safety-check, since we always combine + // FSTTransformMutations with an FSTSetMutation or FSTPatchMutation which (if successful) should + // end up with an existing document. + proto.currentDocument.exists = YES; + + } else if (mutationClass == [FSTDeleteMutation class]) { + FSTDeleteMutation *delete = (FSTDeleteMutation *)mutation; + proto.delete_p = [self encodedDocumentKey:delete.key]; + + } else { + FSTFail(@"Unknown mutation type %@", NSStringFromClass(mutationClass)); + } + + if (!mutation.precondition.isNone) { + proto.currentDocument = [self encodedPrecondition:mutation.precondition]; + } + + return proto; +} + +- (FSTMutation *)decodedMutation:(GCFSWrite *)mutation { + FSTPrecondition *precondition = [mutation hasCurrentDocument] + ? [self decodedPrecondition:mutation.currentDocument] + : [FSTPrecondition none]; + + switch (mutation.operationOneOfCase) { + case GCFSWrite_Operation_OneOfCase_Update: + if (mutation.hasUpdateMask) { + return [[FSTPatchMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name] + fieldMask:[self decodedFieldMask:mutation.updateMask] + value:[self decodedFields:mutation.update.fields] + precondition:precondition]; + } else { + return [[FSTSetMutation alloc] initWithKey:[self decodedDocumentKey:mutation.update.name] + value:[self decodedFields:mutation.update.fields] + precondition:precondition]; + } + + case GCFSWrite_Operation_OneOfCase_Delete_p: + return [[FSTDeleteMutation alloc] initWithKey:[self decodedDocumentKey:mutation.delete_p] + precondition:precondition]; + + case GCFSWrite_Operation_OneOfCase_Transform: { + FSTPreconditionExists exists = precondition.exists; + FSTAssert(exists == FSTPreconditionExistsYes, + @"Transforms must have precondition \"exists == true\""); + + return [[FSTTransformMutation alloc] + initWithKey:[self decodedDocumentKey:mutation.transform.document] + fieldTransforms:[self decodedFieldTransforms:mutation.transform.fieldTransformsArray]]; + } + + default: + // Note that insert is intentionally unhandled, since we don't ever deal in them. + FSTFail(@"Unknown mutation operation: %d", mutation.operationOneOfCase); + } +} + +- (GCFSPrecondition *)encodedPrecondition:(FSTPrecondition *)precondition { + FSTAssert(!precondition.isNone, @"Can't serialize an empty precondition"); + GCFSPrecondition *message = [GCFSPrecondition message]; + if (precondition.updateTime) { + message.updateTime = [self encodedVersion:precondition.updateTime]; + } else if (precondition.exists != FSTPreconditionExistsNotSet) { + message.exists = precondition.exists == FSTPreconditionExistsYes; + } else { + FSTFail(@"Unknown precondition: %@", precondition); + } + return message; +} + +- (FSTPrecondition *)decodedPrecondition:(GCFSPrecondition *)precondition { + switch (precondition.conditionTypeOneOfCase) { + case GCFSPrecondition_ConditionType_OneOfCase_GPBUnsetOneOfCase: + return [FSTPrecondition none]; + + case GCFSPrecondition_ConditionType_OneOfCase_Exists: + return [FSTPrecondition preconditionWithExists:precondition.exists]; + + case GCFSPrecondition_ConditionType_OneOfCase_UpdateTime: + return [FSTPrecondition + preconditionWithUpdateTime:[self decodedVersion:precondition.updateTime]]; + + default: + FSTFail(@"Unrecognized Precondition one-of case %@", precondition); + } +} + +- (GCFSDocumentMask *)encodedFieldMask:(FSTFieldMask *)fieldMask { + GCFSDocumentMask *mask = [GCFSDocumentMask message]; + for (FSTFieldPath *field in fieldMask.fields) { + [mask.fieldPathsArray addObject:field.canonicalString]; + } + return mask; +} + +- (FSTFieldMask *)decodedFieldMask:(GCFSDocumentMask *)fieldMask { + NSMutableArray<FSTFieldPath *> *fields = + [NSMutableArray arrayWithCapacity:fieldMask.fieldPathsArray_Count]; + for (NSString *path in fieldMask.fieldPathsArray) { + [fields addObject:[FSTFieldPath pathWithServerFormat:path]]; + } + return [[FSTFieldMask alloc] initWithFields:fields]; +} + +- (NSMutableArray<GCFSDocumentTransform_FieldTransform *> *)encodedFieldTransforms: + (NSArray<FSTFieldTransform *> *)fieldTransforms { + NSMutableArray *protos = [NSMutableArray array]; + for (FSTFieldTransform *fieldTransform in fieldTransforms) { + FSTAssert([fieldTransform.transform isKindOfClass:[FSTServerTimestampTransform class]], + @"Unknown transform: %@", fieldTransform.transform); + GCFSDocumentTransform_FieldTransform *proto = [GCFSDocumentTransform_FieldTransform message]; + proto.fieldPath = fieldTransform.path.canonicalString; + proto.setToServerValue = GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime; + [protos addObject:proto]; + } + return protos; +} + +- (NSArray<FSTFieldTransform *> *)decodedFieldTransforms: + (NSArray<GCFSDocumentTransform_FieldTransform *> *)protos { + NSMutableArray<FSTFieldTransform *> *fieldTransforms = [NSMutableArray array]; + for (GCFSDocumentTransform_FieldTransform *proto in protos) { + FSTAssert( + proto.setToServerValue == GCFSDocumentTransform_FieldTransform_ServerValue_RequestTime, + @"Unknown transform setToServerValue: %d", proto.setToServerValue); + [fieldTransforms + addObject:[[FSTFieldTransform alloc] + initWithPath:[FSTFieldPath pathWithServerFormat:proto.fieldPath] + transform:[FSTServerTimestampTransform serverTimestampTransform]]]; + } + return fieldTransforms; +} + +#pragma mark - FSTMutationResult <= GCFSWriteResult proto + +- (FSTMutationResult *)decodedMutationResult:(GCFSWriteResult *)mutation { + // NOTE: Deletes don't have an updateTime. + FSTSnapshotVersion *_Nullable version = + mutation.updateTime ? [self decodedVersion:mutation.updateTime] : nil; + NSMutableArray *_Nullable transformResults = nil; + if (mutation.transformResultsArray.count > 0) { + transformResults = [NSMutableArray array]; + for (GCFSValue *result in mutation.transformResultsArray) { + [transformResults addObject:[self decodedFieldValue:result]]; + } + } + return [[FSTMutationResult alloc] initWithVersion:version transformResults:transformResults]; +} + +#pragma mark - FSTQueryData => GCFSTarget proto + +- (nullable NSMutableDictionary<NSString *, NSString *> *)encodedListenRequestLabelsForQueryData: + (FSTQueryData *)queryData { + NSString *value = [self encodedLabelForPurpose:queryData.purpose]; + if (!value) { + return nil; + } + + NSMutableDictionary<NSString *, NSString *> *result = + [NSMutableDictionary dictionaryWithCapacity:1]; + [result setObject:value forKey:@"goog-listen-tags"]; + return result; +} + +- (nullable NSString *)encodedLabelForPurpose:(FSTQueryPurpose)purpose { + switch (purpose) { + case FSTQueryPurposeListen: + return nil; + case FSTQueryPurposeExistenceFilterMismatch: + return @"existence-filter-mismatch"; + case FSTQueryPurposeLimboResolution: + return @"limbo-document"; + default: + FSTFail(@"Unrecognized query purpose: %lu", (unsigned long)purpose); + } +} + +- (GCFSTarget *)encodedTarget:(FSTQueryData *)queryData { + GCFSTarget *result = [GCFSTarget message]; + FSTQuery *query = queryData.query; + + if ([query isDocumentQuery]) { + result.documents = [self encodedDocumentsTarget:query]; + } else { + result.query = [self encodedQueryTarget:query]; + } + + result.targetId = queryData.targetID; + if (queryData.resumeToken.length > 0) { + result.resumeToken = queryData.resumeToken; + } + + return result; +} + +- (GCFSTarget_DocumentsTarget *)encodedDocumentsTarget:(FSTQuery *)query { + GCFSTarget_DocumentsTarget *result = [GCFSTarget_DocumentsTarget message]; + NSMutableArray<NSString *> *docs = result.documentsArray; + [docs addObject:[self encodedQueryPath:query.path]]; + return result; +} + +- (FSTQuery *)decodedQueryFromDocumentsTarget:(GCFSTarget_DocumentsTarget *)target { + NSArray<NSString *> *documents = target.documentsArray; + FSTAssert(documents.count == 1, @"DocumentsTarget contained other than 1 document %lu", + (unsigned long)documents.count); + + NSString *name = documents[0]; + return [FSTQuery queryWithPath:[self decodedQueryPath:name]]; +} + +- (GCFSTarget_QueryTarget *)encodedQueryTarget:(FSTQuery *)query { + // Dissect the path into parent, collectionId, and optional key filter. + GCFSTarget_QueryTarget *queryTarget = [GCFSTarget_QueryTarget message]; + if (query.path.length == 0) { + queryTarget.parent = [self encodedQueryPath:query.path]; + } else { + FSTResourcePath *path = query.path; + FSTAssert(path.length % 2 != 0, @"Document queries with filters are not supported."); + queryTarget.parent = [self encodedQueryPath:[path pathByRemovingLastSegment]]; + GCFSStructuredQuery_CollectionSelector *from = [GCFSStructuredQuery_CollectionSelector message]; + from.collectionId = path.lastSegment; + [queryTarget.structuredQuery.fromArray addObject:from]; + } + + // Encode the filters. + GCFSStructuredQuery_Filter *_Nullable where = [self encodedFilters:query.filters]; + if (where) { + queryTarget.structuredQuery.where = where; + } + + NSArray<GCFSStructuredQuery_Order *> *orders = [self encodedSortOrders:query.sortOrders]; + if (orders.count) { + [queryTarget.structuredQuery.orderByArray addObjectsFromArray:orders]; + } + + if (query.limit != NSNotFound) { + queryTarget.structuredQuery.limit.value = (int32_t)query.limit; + } + + if (query.startAt) { + queryTarget.structuredQuery.startAt = [self encodedBound:query.startAt]; + } + + if (query.endAt) { + queryTarget.structuredQuery.endAt = [self encodedBound:query.endAt]; + } + + return queryTarget; +} + +- (FSTQuery *)decodedQueryFromQueryTarget:(GCFSTarget_QueryTarget *)target { + FSTResourcePath *path = [self decodedQueryPath:target.parent]; + + GCFSStructuredQuery *query = target.structuredQuery; + NSUInteger fromCount = query.fromArray_Count; + if (fromCount > 0) { + FSTAssert(fromCount == 1, + @"StructuredQuery.from with more than one collection is not supported."); + + GCFSStructuredQuery_CollectionSelector *from = query.fromArray[0]; + path = [path pathByAppendingSegment:from.collectionId]; + } + + NSArray<id<FSTFilter>> *filterBy; + if (query.hasWhere) { + filterBy = [self decodedFilters:query.where]; + } else { + filterBy = @[]; + } + + NSArray<FSTSortOrder *> *orderBy; + if (query.orderByArray_Count > 0) { + orderBy = [self decodedSortOrders:query.orderByArray]; + } else { + orderBy = @[]; + } + + NSInteger limit = NSNotFound; + if (query.hasLimit) { + limit = query.limit.value; + } + + FSTBound *_Nullable startAt; + if (query.hasStartAt) { + startAt = [self decodedBound:query.startAt]; + } + + FSTBound *_Nullable endAt; + if (query.hasEndAt) { + endAt = [self decodedBound:query.endAt]; + } + + return [[FSTQuery alloc] initWithPath:path + filterBy:filterBy + orderBy:orderBy + limit:limit + startAt:startAt + endAt:endAt]; +} + +#pragma mark Filters + +- (GCFSStructuredQuery_Filter *_Nullable)encodedFilters:(NSArray<id<FSTFilter>> *)filters { + if (filters.count == 0) { + return nil; + } + NSMutableArray<GCFSStructuredQuery_Filter *> *protos = [NSMutableArray array]; + for (id<FSTFilter> filter in filters) { + if ([filter isKindOfClass:[FSTRelationFilter class]]) { + [protos addObject:[self encodedRelationFilter:filter]]; + } else { + [protos addObject:[self encodedUnaryFilter:filter]]; + } + } + if (protos.count == 1) { + // Special case: no existing filters and we only need to add one filter. This can be made the + // single root filter without a composite filter. + return protos[0]; + } + GCFSStructuredQuery_Filter *composite = [GCFSStructuredQuery_Filter message]; + composite.compositeFilter.op = GCFSStructuredQuery_CompositeFilter_Operator_And; + composite.compositeFilter.filtersArray = protos; + return composite; +} + +- (NSArray<id<FSTFilter>> *)decodedFilters:(GCFSStructuredQuery_Filter *)proto { + NSMutableArray<id<FSTFilter>> *result = [NSMutableArray array]; + + NSArray<GCFSStructuredQuery_Filter *> *filters; + if (proto.filterTypeOneOfCase == + GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter) { + FSTAssert(proto.compositeFilter.op == GCFSStructuredQuery_CompositeFilter_Operator_And, + @"Only AND-type composite filters are supported, got %d", proto.compositeFilter.op); + filters = proto.compositeFilter.filtersArray; + } else { + filters = @[ proto ]; + } + + for (GCFSStructuredQuery_Filter *filter in filters) { + switch (filter.filterTypeOneOfCase) { + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_CompositeFilter: + FSTFail(@"Nested composite filters are not supported"); + + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_FieldFilter: + [result addObject:[self decodedRelationFilter:filter.fieldFilter]]; + break; + + case GCFSStructuredQuery_Filter_FilterType_OneOfCase_UnaryFilter: + [result addObject:[self decodedUnaryFilter:filter.unaryFilter]]; + break; + + default: + FSTFail(@"Unrecognized Filter.filterType %d", filter.filterTypeOneOfCase); + } + } + return result; +} + +- (GCFSStructuredQuery_Filter *)encodedRelationFilter:(FSTRelationFilter *)filter { + GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message]; + GCFSStructuredQuery_FieldFilter *fieldFilter = proto.fieldFilter; + fieldFilter.field = [self encodedFieldPath:filter.field]; + fieldFilter.op = [self encodedRelationFilterOperator:filter.filterOperator]; + fieldFilter.value = [self encodedFieldValue:filter.value]; + return proto; +} + +- (FSTRelationFilter *)decodedRelationFilter:(GCFSStructuredQuery_FieldFilter *)proto { + FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + FSTRelationFilterOperator filterOperator = [self decodedRelationFilterOperator:proto.op]; + FSTFieldValue *value = [self decodedFieldValue:proto.value]; + return [FSTRelationFilter filterWithField:fieldPath filterOperator:filterOperator value:value]; +} + +- (GCFSStructuredQuery_Filter *)encodedUnaryFilter:(id<FSTFilter>)filter { + GCFSStructuredQuery_Filter *proto = [GCFSStructuredQuery_Filter message]; + proto.unaryFilter.field = [self encodedFieldPath:filter.field]; + if ([filter isKindOfClass:[FSTNanFilter class]]) { + proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNan; + } else if ([filter isKindOfClass:[FSTNullFilter class]]) { + proto.unaryFilter.op = GCFSStructuredQuery_UnaryFilter_Operator_IsNull; + } else { + FSTFail(@"Unrecognized filter: %@", filter); + } + return proto; +} + +- (id<FSTFilter>)decodedUnaryFilter:(GCFSStructuredQuery_UnaryFilter *)proto { + FSTFieldPath *field = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + switch (proto.op) { + case GCFSStructuredQuery_UnaryFilter_Operator_IsNan: + return [[FSTNanFilter alloc] initWithField:field]; + + case GCFSStructuredQuery_UnaryFilter_Operator_IsNull: + return [[FSTNullFilter alloc] initWithField:field]; + + default: + FSTFail(@"Unrecognized UnaryFilter.operator %d", proto.op); + } +} + +- (GCFSStructuredQuery_FieldReference *)encodedFieldPath:(FSTFieldPath *)fieldPath { + GCFSStructuredQuery_FieldReference *ref = [GCFSStructuredQuery_FieldReference message]; + ref.fieldPath = fieldPath.canonicalString; + return ref; +} + +- (GCFSStructuredQuery_FieldFilter_Operator)encodedRelationFilterOperator: + (FSTRelationFilterOperator)filterOperator { + switch (filterOperator) { + case FSTRelationFilterOperatorLessThan: + return GCFSStructuredQuery_FieldFilter_Operator_LessThan; + case FSTRelationFilterOperatorLessThanOrEqual: + return GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual; + case FSTRelationFilterOperatorEqual: + return GCFSStructuredQuery_FieldFilter_Operator_Equal; + case FSTRelationFilterOperatorGreaterThanOrEqual: + return GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual; + case FSTRelationFilterOperatorGreaterThan: + return GCFSStructuredQuery_FieldFilter_Operator_GreaterThan; + default: + FSTFail(@"Unhandled FSTRelationFilterOperator: %ld", (long)filterOperator); + } +} + +- (FSTRelationFilterOperator)decodedRelationFilterOperator: + (GCFSStructuredQuery_FieldFilter_Operator)filterOperator { + switch (filterOperator) { + case GCFSStructuredQuery_FieldFilter_Operator_LessThan: + return FSTRelationFilterOperatorLessThan; + case GCFSStructuredQuery_FieldFilter_Operator_LessThanOrEqual: + return FSTRelationFilterOperatorLessThanOrEqual; + case GCFSStructuredQuery_FieldFilter_Operator_Equal: + return FSTRelationFilterOperatorEqual; + case GCFSStructuredQuery_FieldFilter_Operator_GreaterThanOrEqual: + return FSTRelationFilterOperatorGreaterThanOrEqual; + case GCFSStructuredQuery_FieldFilter_Operator_GreaterThan: + return FSTRelationFilterOperatorGreaterThan; + default: + FSTFail(@"Unhandled FieldFilter.operator: %d", filterOperator); + } +} + +#pragma mark Property Orders + +- (NSArray<GCFSStructuredQuery_Order *> *)encodedSortOrders:(NSArray<FSTSortOrder *> *)orders { + NSMutableArray<GCFSStructuredQuery_Order *> *protos = [NSMutableArray array]; + for (FSTSortOrder *order in orders) { + [protos addObject:[self encodedSortOrder:order]]; + } + return protos; +} + +- (NSArray<FSTSortOrder *> *)decodedSortOrders:(NSArray<GCFSStructuredQuery_Order *> *)protos { + NSMutableArray<FSTSortOrder *> *result = [NSMutableArray arrayWithCapacity:protos.count]; + for (GCFSStructuredQuery_Order *orderProto in protos) { + [result addObject:[self decodedSortOrder:orderProto]]; + } + return result; +} + +- (GCFSStructuredQuery_Order *)encodedSortOrder:(FSTSortOrder *)sortOrder { + GCFSStructuredQuery_Order *proto = [GCFSStructuredQuery_Order message]; + proto.field = [self encodedFieldPath:sortOrder.field]; + if (sortOrder.ascending) { + proto.direction = GCFSStructuredQuery_Direction_Ascending; + } else { + proto.direction = GCFSStructuredQuery_Direction_Descending; + } + return proto; +} + +- (FSTSortOrder *)decodedSortOrder:(GCFSStructuredQuery_Order *)proto { + FSTFieldPath *fieldPath = [FSTFieldPath pathWithServerFormat:proto.field.fieldPath]; + BOOL ascending; + switch (proto.direction) { + case GCFSStructuredQuery_Direction_Ascending: + ascending = YES; + break; + case GCFSStructuredQuery_Direction_Descending: + ascending = NO; + break; + default: + FSTFail(@"Unrecognized GCFSStructuredQuery_Direction %d", proto.direction); + } + return [FSTSortOrder sortOrderWithFieldPath:fieldPath ascending:ascending]; +} + +#pragma mark - Bounds/Cursors + +- (GCFSCursor *)encodedBound:(FSTBound *)bound { + GCFSCursor *proto = [GCFSCursor message]; + proto.before = bound.isBefore; + for (FSTFieldValue *fieldValue in bound.position) { + GCFSValue *value = [self encodedFieldValue:fieldValue]; + [proto.valuesArray addObject:value]; + } + return proto; +} + +- (FSTBound *)decodedBound:(GCFSCursor *)proto { + NSMutableArray<FSTFieldValue *> *indexComponents = [NSMutableArray array]; + + for (GCFSValue *valueProto in proto.valuesArray) { + FSTFieldValue *value = [self decodedFieldValue:valueProto]; + [indexComponents addObject:value]; + } + + return [FSTBound boundWithPosition:indexComponents isBefore:proto.before]; +} + +#pragma mark - FSTWatchChange <= GCFSListenResponse proto + +- (FSTWatchChange *)decodedWatchChange:(GCFSListenResponse *)watchChange { + switch (watchChange.responseTypeOneOfCase) { + case GCFSListenResponse_ResponseType_OneOfCase_TargetChange: + return [self decodedTargetChangeFromWatchChange:watchChange.targetChange]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentChange: + return [self decodedDocumentChange:watchChange.documentChange]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentDelete: + return [self decodedDocumentDelete:watchChange.documentDelete]; + + case GCFSListenResponse_ResponseType_OneOfCase_DocumentRemove: + return [self decodedDocumentRemove:watchChange.documentRemove]; + + case GCFSListenResponse_ResponseType_OneOfCase_Filter: + return [self decodedExistenceFilterWatchChange:watchChange.filter]; + + default: + FSTFail(@"Unknown WatchChange.changeType %" PRId32, watchChange.responseTypeOneOfCase); + } +} + +- (FSTSnapshotVersion *)versionFromListenResponse:(GCFSListenResponse *)watchChange { + // We have only reached a consistent snapshot for the entire stream if there is a read_time set + // and it applies to all targets (i.e. the list of targets is empty). The backend is guaranteed to + // send such responses. + if (watchChange.responseTypeOneOfCase != GCFSListenResponse_ResponseType_OneOfCase_TargetChange) { + return [FSTSnapshotVersion noVersion]; + } + if (watchChange.targetChange.targetIdsArray.count != 0) { + return [FSTSnapshotVersion noVersion]; + } + return [self decodedVersion:watchChange.targetChange.readTime]; +} + +- (FSTWatchTargetChange *)decodedTargetChangeFromWatchChange:(GCFSTargetChange *)change { + FSTWatchTargetChangeState state = [self decodedWatchTargetChangeState:change.targetChangeType]; + NSMutableArray<NSNumber *> *targetIDs = + [NSMutableArray arrayWithCapacity:change.targetIdsArray_Count]; + + [change.targetIdsArray enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { + [targetIDs addObject:@(value)]; + }]; + + NSError *cause = nil; + if (change.hasCause) { + cause = [NSError errorWithDomain:FIRFirestoreErrorDomain + code:change.cause.code + userInfo:@{NSLocalizedDescriptionKey : change.cause.message}]; + } + + return [[FSTWatchTargetChange alloc] initWithState:state + targetIDs:targetIDs + resumeToken:change.resumeToken + cause:cause]; +} + +- (FSTWatchTargetChangeState)decodedWatchTargetChangeState: + (GCFSTargetChange_TargetChangeType)state { + switch (state) { + case GCFSTargetChange_TargetChangeType_NoChange: + return FSTWatchTargetChangeStateNoChange; + case GCFSTargetChange_TargetChangeType_Add: + return FSTWatchTargetChangeStateAdded; + case GCFSTargetChange_TargetChangeType_Remove: + return FSTWatchTargetChangeStateRemoved; + case GCFSTargetChange_TargetChangeType_Current: + return FSTWatchTargetChangeStateCurrent; + case GCFSTargetChange_TargetChangeType_Reset: + return FSTWatchTargetChangeStateReset; + default: + FSTFail(@"Unexpected TargetChange.state: %" PRId32, state); + } +} + +- (NSArray<NSNumber *> *)decodedIntegerArray:(GPBInt32Array *)values { + NSMutableArray<NSNumber *> *result = [NSMutableArray arrayWithCapacity:values.count]; + [values enumerateValuesWithBlock:^(int32_t value, NSUInteger idx, BOOL *stop) { + [result addObject:@(value)]; + }]; + return result; +} + +- (FSTDocumentWatchChange *)decodedDocumentChange:(GCFSDocumentChange *)change { + FSTObjectValue *value = [self decodedFields:change.document.fields]; + FSTDocumentKey *key = [self decodedDocumentKey:change.document.name]; + FSTSnapshotVersion *version = [self decodedVersion:change.document.updateTime]; + FSTAssert(![version isEqual:[FSTSnapshotVersion noVersion]], + @"Got a document change with no snapshot version"); + FSTMaybeDocument *document = + [FSTDocument documentWithData:value key:key version:version hasLocalMutations:NO]; + + NSArray<NSNumber *> *updatedTargetIds = [self decodedIntegerArray:change.targetIdsArray]; + NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:updatedTargetIds + removedTargetIDs:removedTargetIds + documentKey:document.key + document:document]; +} + +- (FSTDocumentWatchChange *)decodedDocumentDelete:(GCFSDocumentDelete *)change { + FSTDocumentKey *key = [self decodedDocumentKey:change.document]; + // Note that version might be unset in which case we use [FSTSnapshotVersion noVersion] + FSTSnapshotVersion *version = [self decodedVersion:change.readTime]; + FSTMaybeDocument *document = [FSTDeletedDocument documentWithKey:key version:version]; + + NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:removedTargetIds + documentKey:document.key + document:document]; +} + +- (FSTDocumentWatchChange *)decodedDocumentRemove:(GCFSDocumentRemove *)change { + FSTDocumentKey *key = [self decodedDocumentKey:change.document]; + NSArray<NSNumber *> *removedTargetIds = [self decodedIntegerArray:change.removedTargetIdsArray]; + + return [[FSTDocumentWatchChange alloc] initWithUpdatedTargetIDs:@[] + removedTargetIDs:removedTargetIds + documentKey:key + document:nil]; +} + +- (FSTExistenceFilterWatchChange *)decodedExistenceFilterWatchChange:(GCFSExistenceFilter *)filter { + // TODO(dimond): implement existence filter parsing + FSTExistenceFilter *existenceFilter = [FSTExistenceFilter filterWithCount:filter.count]; + FSTTargetID targetID = filter.targetId; + return [FSTExistenceFilterWatchChange changeWithFilter:existenceFilter targetID:targetID]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTWatchChange.h b/Firestore/Source/Remote/FSTWatchChange.h new file mode 100644 index 0000000..6b65279 --- /dev/null +++ b/Firestore/Source/Remote/FSTWatchChange.h @@ -0,0 +1,118 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTTypes.h" + +@class FSTDocumentKey; +@class FSTExistenceFilter; +@class FSTMaybeDocument; +@class FSTSnapshotVersion; + +NS_ASSUME_NONNULL_BEGIN + +/** + * FSTWatchChange is the internal representation of the watcher API protocol buffers. + * This is an empty abstract class so that all the different kinds of changes can have a common + * base class. + */ +@interface FSTWatchChange : NSObject +@end + +/** + * FSTDocumentWatchChange represents a changed document and a list of target ids to which this + * change applies. If the document has been deleted, the deleted document will be provided. + */ +@interface FSTDocumentWatchChange : FSTWatchChange + +- (instancetype)initWithUpdatedTargetIDs:(NSArray<NSNumber *> *)updatedTargetIDs + removedTargetIDs:(NSArray<NSNumber *> *)removedTargetIDs + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTMaybeDocument *)document + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** The new document applies to all of these targets. */ +@property(nonatomic, strong, readonly) NSArray<NSNumber *> *updatedTargetIDs; + +/** The new document is removed from all of these targets. */ +@property(nonatomic, strong, readonly) NSArray<NSNumber *> *removedTargetIDs; + +/** The key of the document for this change. */ +@property(nonatomic, strong, readonly) FSTDocumentKey *documentKey; + +/** + * The new document or DeletedDocument if it was deleted. Is null if the document went out of + * view without the server sending a new document. + */ +@property(nonatomic, strong, readonly, nullable) FSTMaybeDocument *document; + +@end + +/** + * An ExistenceFilterWatchChange applies to the targets and is required to verify the current client + * state against expected state sent from the server. + */ +@interface FSTExistenceFilterWatchChange : FSTWatchChange + ++ (instancetype)changeWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID; + +- (instancetype)init NS_UNAVAILABLE; + +@property(nonatomic, strong, readonly) FSTExistenceFilter *filter; +@property(nonatomic, assign, readonly) FSTTargetID targetID; +@end + +/** FSTWatchTargetChangeState is the kind of change that happened to the watch target. */ +typedef NS_ENUM(NSInteger, FSTWatchTargetChangeState) { + FSTWatchTargetChangeStateNoChange, + FSTWatchTargetChangeStateAdded, + FSTWatchTargetChangeStateRemoved, + FSTWatchTargetChangeStateCurrent, + FSTWatchTargetChangeStateReset, +}; + +/** FSTWatchTargetChange is a change to a watch target. */ +@interface FSTWatchTargetChange : FSTWatchChange + +- (instancetype)initWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + resumeToken:(NSData *)resumeToken + cause:(nullable NSError *)cause NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** What kind of change occurred to the watch target. */ +@property(nonatomic, assign, readonly) FSTWatchTargetChangeState state; + +/** The target IDs that were added/removed/set. */ +@property(nonatomic, strong, readonly) NSArray<NSNumber *> *targetIDs; + +/** + * An opaque, server-assigned token that allows watching a query to be resumed after disconnecting + * without retransmitting all the data that matches the query. The resume token essentially + * identifies a point in time from which the server should resume sending results. + */ +@property(nonatomic, strong, readonly) NSData *resumeToken; + +/** An RPC error indicating why the watch failed. */ +@property(nonatomic, strong, readonly, nullable) NSError *cause; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Remote/FSTWatchChange.m b/Firestore/Source/Remote/FSTWatchChange.m new file mode 100644 index 0000000..1ace26e --- /dev/null +++ b/Firestore/Source/Remote/FSTWatchChange.m @@ -0,0 +1,150 @@ +/* + * 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 "FSTWatchChange.h" + +#import "FSTDocument.h" +#import "FSTDocumentKey.h" +#import "FSTExistenceFilter.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation FSTWatchChange +@end + +@implementation FSTDocumentWatchChange + +- (instancetype)initWithUpdatedTargetIDs:(NSArray<NSNumber *> *)updatedTargetIDs + removedTargetIDs:(NSArray<NSNumber *> *)removedTargetIDs + documentKey:(FSTDocumentKey *)documentKey + document:(nullable FSTMaybeDocument *)document { + self = [super init]; + if (self) { + _updatedTargetIDs = updatedTargetIDs; + _removedTargetIDs = removedTargetIDs; + _documentKey = documentKey; + _document = document; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTDocumentWatchChange class]]) { + return NO; + } + + FSTDocumentWatchChange *otherChange = (FSTDocumentWatchChange *)other; + return [_updatedTargetIDs isEqual:otherChange.updatedTargetIDs] && + [_removedTargetIDs isEqual:otherChange.removedTargetIDs] && + [_documentKey isEqual:otherChange.documentKey] && + (_document == otherChange.document || [_document isEqual:otherChange.document]); +} + +- (NSUInteger)hash { + NSUInteger hash = self.updatedTargetIDs.hash; + hash = hash * 31 + self.removedTargetIDs.hash; + hash = hash * 31 + self.documentKey.hash; + hash = hash * 31 + self.document.hash; + return hash; +} + +@end + +@interface FSTExistenceFilterWatchChange () + +- (instancetype)initWithFilter:(FSTExistenceFilter *)filter + targetID:(FSTTargetID)targetID NS_DESIGNATED_INITIALIZER; + +@end + +@implementation FSTExistenceFilterWatchChange + ++ (instancetype)changeWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID { + return [[FSTExistenceFilterWatchChange alloc] initWithFilter:filter targetID:targetID]; +} + +- (instancetype)initWithFilter:(FSTExistenceFilter *)filter targetID:(FSTTargetID)targetID { + self = [super init]; + if (self) { + _filter = filter; + _targetID = targetID; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTExistenceFilterWatchChange class]]) { + return NO; + } + + FSTExistenceFilterWatchChange *otherChange = (FSTExistenceFilterWatchChange *)other; + return [_filter isEqual:otherChange->_filter] && _targetID == otherChange->_targetID; +} + +- (NSUInteger)hash { + return self.filter.hash; +} + +@end + +@implementation FSTWatchTargetChange + +- (instancetype)initWithState:(FSTWatchTargetChangeState)state + targetIDs:(NSArray<NSNumber *> *)targetIDs + resumeToken:(NSData *)resumeToken + cause:(nullable NSError *)cause { + self = [super init]; + if (self) { + _state = state; + _targetIDs = targetIDs; + _resumeToken = resumeToken; + _cause = cause; + } + return self; +} + +- (BOOL)isEqual:(id)other { + if (other == self) { + return YES; + } + if (![other isMemberOfClass:[FSTWatchTargetChange class]]) { + return NO; + } + + FSTWatchTargetChange *otherChange = (FSTWatchTargetChange *)other; + return _state == otherChange->_state && [_targetIDs isEqual:otherChange->_targetIDs] && + [_resumeToken isEqual:otherChange->_resumeToken] && + (_cause == otherChange->_cause || [_cause isEqual:otherChange->_cause]); +} + +- (NSUInteger)hash { + NSUInteger hash = (NSUInteger)self.state; + + hash = hash * 31 + self.targetIDs.hash; + hash = hash * 31 + self.resumeToken.hash; + hash = hash * 31 + self.cause.hash; + return hash; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTAssert.h b/Firestore/Source/Util/FSTAssert.h new file mode 100644 index 0000000..77bbb1d --- /dev/null +++ b/Firestore/Source/Util/FSTAssert.h @@ -0,0 +1,77 @@ +/* + * 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. + */ + +#include <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +// Fails the current Objective-C method if the given condition is false. +// +// Unlike NSAssert, this macro is never compiled out if assertions are disabled. +#define FSTAssert(condition, format, ...) \ + do { \ + if (!(condition)) { \ + FSTFail((format), ##__VA_ARGS__); \ + } \ + } while (0) + +// Fails the current C function if the given condition is false. +// +// Unlike NSCAssert, this macro is never compiled out if assertions are disabled. +#define FSTCAssert(condition, format, ...) \ + do { \ + if (!(condition)) { \ + FSTCFail((format), ##__VA_ARGS__); \ + } \ + } while (0) + +// Unconditionally fails the current Objective-C method. +// +// This macro fails by calling [[NSAssertionHandler currentHandler] handleFailureInMethod]. It +// also calls abort(3) in order to make this macro appear to never return, even though the call +// to handleFailureInMethod itself never returns. +#define FSTFail(format, ...) \ + do { \ + NSString *_file = [NSString stringWithUTF8String:__FILE__]; \ + NSString *_description = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + [[NSAssertionHandler currentHandler] \ + handleFailureInMethod:_cmd \ + object:self \ + file:_file \ + lineNumber:__LINE__ \ + description:@"FIRESTORE INTERNAL ASSERTION FAILED: %@", _description]; \ + abort(); \ + } while (0) + +// Unconditionally fails the current C function. +// +// This macro fails by calling [[NSAssertionHandler currentHandler] handleFailureInFunction]. It +// also calls abort(3) in order to make this macro appear to never return, even though the call +// to handleFailureInFunction itself never returns. +#define FSTCFail(format, ...) \ + do { \ + NSString *_file = [NSString stringWithUTF8String:__FILE__]; \ + NSString *_function = [NSString stringWithUTF8String:__PRETTY_FUNCTION__]; \ + NSString *_description = [NSString stringWithFormat:(format), ##__VA_ARGS__]; \ + [[NSAssertionHandler currentHandler] \ + handleFailureInFunction:_function \ + file:_file \ + lineNumber:__LINE__ \ + description:@"FIRESTORE INTERNAL ASSERTION FAILED: %@", _description]; \ + abort(); \ + } while (0) + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.h b/Firestore/Source/Util/FSTAsyncQueryListener.h new file mode 100644 index 0000000..0ff1551 --- /dev/null +++ b/Firestore/Source/Util/FSTAsyncQueryListener.h @@ -0,0 +1,48 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTViewSnapshot.h" + +NS_ASSUME_NONNULL_BEGIN + +@class FSTDispatchQueue; +@class FSTQueryListener; + +/** + * A wrapper class around FSTQueryListener that dispatches events asynchronously. + */ +@interface FSTAsyncQueryListener : NSObject + +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler + NS_DESIGNATED_INITIALIZER; + +- (instancetype)init NS_UNAVAILABLE; + +/** + * Synchronously mutes the listener and raise no further events. This method is thread safe can be + * called from any queue. + */ +- (void)mute; + +/** Creates an asynchronous version of the provided snapshot handler. */ +- (FSTViewSnapshotHandler)asyncSnapshotHandler; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTAsyncQueryListener.m b/Firestore/Source/Util/FSTAsyncQueryListener.m new file mode 100644 index 0000000..31951e1 --- /dev/null +++ b/Firestore/Source/Util/FSTAsyncQueryListener.m @@ -0,0 +1,50 @@ +/* + * 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 "FSTAsyncQueryListener.h" + +#import "FSTDispatchQueue.h" + +@implementation FSTAsyncQueryListener { + volatile BOOL _muted; + FSTViewSnapshotHandler _snapshotHandler; + FSTDispatchQueue *_dispatchQueue; +} + +- (instancetype)initWithDispatchQueue:(FSTDispatchQueue *)dispatchQueue + snapshotHandler:(FSTViewSnapshotHandler)snapshotHandler { + if (self = [super init]) { + _dispatchQueue = dispatchQueue; + _snapshotHandler = snapshotHandler; + } + return self; +} + +- (FSTViewSnapshotHandler)asyncSnapshotHandler { + return ^(FSTViewSnapshot *_Nullable snapshot, NSError *_Nullable error) { + [_dispatchQueue dispatchAsync:^{ + if (!_muted) { + _snapshotHandler(snapshot, error); + } + }]; + }; +} + +- (void)mute { + _muted = true; +} + +@end diff --git a/Firestore/Source/Util/FSTClasses.h b/Firestore/Source/Util/FSTClasses.h new file mode 100644 index 0000000..77dca12 --- /dev/null +++ b/Firestore/Source/Util/FSTClasses.h @@ -0,0 +1,40 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +// A convenience macro for unimplemented methods. Use as follows: +// +// @throw FSTAbstractMethodException(); // NOLINT +#define FSTAbstractMethodException() \ + [NSException exceptionWithName:NSInternalInconsistencyException \ + reason:[NSString stringWithFormat:@"You must override %s in a subclass", \ + __func__] \ + userInfo:nil]; + +// Declare a weak pointer to the given variable +#define FSTWeakify(var) __weak typeof(var) fstWeakPointerTo##var = var; + +// Declare a strong pointer to a variable that's been FSTWeakified. This creates a shadow of the +// original. +#define FSTStrongify(var) \ + _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") \ + __strong typeof(var) var = fstWeakPointerTo##var; \ + _Pragma("clang diagnostic pop") + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTComparison.h b/Firestore/Source/Util/FSTComparison.h new file mode 100644 index 0000000..e6e57e6 --- /dev/null +++ b/Firestore/Source/Util/FSTComparison.h @@ -0,0 +1,66 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** Compares two NSStrings. */ +NSComparisonResult FSTCompareStrings(NSString *left, NSString *right); + +/** Compares two BOOLs. */ +NSComparisonResult FSTCompareBools(BOOL left, BOOL right); + +/** Compares two integers. */ +NSComparisonResult FSTCompareInts(int left, int right); + +/** Compares two int32_t. */ +NSComparisonResult FSTCompareInt32s(int32_t left, int32_t right); + +/** Compares two int64_t. */ +NSComparisonResult FSTCompareInt64s(int64_t left, int64_t right); + +/** Compares two NSUIntegers. */ +NSComparisonResult FSTCompareUIntegers(NSUInteger left, NSUInteger right); + +/** Compares two doubles (using Firestore semantics for NaN). */ +NSComparisonResult FSTCompareDoubles(double left, double right); + +/** Compares a double and an int64_t. */ +NSComparisonResult FSTCompareMixed(double doubleValue, int64_t longValue); + +/** Compare two NSData byte sequences. */ +NSComparisonResult FSTCompareBytes(NSData *left, NSData *right); + +/** A simple NSComparator for comparing NSNumber instances. */ +extern const NSComparator FSTNumberComparator; + +/** A simple NSComparator for comparing NSString instances. */ +extern const NSComparator FSTStringComparator; + +/** + * Compares the bitwise representation of two doubles, but normalizes NaN values. This is + * similar to what the backend and android clients do, including comparing -0.0 as not equal to 0.0. + */ +BOOL FSTDoubleBitwiseEquals(double left, double right); + +/** + * Computes a bitwise hash of a double, but normalizes NaN values, suitable for use when using + * FSTDoublesAreBitwiseEqual for equality. + */ +NSUInteger FSTDoubleBitwiseHash(double d); + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTComparison.m b/Firestore/Source/Util/FSTComparison.m new file mode 100644 index 0000000..e4f4ccb --- /dev/null +++ b/Firestore/Source/Util/FSTComparison.m @@ -0,0 +1,175 @@ +/* + * 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 "FSTComparison.h" + +NS_ASSUME_NONNULL_BEGIN + +union DoubleBits { + double d; + uint64_t bits; +}; + +const NSComparator FSTNumberComparator = ^NSComparisonResult(NSNumber *left, NSNumber *right) { + return [left compare:right]; +}; + +const NSComparator FSTStringComparator = ^NSComparisonResult(NSString *left, NSString *right) { + return FSTCompareStrings(left, right); +}; + +NSComparisonResult FSTCompareStrings(NSString *left, NSString *right) { + // NOTE: NSLiteralSearch is necessary to compare the raw character codes. By default, + // precomposed characters are considered equivalent to their decomposed equivalents. + return [left compare:right options:NSLiteralSearch]; +} + +NSComparisonResult FSTCompareBools(BOOL left, BOOL right) { + if (!left) { + return right ? NSOrderedAscending : NSOrderedSame; + } else { + return right ? NSOrderedSame : NSOrderedDescending; + } +} + +NSComparisonResult FSTCompareInts(int left, int right) { + if (left > right) { + return NSOrderedDescending; + } + if (right > left) { + return NSOrderedAscending; + } + return NSOrderedSame; +} + +NSComparisonResult FSTCompareInt32s(int32_t left, int32_t right) { + if (left > right) { + return NSOrderedDescending; + } + if (right > left) { + return NSOrderedAscending; + } + return NSOrderedSame; +} + +NSComparisonResult FSTCompareInt64s(int64_t left, int64_t right) { + if (left > right) { + return NSOrderedDescending; + } + if (right > left) { + return NSOrderedAscending; + } + return NSOrderedSame; +} + +NSComparisonResult FSTCompareUIntegers(NSUInteger left, NSUInteger right) { + if (left > right) { + return NSOrderedDescending; + } + if (right > left) { + return NSOrderedAscending; + } + return NSOrderedSame; +} + +NSComparisonResult FSTCompareDoubles(double left, double right) { + // NaN sorts equal to itself and before any other number. + if (left < right) { + return NSOrderedAscending; + } else if (left > right) { + return NSOrderedDescending; + } else if (left == right) { + return NSOrderedSame; + } else { + // One or both left and right is NaN. + if (isnan(left)) { + return isnan(right) ? NSOrderedSame : NSOrderedAscending; + } else { + return NSOrderedDescending; + } + } +} + +static const double LONG_MIN_VALUE_AS_DOUBLE = (double)LLONG_MIN; +static const double LONG_MAX_VALUE_AS_DOUBLE = (double)LLONG_MAX; + +NSComparisonResult FSTCompareMixed(double doubleValue, int64_t longValue) { + // LLONG_MIN has an exact representation as double, so to check for a value outside the range + // representable by long, we have to check for strictly less than LLONG_MIN. Note that this also + // handles negative infinity. + if (doubleValue < LONG_MIN_VALUE_AS_DOUBLE) { + return NSOrderedAscending; + } + + // LLONG_MAX has no exact representation as double (casting as we've done makes 2^63, which is + // larger than LLONG_MAX), so consider any value greater than or equal to the threshold to be out + // of range. This also handles positive infinity. + if (doubleValue >= LONG_MAX_VALUE_AS_DOUBLE) { + return NSOrderedDescending; + } + + // In Firestore NaN is defined to compare before all other numbers. + if (isnan(doubleValue)) { + return NSOrderedAscending; + } + + int64_t doubleAsLong = (int64_t)doubleValue; + NSComparisonResult cmp = FSTCompareInt64s(doubleAsLong, longValue); + if (cmp != NSOrderedSame) { + return cmp; + } + + // At this point the long representations are equal but this could be due to rounding. + double longAsDouble = (double)longValue; + return FSTCompareDoubles(doubleValue, longAsDouble); +} + +NSComparisonResult FSTCompareBytes(NSData *left, NSData *right) { + NSUInteger minLength = MIN(left.length, right.length); + int result = memcmp(left.bytes, right.bytes, minLength); + if (result < 0) { + return NSOrderedAscending; + } else if (result > 0) { + return NSOrderedDescending; + } else if (left.length < right.length) { + return NSOrderedAscending; + } else if (left.length > right.length) { + return NSOrderedDescending; + } else { + return NSOrderedSame; + } +} + +/** Helper to normalize a double and then return the raw bits as a uint64_t. */ +uint64_t FSTDoubleBits(double d) { + if (isnan(d)) { + d = NAN; + } + union DoubleBits converter = {.d = d}; + return converter.bits; +} + +BOOL FSTDoubleBitwiseEquals(double left, double right) { + return FSTDoubleBits(left) == FSTDoubleBits(right); +} + +NSUInteger FSTDoubleBitwiseHash(double d) { + uint64_t bits = FSTDoubleBits(d); + // Note that x ^ (x >> 32) works fine for both 32 and 64 bit definitions of NSUInteger + return (((NSUInteger)bits) ^ (NSUInteger)(bits >> 32)); +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTDispatchQueue.h b/Firestore/Source/Util/FSTDispatchQueue.h new file mode 100644 index 0000000..da6b3fe --- /dev/null +++ b/Firestore/Source/Util/FSTDispatchQueue.h @@ -0,0 +1,58 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDispatchQueue : NSObject + +/** Creates and returns an FSTDispatchQueue wrapping the specified dispatch_queue_t. */ ++ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue; + +- (instancetype)init __attribute__((unavailable("Use static constructor method."))); + +/** + * Asserts that we are already running on this queue (actually, we can only verify that the + * queue's label is the same, but hopefully that's good enough.) + */ +- (void)verifyIsCurrentQueue; + +/** + * Same as dispatch_async() except it asserts that we're not already on the queue, since this + * generally indicates a bug (and can lead to re-ordering of operations, etc). + * + * @param block The block to run. + */ +- (void)dispatchAsync:(void (^)())block; + +/** + * Unlike dispatchAsync: this method does not require you to dispatch to a different queue than + * the current one (thus it is equivalent to a raw dispatch_async()). + * + * This is useful, e.g. for dispatching to the user's queue directly from user API call (in which + * case we don't know if we're already on the user's queue or not). + * + * @param block The block to run. + */ +- (void)dispatchAsyncAllowingSameQueue:(void (^)())block; + +/** The underlying wrapped dispatch_queue_t */ +@property(nonatomic, strong, readonly) dispatch_queue_t queue; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTDispatchQueue.m b/Firestore/Source/Util/FSTDispatchQueue.m new file mode 100644 index 0000000..8d55d28 --- /dev/null +++ b/Firestore/Source/Util/FSTDispatchQueue.m @@ -0,0 +1,75 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FSTAssert.h" +#import "FSTDispatchQueue.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTDispatchQueue () +- (instancetype)initWithQueue:(dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; +@end + +@implementation FSTDispatchQueue + ++ (instancetype)queueWith:(dispatch_queue_t)dispatchQueue { + return [[FSTDispatchQueue alloc] initWithQueue:dispatchQueue]; +} + +- (instancetype)initWithQueue:(dispatch_queue_t)queue { + if (self = [super init]) { + _queue = queue; + } + return self; +} + +- (void)verifyIsCurrentQueue { + FSTAssert([self onTargetQueue], + @"We are running on the wrong dispatch queue. Expected '%@' Actual: '%@'", + [self targetQueueLabel], [self currentQueueLabel]); +} + +- (void)dispatchAsync:(void (^)())block { + FSTAssert(![self onTargetQueue], + @"dispatchAsync called when we are already running on target dispatch queue '%@'", + [self targetQueueLabel]); + + dispatch_async(self.queue, block); +} + +- (void)dispatchAsyncAllowingSameQueue:(void (^)())block { + dispatch_async(self.queue, block); +} + +#pragma mark - Private Methods + +- (NSString *)currentQueueLabel { + return [NSString stringWithUTF8String:dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL)]; +} + +- (NSString *)targetQueueLabel { + return [NSString stringWithUTF8String:dispatch_queue_get_label(self.queue)]; +} + +- (BOOL)onTargetQueue { + return [[self currentQueueLabel] isEqualToString:[self targetQueueLabel]]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTLogger.h b/Firestore/Source/Util/FSTLogger.h new file mode 100644 index 0000000..699570a --- /dev/null +++ b/Firestore/Source/Util/FSTLogger.h @@ -0,0 +1,34 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +#ifdef __cplusplus +extern "C" { +#endif + +/** Logs to NSLog if [FIRFirestore isLoggingEnabled] is YES. */ +void FSTLog(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2); + +void FSTWarn(NSString *format, ...) NS_FORMAT_FUNCTION(1, 2); + +#ifdef __cplusplus +} // extern "C" +#endif + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTLogger.m b/Firestore/Source/Util/FSTLogger.m new file mode 100644 index 0000000..396c788 --- /dev/null +++ b/Firestore/Source/Util/FSTLogger.m @@ -0,0 +1,40 @@ +/* + * 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 "FSTLogger.h" + +#import "FIRFirestore+Internal.h" +#import "FIRLogger.h" + +NS_ASSUME_NONNULL_BEGIN + +void FSTLog(NSString *format, ...) { + if ([FIRFirestore isLoggingEnabled]) { + va_list args; + va_start(args, format); + FIRLogBasic(FIRLoggerLevelDebug, kFIRLoggerFirestore, @"I-FST000001", format, args); + va_end(args); + } +} + +void FSTWarn(NSString *format, ...) { + va_list args; + va_start(args, format); + FIRLogBasic(FIRLoggerLevelWarning, kFIRLoggerFirestore, @"I-FST000001", format, args); + va_end(args); +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUsageValidation.h b/Firestore/Source/Util/FSTUsageValidation.h new file mode 100644 index 0000000..a80dafa --- /dev/null +++ b/Firestore/Source/Util/FSTUsageValidation.h @@ -0,0 +1,45 @@ +/* + * 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. + */ + +#include <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +/** Helper for creating a general exception for invalid usage of an API. */ +NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...); + +/** + * Macro to throw exceptions in response to API usage errors. Avoids the lint warning you usually + * get when using @throw and (unlike a function) doesn't trigger warnings about not all codepaths + * returning a value. + * + * Exceptions should only be used for programmer errors made by consumers of the SDK, e.g. + * invalid method arguments. + * + * For recoverable runtime errors, use NSError**. + * For internal programming errors, use FSTFail(). + */ +#define FSTThrowInvalidUsage(exceptionName, format, ...) \ + do { \ + @throw FSTInvalidUsage(exceptionName, format, ##__VA_ARGS__); \ + } while (0) + +#define FSTThrowInvalidArgument(format, ...) \ + do { \ + @throw FSTInvalidUsage(@"FIRInvalidArgumentException", format, ##__VA_ARGS__); \ + } while (0) + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUsageValidation.m b/Firestore/Source/Util/FSTUsageValidation.m new file mode 100644 index 0000000..82128f4 --- /dev/null +++ b/Firestore/Source/Util/FSTUsageValidation.m @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#include <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +NSException *FSTInvalidUsage(NSString *exceptionName, NSString *format, ...) { + va_list arg_list; + va_start(arg_list, format); + NSString *formattedString = [[NSString alloc] initWithFormat:format arguments:arg_list]; + va_end(arg_list); + + return [[NSException alloc] initWithName:exceptionName reason:formattedString userInfo:nil]; +} + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUtil.h b/Firestore/Source/Util/FSTUtil.h new file mode 100644 index 0000000..3985d10 --- /dev/null +++ b/Firestore/Source/Util/FSTUtil.h @@ -0,0 +1,31 @@ +/* + * 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 <Foundation/Foundation.h> + +NS_ASSUME_NONNULL_BEGIN + +@interface FSTUtil : NSObject + +/** Generates a random double between 0 and 1. */ ++ (double)randomDouble; + +/** Generates a random ID suitable for use as a document ID. */ ++ (NSString *)autoID; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Firestore/Source/Util/FSTUtil.m b/Firestore/Source/Util/FSTUtil.m new file mode 100644 index 0000000..d14c429 --- /dev/null +++ b/Firestore/Source/Util/FSTUtil.m @@ -0,0 +1,44 @@ +/* + * 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 "FSTUtil.h" + +NS_ASSUME_NONNULL_BEGIN + +static const double kArc4RandomMax = 0x100000000; + +static const int kAutoIDLength = 20; +static NSString *const kAutoIDAlphabet = + @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + +@implementation FSTUtil + ++ (double)randomDouble { + return ((double)arc4random() / kArc4RandomMax); +} + ++ (NSString *)autoID { + unichar autoID[kAutoIDLength]; + for (int i = 0; i < kAutoIDLength; i++) { + uint32_t randIndex = arc4random_uniform((uint32_t)kAutoIDAlphabet.length); + autoID[i] = [kAutoIDAlphabet characterAtIndex:randIndex]; + } + return [NSString stringWithCharacters:autoID length:kAutoIDLength]; +} + +@end + +NS_ASSUME_NONNULL_END |