aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Source
diff options
context:
space:
mode:
authorGravatar Gil <mcg@google.com>2017-10-03 08:55:22 -0700
committerGravatar GitHub <noreply@github.com>2017-10-03 08:55:22 -0700
commitbde743ed25166a0b320ae157bfb1d68064f531c9 (patch)
tree4dd7525d9df32fa5dbdb721d4b0d4f9b87f5e884 /Firestore/Source
parentbf550507ffa8beee149383a5bf1e2363bccefbb4 (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')
-rw-r--r--Firestore/Source/API/FIRCollectionReference+Internal.h28
-rw-r--r--Firestore/Source/API/FIRCollectionReference.m113
-rw-r--r--Firestore/Source/API/FIRDocumentChange+Internal.h32
-rw-r--r--Firestore/Source/API/FIRDocumentChange.m129
-rw-r--r--Firestore/Source/API/FIRDocumentReference+Internal.h34
-rw-r--r--Firestore/Source/API/FIRDocumentReference.m285
-rw-r--r--Firestore/Source/API/FIRDocumentSnapshot+Internal.h37
-rw-r--r--Firestore/Source/API/FIRDocumentSnapshot.m175
-rw-r--r--Firestore/Source/API/FIRFieldPath+Internal.h39
-rw-r--r--Firestore/Source/API/FIRFieldPath.m101
-rw-r--r--Firestore/Source/API/FIRFieldValue+Internal.h37
-rw-r--r--Firestore/Source/API/FIRFieldValue.m96
-rw-r--r--Firestore/Source/API/FIRFirestore+Internal.h64
-rw-r--r--Firestore/Source/API/FIRFirestore.m284
-rw-r--r--Firestore/Source/API/FIRFirestoreSettings.m92
-rw-r--r--Firestore/Source/API/FIRFirestoreVersion.h22
-rw-r--r--Firestore/Source/API/FIRFirestoreVersion.m29
-rw-r--r--Firestore/Source/API/FIRGeoPoint+Internal.h26
-rw-r--r--Firestore/Source/API/FIRGeoPoint.m85
-rw-r--r--Firestore/Source/API/FIRListenerRegistration+Internal.h34
-rw-r--r--Firestore/Source/API/FIRListenerRegistration.m57
-rw-r--r--Firestore/Source/API/FIRQuery+Internal.h29
-rw-r--r--Firestore/Source/API/FIRQuery.m520
-rw-r--r--Firestore/Source/API/FIRQuerySnapshot+Internal.h37
-rw-r--r--Firestore/Source/API/FIRQuerySnapshot.m125
-rw-r--r--Firestore/Source/API/FIRQuery_Init.h32
-rw-r--r--Firestore/Source/API/FIRSetOptions+Internal.h33
-rw-r--r--Firestore/Source/API/FIRSetOptions.m65
-rw-r--r--Firestore/Source/API/FIRSnapshotMetadata+Internal.h29
-rw-r--r--Firestore/Source/API/FIRSnapshotMetadata.m49
-rw-r--r--Firestore/Source/API/FIRTransaction+Internal.h27
-rw-r--r--Firestore/Source/API/FIRTransaction.m147
-rw-r--r--Firestore/Source/API/FIRWriteBatch+Internal.h25
-rw-r--r--Firestore/Source/API/FIRWriteBatch.m116
-rw-r--r--Firestore/Source/API/FSTUserDataConverter.h124
-rw-r--r--Firestore/Source/API/FSTUserDataConverter.m568
-rw-r--r--Firestore/Source/Auth/FSTCredentialsProvider.h113
-rw-r--r--Firestore/Source/Auth/FSTCredentialsProvider.m161
-rw-r--r--Firestore/Source/Auth/FSTEmptyCredentialsProvider.h28
-rw-r--r--Firestore/Source/Auth/FSTEmptyCredentialsProvider.m47
-rw-r--r--Firestore/Source/Auth/FSTUser.h43
-rw-r--r--Firestore/Source/Auth/FSTUser.m68
-rw-r--r--Firestore/Source/Core/FSTDatabaseInfo.h55
-rw-r--r--Firestore/Source/Core/FSTDatabaseInfo.m70
-rw-r--r--Firestore/Source/Core/FSTEventManager.h88
-rw-r--r--Firestore/Source/Core/FSTEventManager.m335
-rw-r--r--Firestore/Source/Core/FSTFirestoreClient.h87
-rw-r--r--Firestore/Source/Core/FSTFirestoreClient.m271
-rw-r--r--Firestore/Source/Core/FSTQuery.h269
-rw-r--r--Firestore/Source/Core/FSTQuery.m759
-rw-r--r--Firestore/Source/Core/FSTSnapshotVersion.h43
-rw-r--r--Firestore/Source/Core/FSTSnapshotVersion.m80
-rw-r--r--Firestore/Source/Core/FSTSyncEngine.h105
-rw-r--r--Firestore/Source/Core/FSTSyncEngine.m520
-rw-r--r--Firestore/Source/Core/FSTTargetIDGenerator.h55
-rw-r--r--Firestore/Source/Core/FSTTargetIDGenerator.m105
-rw-r--r--Firestore/Source/Core/FSTTimestamp.h72
-rw-r--r--Firestore/Source/Core/FSTTimestamp.m122
-rw-r--r--Firestore/Source/Core/FSTTransaction.h73
-rw-r--r--Firestore/Source/Core/FSTTransaction.m250
-rw-r--r--Firestore/Source/Core/FSTTypes.h90
-rw-r--r--Firestore/Source/Core/FSTView.h143
-rw-r--r--Firestore/Source/Core/FSTView.m451
-rw-r--r--Firestore/Source/Core/FSTViewSnapshot.h117
-rw-r--r--Firestore/Source/Core/FSTViewSnapshot.m231
-rw-r--r--Firestore/Source/Local/FSTDocumentReference.h61
-rw-r--r--Firestore/Source/Local/FSTDocumentReference.m83
-rw-r--r--Firestore/Source/Local/FSTEagerGarbageCollector.h36
-rw-r--r--Firestore/Source/Local/FSTEagerGarbageCollector.m89
-rw-r--r--Firestore/Source/Local/FSTGarbageCollector.h95
-rw-r--r--Firestore/Source/Local/FSTLevelDB.h105
-rw-r--r--Firestore/Source/Local/FSTLevelDB.mm246
-rw-r--r--Firestore/Source/Local/FSTLevelDBKey.h344
-rw-r--r--Firestore/Source/Local/FSTLevelDBKey.mm757
-rw-r--r--Firestore/Source/Local/FSTLevelDBMutationQueue.h64
-rw-r--r--Firestore/Source/Local/FSTLevelDBMutationQueue.mm637
-rw-r--r--Firestore/Source/Local/FSTLevelDBQueryCache.h54
-rw-r--r--Firestore/Source/Local/FSTLevelDBQueryCache.mm340
-rw-r--r--Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.h50
-rw-r--r--Firestore/Source/Local/FSTLevelDBRemoteDocumentCache.mm153
-rw-r--r--Firestore/Source/Local/FSTLocalDocumentsView.h62
-rw-r--r--Firestore/Source/Local/FSTLocalDocumentsView.m182
-rw-r--r--Firestore/Source/Local/FSTLocalSerializer.h72
-rw-r--r--Firestore/Source/Local/FSTLocalSerializer.m208
-rw-r--r--Firestore/Source/Local/FSTLocalStore.h194
-rw-r--r--Firestore/Source/Local/FSTLocalStore.m546
-rw-r--r--Firestore/Source/Local/FSTLocalViewChanges.h51
-rw-r--r--Firestore/Source/Local/FSTLocalViewChanges.m76
-rw-r--r--Firestore/Source/Local/FSTLocalWriteResult.h36
-rw-r--r--Firestore/Source/Local/FSTLocalWriteResult.m43
-rw-r--r--Firestore/Source/Local/FSTMemoryMutationQueue.h34
-rw-r--r--Firestore/Source/Local/FSTMemoryMutationQueue.m441
-rw-r--r--Firestore/Source/Local/FSTMemoryPersistence.h33
-rw-r--r--Firestore/Source/Local/FSTMemoryPersistence.m107
-rw-r--r--Firestore/Source/Local/FSTMemoryQueryCache.h30
-rw-r--r--Firestore/Source/Local/FSTMemoryQueryCache.m131
-rw-r--r--Firestore/Source/Local/FSTMemoryRemoteDocumentCache.h29
-rw-r--r--Firestore/Source/Local/FSTMemoryRemoteDocumentCache.m84
-rw-r--r--Firestore/Source/Local/FSTMutationQueue.h159
-rw-r--r--Firestore/Source/Local/FSTNoOpGarbageCollector.h32
-rw-r--r--Firestore/Source/Local/FSTNoOpGarbageCollector.m45
-rw-r--r--Firestore/Source/Local/FSTPersistence.h103
-rw-r--r--Firestore/Source/Local/FSTQueryCache.h113
-rw-r--r--Firestore/Source/Local/FSTQueryData.h82
-rw-r--r--Firestore/Source/Local/FSTQueryData.m93
-rw-r--r--Firestore/Source/Local/FSTReferenceSet.h71
-rw-r--r--Firestore/Source/Local/FSTReferenceSet.m135
-rw-r--r--Firestore/Source/Local/FSTRemoteDocumentCache.h76
-rw-r--r--Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.h66
-rw-r--r--Firestore/Source/Local/FSTRemoteDocumentChangeBuffer.m88
-rw-r--r--Firestore/Source/Local/FSTWriteGroup.h97
-rw-r--r--Firestore/Source/Local/FSTWriteGroup.mm145
-rw-r--r--Firestore/Source/Local/FSTWriteGroupTracker.h45
-rw-r--r--Firestore/Source/Local/FSTWriteGroupTracker.m52
-rw-r--r--Firestore/Source/Local/StringView.h85
-rw-r--r--Firestore/Source/Model/FSTDatabaseID.h48
-rw-r--r--Firestore/Source/Model/FSTDatabaseID.m90
-rw-r--r--Firestore/Source/Model/FSTDocument.h58
-rw-r--r--Firestore/Source/Model/FSTDocument.m139
-rw-r--r--Firestore/Source/Model/FSTDocumentDictionary.h44
-rw-r--r--Firestore/Source/Model/FSTDocumentDictionary.m42
-rw-r--r--Firestore/Source/Model/FSTDocumentKey.h66
-rw-r--r--Firestore/Source/Model/FSTDocumentKey.m105
-rw-r--r--Firestore/Source/Model/FSTDocumentKeySet.h35
-rw-r--r--Firestore/Source/Model/FSTDocumentKeySet.m31
-rw-r--r--Firestore/Source/Model/FSTDocumentSet.h95
-rw-r--r--Firestore/Source/Model/FSTDocumentSet.m197
-rw-r--r--Firestore/Source/Model/FSTDocumentVersionDictionary.h40
-rw-r--r--Firestore/Source/Model/FSTDocumentVersionDictionary.m37
-rw-r--r--Firestore/Source/Model/FSTFieldValue.h242
-rw-r--r--Firestore/Source/Model/FSTFieldValue.m837
-rw-r--r--Firestore/Source/Model/FSTMutation.h325
-rw-r--r--Firestore/Source/Model/FSTMutation.m575
-rw-r--r--Firestore/Source/Model/FSTMutationBatch.h119
-rw-r--r--Firestore/Source/Model/FSTMutationBatch.m176
-rw-r--r--Firestore/Source/Model/FSTPath.h141
-rw-r--r--Firestore/Source/Model/FSTPath.m356
-rw-r--r--Firestore/Source/Public/FIRCollectionReference.h99
-rw-r--r--Firestore/Source/Public/FIRDocumentChange.h70
-rw-r--r--Firestore/Source/Public/FIRDocumentReference.h219
-rw-r--r--Firestore/Source/Public/FIRDocumentSnapshot.h68
-rw-r--r--Firestore/Source/Public/FIRFieldPath.h50
-rw-r--r--Firestore/Source/Public/FIRFieldValue.h45
-rw-r--r--Firestore/Source/Public/FIRFirestore.h145
-rw-r--r--Firestore/Source/Public/FIRFirestoreErrors.h105
-rw-r--r--Firestore/Source/Public/FIRFirestoreSettings.h51
-rw-r--r--Firestore/Source/Public/FIRFirestoreSwiftNameSupport.h29
-rw-r--r--Firestore/Source/Public/FIRGeoPoint.h49
-rw-r--r--Firestore/Source/Public/FIRListenerRegistration.h32
-rw-r--r--Firestore/Source/Public/FIRQuery.h414
-rw-r--r--Firestore/Source/Public/FIRQuerySnapshot.h65
-rw-r--r--Firestore/Source/Public/FIRSetOptions.h46
-rw-r--r--Firestore/Source/Public/FIRSnapshotMetadata.h44
-rw-r--r--Firestore/Source/Public/FIRTransaction.h106
-rw-r--r--Firestore/Source/Public/FIRWriteBatch.h107
-rw-r--r--Firestore/Source/Remote/FSTBufferedWriter.h44
-rw-r--r--Firestore/Source/Remote/FSTBufferedWriter.m134
-rw-r--r--Firestore/Source/Remote/FSTDatastore.h365
-rw-r--r--Firestore/Source/Remote/FSTDatastore.m1027
-rw-r--r--Firestore/Source/Remote/FSTExistenceFilter.h31
-rw-r--r--Firestore/Source/Remote/FSTExistenceFilter.m53
-rw-r--r--Firestore/Source/Remote/FSTExponentialBackoff.h79
-rw-r--r--Firestore/Source/Remote/FSTExponentialBackoff.m97
-rw-r--r--Firestore/Source/Remote/FSTRemoteEvent.h213
-rw-r--r--Firestore/Source/Remote/FSTRemoteEvent.m516
-rw-r--r--Firestore/Source/Remote/FSTRemoteStore.h143
-rw-r--r--Firestore/Source/Remote/FSTRemoteStore.m599
-rw-r--r--Firestore/Source/Remote/FSTSerializerBeta.h110
-rw-r--r--Firestore/Source/Remote/FSTSerializerBeta.m1084
-rw-r--r--Firestore/Source/Remote/FSTWatchChange.h118
-rw-r--r--Firestore/Source/Remote/FSTWatchChange.m150
-rw-r--r--Firestore/Source/Util/FSTAssert.h77
-rw-r--r--Firestore/Source/Util/FSTAsyncQueryListener.h48
-rw-r--r--Firestore/Source/Util/FSTAsyncQueryListener.m50
-rw-r--r--Firestore/Source/Util/FSTClasses.h40
-rw-r--r--Firestore/Source/Util/FSTComparison.h66
-rw-r--r--Firestore/Source/Util/FSTComparison.m175
-rw-r--r--Firestore/Source/Util/FSTDispatchQueue.h58
-rw-r--r--Firestore/Source/Util/FSTDispatchQueue.m75
-rw-r--r--Firestore/Source/Util/FSTLogger.h34
-rw-r--r--Firestore/Source/Util/FSTLogger.m40
-rw-r--r--Firestore/Source/Util/FSTUsageValidation.h45
-rw-r--r--Firestore/Source/Util/FSTUsageValidation.m30
-rw-r--r--Firestore/Source/Util/FSTUtil.h31
-rw-r--r--Firestore/Source/Util/FSTUtil.m44
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