aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Source/Core
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/Core
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/Core')
-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
23 files changed, 4391 insertions, 0 deletions
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