aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firestore/Source/Remote/FSTRemoteEvent.mm
diff options
context:
space:
mode:
Diffstat (limited to 'Firestore/Source/Remote/FSTRemoteEvent.mm')
-rw-r--r--Firestore/Source/Remote/FSTRemoteEvent.mm528
1 files changed, 528 insertions, 0 deletions
diff --git a/Firestore/Source/Remote/FSTRemoteEvent.mm b/Firestore/Source/Remote/FSTRemoteEvent.mm
new file mode 100644
index 0000000..88999e4
--- /dev/null
+++ b/Firestore/Source/Remote/FSTRemoteEvent.mm
@@ -0,0 +1,528 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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/Remote/FSTRemoteEvent.h"
+
+#import "Firestore/Source/Core/FSTSnapshotVersion.h"
+#import "Firestore/Source/Model/FSTDocument.h"
+#import "Firestore/Source/Model/FSTDocumentKey.h"
+#import "Firestore/Source/Remote/FSTWatchChange.h"
+#import "Firestore/Source/Util/FSTAssert.h"
+#import "Firestore/Source/Util/FSTClasses.h"
+#import "Firestore/Source/Util/FSTLogger.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+#pragma mark - FSTTargetMapping
+
+@interface FSTTargetMapping ()
+
+/** Private mutator method to add a document key to the mapping */
+- (void)addDocumentKey:(FSTDocumentKey *)documentKey;
+
+/** Private mutator method to remove a document key from the mapping */
+- (void)removeDocumentKey:(FSTDocumentKey *)documentKey;
+
+@end
+
+@implementation FSTTargetMapping
+
+- (void)addDocumentKey:(FSTDocumentKey *)documentKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+- (void)removeDocumentKey:(FSTDocumentKey *)documentKey {
+ @throw FSTAbstractMethodException(); // NOLINT
+}
+
+@end
+
+#pragma mark - FSTResetMapping
+
+@interface FSTResetMapping ()
+@property(nonatomic, strong) FSTDocumentKeySet *documents;
+@end
+
+@implementation FSTResetMapping
+
++ (instancetype)mappingWithDocuments:(NSArray<FSTDocument *> *)documents {
+ FSTResetMapping *mapping = [[FSTResetMapping alloc] init];
+ for (FSTDocument *doc in documents) {
+ mapping.documents = [mapping.documents setByAddingObject:doc.key];
+ }
+ return mapping;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _documents = [FSTDocumentKeySet keySet];
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTResetMapping class]]) {
+ return NO;
+ }
+
+ FSTResetMapping *otherMapping = (FSTResetMapping *)other;
+ return [self.documents isEqual:otherMapping.documents];
+}
+
+- (NSUInteger)hash {
+ return self.documents.hash;
+}
+
+- (void)addDocumentKey:(FSTDocumentKey *)documentKey {
+ self.documents = [self.documents setByAddingObject:documentKey];
+}
+
+- (void)removeDocumentKey:(FSTDocumentKey *)documentKey {
+ self.documents = [self.documents setByRemovingObject:documentKey];
+}
+
+@end
+
+#pragma mark - FSTUpdateMapping
+
+@interface FSTUpdateMapping ()
+@property(nonatomic, strong) FSTDocumentKeySet *addedDocuments;
+@property(nonatomic, strong) FSTDocumentKeySet *removedDocuments;
+@end
+
+@implementation FSTUpdateMapping
+
++ (FSTUpdateMapping *)mappingWithAddedDocuments:(NSArray<FSTDocument *> *)added
+ removedDocuments:(NSArray<FSTDocument *> *)removed {
+ FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init];
+ for (FSTDocument *doc in added) {
+ mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key];
+ }
+ for (FSTDocument *doc in removed) {
+ mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key];
+ }
+ return mapping;
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _addedDocuments = [FSTDocumentKeySet keySet];
+ _removedDocuments = [FSTDocumentKeySet keySet];
+ }
+ return self;
+}
+
+- (BOOL)isEqual:(id)other {
+ if (other == self) {
+ return YES;
+ }
+ if (![other isMemberOfClass:[FSTUpdateMapping class]]) {
+ return NO;
+ }
+
+ FSTUpdateMapping *otherMapping = (FSTUpdateMapping *)other;
+ return [self.addedDocuments isEqual:otherMapping.addedDocuments] &&
+ [self.removedDocuments isEqual:otherMapping.removedDocuments];
+}
+
+- (NSUInteger)hash {
+ return self.addedDocuments.hash * 31 + self.removedDocuments.hash;
+}
+
+- (FSTDocumentKeySet *)applyTo:(FSTDocumentKeySet *)keys {
+ __block FSTDocumentKeySet *result = keys;
+ [self.addedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ result = [result setByAddingObject:key];
+ }];
+ [self.removedDocuments enumerateObjectsUsingBlock:^(FSTDocumentKey *key, BOOL *stop) {
+ result = [result setByRemovingObject:key];
+ }];
+ return result;
+}
+
+- (void)addDocumentKey:(FSTDocumentKey *)documentKey {
+ self.addedDocuments = [self.addedDocuments setByAddingObject:documentKey];
+ self.removedDocuments = [self.removedDocuments setByRemovingObject:documentKey];
+}
+
+- (void)removeDocumentKey:(FSTDocumentKey *)documentKey {
+ self.addedDocuments = [self.addedDocuments setByRemovingObject:documentKey];
+ self.removedDocuments = [self.removedDocuments setByAddingObject:documentKey];
+}
+
+@end
+
+#pragma mark - FSTTargetChange
+
+@interface FSTTargetChange ()
+@property(nonatomic, assign) FSTCurrentStatusUpdate currentStatusUpdate;
+@property(nonatomic, strong, nullable) FSTTargetMapping *mapping;
+@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion;
+@property(nonatomic, strong) NSData *resumeToken;
+@end
+
+@implementation FSTTargetChange
+
+- (instancetype)init {
+ if (self = [super init]) {
+ _currentStatusUpdate = FSTCurrentStatusUpdateNone;
+ _resumeToken = [NSData data];
+ }
+ return self;
+}
+
++ (instancetype)changeWithDocuments:(NSArray<FSTMaybeDocument *> *)docs
+ currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate {
+ FSTUpdateMapping *mapping = [[FSTUpdateMapping alloc] init];
+ for (FSTMaybeDocument *doc in docs) {
+ if ([doc isKindOfClass:[FSTDeletedDocument class]]) {
+ mapping.removedDocuments = [mapping.removedDocuments setByAddingObject:doc.key];
+ } else {
+ mapping.addedDocuments = [mapping.addedDocuments setByAddingObject:doc.key];
+ }
+ }
+ FSTTargetChange *change = [[FSTTargetChange alloc] init];
+ change.mapping = mapping;
+ change.currentStatusUpdate = currentStatusUpdate;
+ return change;
+}
+
++ (instancetype)changeWithMapping:(FSTTargetMapping *)mapping
+ snapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ currentStatusUpdate:(FSTCurrentStatusUpdate)currentStatusUpdate {
+ FSTTargetChange *change = [[FSTTargetChange alloc] init];
+ change.mapping = mapping;
+ change.snapshotVersion = snapshotVersion;
+ change.currentStatusUpdate = currentStatusUpdate;
+ return change;
+}
+
+- (FSTTargetMapping *)mapping {
+ if (!_mapping) {
+ // Create an FSTUpdateMapping by default, since resets are always explicit
+ _mapping = [[FSTUpdateMapping alloc] init];
+ }
+ return _mapping;
+}
+
+/**
+ * Sets the resume token but only when it has a new value. Empty resumeTokens are
+ * discarded.
+ */
+- (void)setResumeToken:(NSData *)resumeToken {
+ if (resumeToken.length > 0) {
+ _resumeToken = resumeToken;
+ }
+}
+
+@end
+
+#pragma mark - FSTRemoteEvent
+
+@interface FSTRemoteEvent () {
+ NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *_documentUpdates;
+ NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *_targetChanges;
+}
+
+- (instancetype)
+initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ targetChanges:(NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges
+ documentUpdates:
+ (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates;
+
+@property(nonatomic, strong) FSTSnapshotVersion *snapshotVersion;
+
+@end
+
+@implementation FSTRemoteEvent
+
++ (instancetype)
+eventWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges
+ documentUpdates:
+ (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates {
+ return [[FSTRemoteEvent alloc] initWithSnapshotVersion:snapshotVersion
+ targetChanges:targetChanges
+ documentUpdates:documentUpdates];
+}
+
+- (instancetype)
+initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ targetChanges:(NSMutableDictionary<NSNumber *, FSTTargetChange *> *)targetChanges
+ documentUpdates:
+ (NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates {
+ self = [super init];
+ if (self) {
+ _snapshotVersion = snapshotVersion;
+ _targetChanges = targetChanges;
+ _documentUpdates = documentUpdates;
+ }
+ return self;
+}
+
+- (NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *)targetChanges {
+ return static_cast<NSDictionary<FSTBoxedTargetID *, FSTTargetChange *> *>(_targetChanges);
+}
+
+- (NSDictionary<FSTDocumentKey *, FSTMaybeDocument *> *)documentUpdates {
+ return static_cast<NSDictionary<FSTDocumentKey *, FSTMaybeDocument *> *>(_documentUpdates);
+}
+
+/** Adds a document update to this remote event */
+- (void)addDocumentUpdate:(FSTMaybeDocument *)document {
+ _documentUpdates[document.key] = document;
+}
+
+/** Handles an existence filter mismatch */
+- (void)handleExistenceFilterMismatchForTargetID:(FSTBoxedTargetID *)targetID {
+ // An existence filter mismatch will reset the query and we need to reset the mapping to contain
+ // no documents and an empty resume token.
+ //
+ // Note:
+ // * The reset mapping is empty, specifically forcing the consumer of the change to
+ // forget all keys for this targetID;
+ // * The resume snapshot for this target must be reset
+ // * The target must be unacked because unwatching and rewatching introduces a race for
+ // changes.
+ //
+ // TODO(dimond): keep track of reset targets not to raise.
+ FSTTargetChange *targetChange =
+ [FSTTargetChange changeWithMapping:[[FSTResetMapping alloc] init]
+ snapshotVersion:[FSTSnapshotVersion noVersion]
+ currentStatusUpdate:FSTCurrentStatusUpdateMarkNotCurrent];
+ _targetChanges[targetID] = targetChange;
+}
+
+@end
+
+#pragma mark - FSTWatchChangeAggregator
+
+@interface FSTWatchChangeAggregator ()
+
+/** The snapshot version for every target change this creates. */
+@property(nonatomic, strong, readonly) FSTSnapshotVersion *snapshotVersion;
+
+/** Keeps track of the current target mappings */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges;
+
+/** Keeps track of document to update */
+@property(nonatomic, strong, readonly)
+ NSMutableDictionary<FSTDocumentKey *, FSTMaybeDocument *> *documentUpdates;
+
+/** The set of open listens on the client */
+@property(nonatomic, strong, readonly)
+ NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *listenTargets;
+
+/** Whether this aggregator was frozen and can no longer be modified */
+@property(nonatomic, assign) BOOL frozen;
+
+@end
+
+@implementation FSTWatchChangeAggregator {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *_existenceFilters;
+}
+
+- (instancetype)
+initWithSnapshotVersion:(FSTSnapshotVersion *)snapshotVersion
+ listenTargets:(NSDictionary<FSTBoxedTargetID *, FSTQueryData *> *)listenTargets
+ pendingTargetResponses:(NSDictionary<FSTBoxedTargetID *, NSNumber *> *)pendingTargetResponses {
+ self = [super init];
+ if (self) {
+ _snapshotVersion = snapshotVersion;
+
+ _frozen = NO;
+ _targetChanges = [NSMutableDictionary dictionary];
+ _listenTargets = listenTargets;
+ _pendingTargetResponses = [NSMutableDictionary dictionaryWithDictionary:pendingTargetResponses];
+
+ _existenceFilters = [NSMutableDictionary dictionary];
+ _documentUpdates = [NSMutableDictionary dictionary];
+ }
+ return self;
+}
+
+- (NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *)existenceFilters {
+ return static_cast<NSDictionary<FSTBoxedTargetID *, FSTExistenceFilter *> *>(_existenceFilters);
+}
+
+- (FSTTargetChange *)targetChangeForTargetID:(FSTBoxedTargetID *)targetID {
+ FSTTargetChange *change = self.targetChanges[targetID];
+ if (!change) {
+ change = [[FSTTargetChange alloc] init];
+ change.snapshotVersion = self.snapshotVersion;
+ self.targetChanges[targetID] = change;
+ }
+ return change;
+}
+
+- (void)addWatchChanges:(NSArray<FSTWatchChange *> *)watchChanges {
+ FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator");
+ for (FSTWatchChange *watchChange in watchChanges) {
+ [self addWatchChange:watchChange];
+ }
+}
+
+- (void)addWatchChange:(FSTWatchChange *)watchChange {
+ FSTAssert(!self.frozen, @"Trying to modify frozen FSTWatchChangeAggregator");
+ if ([watchChange isKindOfClass:[FSTDocumentWatchChange class]]) {
+ [self addDocumentChange:(FSTDocumentWatchChange *)watchChange];
+ } else if ([watchChange isKindOfClass:[FSTWatchTargetChange class]]) {
+ [self addTargetChange:(FSTWatchTargetChange *)watchChange];
+ } else if ([watchChange isKindOfClass:[FSTExistenceFilterWatchChange class]]) {
+ [self addExistenceFilterChange:(FSTExistenceFilterWatchChange *)watchChange];
+ } else {
+ FSTFail(@"Unknown watch change: %@", watchChange);
+ }
+}
+
+- (void)addDocumentChange:(FSTDocumentWatchChange *)docChange {
+ BOOL relevant = NO;
+
+ for (FSTBoxedTargetID *targetID in docChange.updatedTargetIDs) {
+ if ([self isActiveTarget:targetID]) {
+ FSTTargetChange *change = [self targetChangeForTargetID:targetID];
+ [change.mapping addDocumentKey:docChange.documentKey];
+ relevant = YES;
+ }
+ }
+
+ for (FSTBoxedTargetID *targetID in docChange.removedTargetIDs) {
+ if ([self isActiveTarget:targetID]) {
+ FSTTargetChange *change = [self targetChangeForTargetID:targetID];
+ [change.mapping removeDocumentKey:docChange.documentKey];
+ relevant = YES;
+ }
+ }
+
+ // Only update the document if there is a new document to replace, this might be just a target
+ // update instead.
+ if (docChange.document && relevant) {
+ self.documentUpdates[docChange.documentKey] = docChange.document;
+ }
+}
+
+- (void)addTargetChange:(FSTWatchTargetChange *)targetChange {
+ for (FSTBoxedTargetID *targetID in targetChange.targetIDs) {
+ FSTTargetChange *change = [self targetChangeForTargetID:targetID];
+ switch (targetChange.state) {
+ case FSTWatchTargetChangeStateNoChange:
+ if ([self isActiveTarget:targetID]) {
+ // Creating the change above satisfies the semantics of no-change.
+ change.resumeToken = targetChange.resumeToken;
+ }
+ break;
+ case FSTWatchTargetChangeStateAdded:
+ [self recordResponseForTargetID:targetID];
+ if (![self.pendingTargetResponses objectForKey:targetID]) {
+ // We have a freshly added target, so we need to reset any state that we had previously
+ // This can happen e.g. when remove and add back a target for existence filter
+ // mismatches.
+ change.mapping = nil;
+ change.currentStatusUpdate = FSTCurrentStatusUpdateNone;
+ [_existenceFilters removeObjectForKey:targetID];
+ }
+ change.resumeToken = targetChange.resumeToken;
+ break;
+ case FSTWatchTargetChangeStateRemoved:
+ // We need to keep track of removed targets to we can post-filter and remove any target
+ // changes.
+ [self recordResponseForTargetID:targetID];
+ FSTAssert(!targetChange.cause, @"WatchChangeAggregator does not handle errored targets.");
+ break;
+ case FSTWatchTargetChangeStateCurrent:
+ if ([self isActiveTarget:targetID]) {
+ change.currentStatusUpdate = FSTCurrentStatusUpdateMarkCurrent;
+ change.resumeToken = targetChange.resumeToken;
+ }
+ break;
+ case FSTWatchTargetChangeStateReset:
+ if ([self isActiveTarget:targetID]) {
+ // Overwrite any existing target mapping with a reset mapping. Every subsequent update
+ // will modify the reset mapping, not an update mapping.
+ change.mapping = [[FSTResetMapping alloc] init];
+ change.resumeToken = targetChange.resumeToken;
+ }
+ break;
+ default:
+ FSTWarn(@"Unknown target watch change type: %ld", (long)targetChange.state);
+ }
+ }
+}
+
+/**
+ * Records that we got a watch target add/remove by decrementing the number of pending target
+ * responses that we have.
+ */
+- (void)recordResponseForTargetID:(FSTBoxedTargetID *)targetID {
+ NSNumber *count = [self.pendingTargetResponses objectForKey:targetID];
+ int newCount = count ? [count intValue] - 1 : -1;
+ if (newCount == 0) {
+ [self.pendingTargetResponses removeObjectForKey:targetID];
+ } else {
+ [self.pendingTargetResponses setObject:[NSNumber numberWithInt:newCount] forKey:targetID];
+ }
+}
+
+/**
+ * Returns true if the given targetId is active. Active targets are those for which there are no
+ * pending requests to add a listen and are in the current list of targets the client cares about.
+ *
+ * Clients can repeatedly listen and stop listening to targets, so this check is useful in
+ * preventing in preventing race conditions for a target where events arrive but the server hasn't
+ * yet acknowledged the intended change in state.
+ */
+- (BOOL)isActiveTarget:(FSTBoxedTargetID *)targetID {
+ return [self.listenTargets objectForKey:targetID] &&
+ ![self.pendingTargetResponses objectForKey:targetID];
+}
+
+- (void)addExistenceFilterChange:(FSTExistenceFilterWatchChange *)existenceFilterChange {
+ FSTBoxedTargetID *targetID = @(existenceFilterChange.targetID);
+ if ([self isActiveTarget:targetID]) {
+ _existenceFilters[targetID] = existenceFilterChange.filter;
+ }
+}
+
+- (FSTRemoteEvent *)remoteEvent {
+ NSMutableDictionary<FSTBoxedTargetID *, FSTTargetChange *> *targetChanges = self.targetChanges;
+
+ NSMutableArray *targetsToRemove = [NSMutableArray array];
+
+ // Apply any inactive targets.
+ for (FSTBoxedTargetID *targetID in [targetChanges keyEnumerator]) {
+ if (![self isActiveTarget:targetID]) {
+ [targetsToRemove addObject:targetID];
+ }
+ }
+
+ [targetChanges removeObjectsForKeys:targetsToRemove];
+
+ // Mark this aggregator as frozen so no further modifications are made.
+ self.frozen = YES;
+ return [FSTRemoteEvent eventWithSnapshotVersion:self.snapshotVersion
+ targetChanges:targetChanges
+ documentUpdates:self.documentUpdates];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END