diff options
Diffstat (limited to 'Firestore/Source/Core/FSTEventManager.mm')
-rw-r--r-- | Firestore/Source/Core/FSTEventManager.mm | 335 |
1 files changed, 335 insertions, 0 deletions
diff --git a/Firestore/Source/Core/FSTEventManager.mm b/Firestore/Source/Core/FSTEventManager.mm new file mode 100644 index 0000000..bc204a0 --- /dev/null +++ b/Firestore/Source/Core/FSTEventManager.mm @@ -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 "Firestore/Source/Core/FSTEventManager.h" + +#import "Firestore/Source/Core/FSTQuery.h" +#import "Firestore/Source/Core/FSTSyncEngine.h" +#import "Firestore/Source/Model/FSTDocumentSet.h" +#import "Firestore/Source/Util/FSTAssert.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)applyChangedOnlineState:(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 applyChangedOnlineState: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)applyChangedOnlineState:(FSTOnlineState)onlineState { + self.onlineState = onlineState; + for (FSTQueryListenersInfo *info in self.queries.objectEnumerator) { + for (FSTQueryListener *listener in info.listeners) { + [listener applyChangedOnlineState:onlineState]; + } + } +} + +@end + +NS_ASSUME_NONNULL_END |