aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database/FViewProcessor.m
diff options
context:
space:
mode:
Diffstat (limited to 'Firebase/Database/FViewProcessor.m')
-rw-r--r--Firebase/Database/FViewProcessor.m654
1 files changed, 654 insertions, 0 deletions
diff --git a/Firebase/Database/FViewProcessor.m b/Firebase/Database/FViewProcessor.m
new file mode 100644
index 0000000..41ff91d
--- /dev/null
+++ b/Firebase/Database/FViewProcessor.m
@@ -0,0 +1,654 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FViewProcessor.h"
+#import "FCompleteChildSource.h"
+#import "FWriteTreeRef.h"
+#import "FViewCache.h"
+#import "FCacheNode.h"
+#import "FNode.h"
+#import "FOperation.h"
+#import "FOperationSource.h"
+#import "FChildChangeAccumulator.h"
+#import "FNodeFilter.h"
+#import "FOverwrite.h"
+#import "FMerge.h"
+#import "FAckUserWrite.h"
+#import "FViewProcessorResult.h"
+#import "FIRDataEventType.h"
+#import "FChange.h"
+#import "FEmptyNode.h"
+#import "FChildrenNode.h"
+#import "FPath.h"
+#import "FKeyIndex.h"
+#import "FCompoundWrite.h"
+#import "FImmutableTree.h"
+
+/**
+* An implementation of FCompleteChildSource that never returns any additional children
+*/
+@interface FNoCompleteChildSource: NSObject<FCompleteChildSource>
+@end
+
+@implementation FNoCompleteChildSource
++ (FNoCompleteChildSource *) instance {
+ static FNoCompleteChildSource *source = nil;
+ static dispatch_once_t once;
+ dispatch_once(&once, ^{
+ source = [[FNoCompleteChildSource alloc] init];
+ });
+ return source;
+}
+
+- (id<FNode>) completeChild:(NSString *)childKey {
+ return nil;
+}
+
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse {
+ return nil;
+}
+@end
+
+/**
+* An implementation of FCompleteChildSource that uses a FWriteTree in addition to any other server data or
+* old event caches available to calculate complete children.
+*/
+@interface FWriteTreeCompleteChildSource: NSObject<FCompleteChildSource>
+@property (nonatomic, strong) FWriteTreeRef *writes;
+@property (nonatomic, strong) FViewCache *viewCache;
+@property (nonatomic, strong) id<FNode> optCompleteServerCache;
+@end
+
+@implementation FWriteTreeCompleteChildSource
+- (id) initWithWrites:(FWriteTreeRef *)writes viewCache:(FViewCache *)viewCache serverCache:(id<FNode>)optCompleteServerCache {
+ self = [super init];
+ if (self) {
+ self.writes = writes;
+ self.viewCache = viewCache;
+ self.optCompleteServerCache = optCompleteServerCache;
+ }
+ return self;
+}
+
+- (id<FNode>) completeChild:(NSString *)childKey {
+ FCacheNode *node = self.viewCache.cachedEventSnap;
+ if ([node isCompleteForChild:childKey]) {
+ return [node.node getImmediateChild:childKey];
+ } else {
+ FCacheNode *serverNode;
+ if (self.optCompleteServerCache) {
+ // Since we're only ever getting child nodes, we can use the key index here
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:self.optCompleteServerCache index:[FKeyIndex keyIndex]];
+ serverNode = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:YES isFiltered:NO];
+ } else {
+ serverNode = self.viewCache.cachedServerSnap;
+ }
+ return [self.writes calculateCompleteChild:childKey cache:serverNode];
+ }
+}
+
+- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse {
+ id<FNode> completeServerData = self.optCompleteServerCache != nil
+ ? self.optCompleteServerCache
+ : self.viewCache.completeServerSnap;
+ return [self.writes calculateNextNodeAfterPost:child
+ completeServerData:completeServerData
+ reverse:reverse
+ index:index];
+}
+
+@end
+
+@interface FViewProcessor ()
+@property (nonatomic, strong) id<FNodeFilter> filter;
+@end
+
+@implementation FViewProcessor
+
+- (id)initWithFilter:(id<FNodeFilter>)nodeFilter {
+ self = [super init];
+ if (self) {
+ self.filter = nodeFilter;
+ }
+ return self;
+}
+
+- (FViewProcessorResult *)applyOperationOn:(FViewCache *)oldViewCache operation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache completeCache:(id <FNode>)optCompleteCache {
+ FChildChangeAccumulator *accumulator = [[FChildChangeAccumulator alloc] init];
+ FViewCache *newViewCache;
+
+ if (operation.type == FOperationTypeOverwrite) {
+ FOverwrite *overwrite = (FOverwrite *) operation;
+ if (operation.source.fromUser) {
+ newViewCache = [self applyUserOverwriteTo:oldViewCache
+ changePath:overwrite.path
+ changedSnap:overwrite.snap
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ NSAssert(operation.source.fromServer, @"Unknown source for overwrite.");
+ // We filter the node if it's a tagged update or the node has been previously filtered and the update is
+ // not at the root in which case it is ok (and necessary) to mark the node unfiltered again
+ BOOL filterServerNode = overwrite.source.isTagged || (oldViewCache.cachedServerSnap.isFiltered &&
+ !overwrite.path.isEmpty);
+ newViewCache = [self applyServerOverwriteTo:oldViewCache
+ changePath:overwrite.path
+ snap:overwrite.snap
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeMerge) {
+ FMerge *merge = (FMerge*)operation;
+ if (operation.source.fromUser) {
+ newViewCache = [self applyUserMergeTo:oldViewCache
+ path:merge.path
+ changedChildren:merge.children
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ NSAssert(operation.source.fromServer, @"Unknown source for merge.");
+ // We filter the node if it's a tagged update or the node has been previously filtered
+ BOOL filterServerNode = merge.source.isTagged || oldViewCache.cachedServerSnap.isFiltered;
+ newViewCache = [self applyServerMergeTo:oldViewCache
+ path:merge.path
+ changedChildren:merge.children
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeAckUserWrite) {
+ FAckUserWrite *ackWrite = (FAckUserWrite *) operation;
+ if (!ackWrite.revert) {
+ newViewCache = [self ackUserWriteOn:oldViewCache
+ ackPath:ackWrite.path
+ affectedTree:ackWrite.affectedTree
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ newViewCache = [self revertUserWriteOn:oldViewCache
+ path:ackWrite.path
+ writesCache:writesCache
+ completeCache:optCompleteCache
+ accumulator:accumulator];
+ }
+ } else if (operation.type == FOperationTypeListenComplete) {
+ newViewCache = [self listenCompleteOldCache:oldViewCache
+ path:operation.path
+ writesCache:writesCache
+ serverCache:optCompleteCache
+ accumulator:accumulator];
+ } else {
+ [NSException raise:NSInternalInconsistencyException format:@"Unknown operation encountered %zd.", operation.type];
+ return nil;
+ }
+
+ NSArray *changes = [self maybeAddValueFromOldViewCache:oldViewCache newViewCache:newViewCache changes:accumulator.changes];
+ FViewProcessorResult *results = [[FViewProcessorResult alloc] initWithViewCache:newViewCache changes:changes];
+ return results;
+}
+
+- (NSArray *) maybeAddValueFromOldViewCache:(FViewCache *)oldViewCache newViewCache:(FViewCache *)newViewCache changes:(NSArray *)changes {
+ NSArray *newChanges = changes;
+ FCacheNode *eventSnap = newViewCache.cachedEventSnap;
+ if (eventSnap.isFullyInitialized) {
+ BOOL isLeafOrEmpty = eventSnap.node.isLeafNode || eventSnap.node.isEmpty;
+ if ([changes count] > 0 ||
+ !oldViewCache.cachedEventSnap.isFullyInitialized ||
+ (isLeafOrEmpty && ![eventSnap.node isEqual:oldViewCache.completeEventSnap]) ||
+ ![eventSnap.node.getPriority isEqual:oldViewCache.completeEventSnap.getPriority]) {
+ FChange *valueChange = [[FChange alloc] initWithType:FIRDataEventTypeValue indexedNode:eventSnap.indexedNode];
+ NSMutableArray *mutableChanges = [changes mutableCopy];
+ [mutableChanges addObject:valueChange];
+ newChanges = mutableChanges;
+ }
+ }
+ return newChanges;
+}
+
+- (FViewCache *) generateEventCacheAfterServerEvent:(FViewCache *)viewCache
+ path:(FPath *)changePath
+ writesCache:(FWriteTreeRef *)writesCache
+ source:(id<FCompleteChildSource>)source
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldEventSnap = viewCache.cachedEventSnap;
+ if ([writesCache shadowingWriteAtPath:changePath] != nil) {
+ // we have a shadowing write, ignore changes.
+ return viewCache;
+ } else {
+ FIndexedNode *newEventCache;
+ if (changePath.isEmpty) {
+ // TODO: figure out how this plays with "sliding ack windows"
+ NSAssert(viewCache.cachedServerSnap.isFullyInitialized, @"If change path is empty, we must have complete server data");
+ id<FNode> nodeWithLocalWrites;
+ if (viewCache.cachedServerSnap.isFiltered) {
+ // We need to special case this, because we need to only apply writes to complete children, or
+ // we might end up raising events for incomplete children. If the server data is filtered deep
+ // writes cannot be guaranteed to be complete
+ id<FNode> serverCache = viewCache.completeServerSnap;
+ FChildrenNode *completeChildren = ([serverCache isKindOfClass:[FChildrenNode class]]) ? serverCache : [FEmptyNode emptyNode];
+ nodeWithLocalWrites = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:completeChildren];
+ } else {
+ nodeWithLocalWrites = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ }
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:nodeWithLocalWrites index:self.filter.index];
+ newEventCache = [self.filter updateFullNode:viewCache.cachedEventSnap.indexedNode
+ withNewNode:indexedNode
+ accumulator:accumulator];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if ([childKey isEqualToString:@".priority"]) {
+ NSAssert(changePath.length == 1, @"Can't have a priority with additional path components");
+ id<FNode> oldEventNode = oldEventSnap.node;
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+ // we might have overwrites for this priority
+ id<FNode> updatedPriority = [writesCache calculateEventCacheAfterServerOverwriteWithChildPath:changePath
+ existingEventSnap:oldEventNode
+ existingServerSnap:serverNode];
+ if (updatedPriority != nil) {
+ newEventCache = [self.filter updatePriority:updatedPriority forNode:oldEventSnap.indexedNode];
+ } else {
+ // priority didn't change, keep old node
+ newEventCache = oldEventSnap.indexedNode;
+ }
+ } else {
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> newEventChild;
+ if ([oldEventSnap isCompleteForChild:childKey]) {
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+ id<FNode> eventChildUpdate = [writesCache calculateEventCacheAfterServerOverwriteWithChildPath:changePath existingEventSnap:oldEventSnap.node existingServerSnap:serverNode];
+ if (eventChildUpdate != nil) {
+ newEventChild = [[oldEventSnap.node getImmediateChild:childKey] updateChild:childChangePath withNewChild:eventChildUpdate];
+ } else {
+ // Nothing changed, just keep the old child
+ newEventChild = [oldEventSnap.node getImmediateChild:childKey];
+ }
+ } else {
+ newEventChild = [writesCache calculateCompleteChild:childKey cache:viewCache.cachedServerSnap];
+ }
+ if (newEventChild != nil) {
+ newEventCache = [self.filter updateChildIn:oldEventSnap.indexedNode
+ forChildKey:childKey
+ newChild:newEventChild
+ affectedPath:childChangePath
+ fromSource:source
+ accumulator:accumulator];
+ } else {
+ // No complete children available or no change
+ newEventCache = oldEventSnap.indexedNode;
+ }
+ }
+ }
+ return [viewCache updateEventSnap:newEventCache
+ isComplete:(oldEventSnap.isFullyInitialized || changePath.isEmpty)
+ isFiltered:self.filter.filtersNodes];
+ }
+}
+
+- (FViewCache *) applyServerOverwriteTo:(FViewCache *)oldViewCache changePath:(FPath *)changePath snap:(id<FNode>)changedSnap
+ writesCache:(FWriteTreeRef *)writesCache completeCache:(id<FNode>)optCompleteCache
+ filterServerNode:(BOOL)filterServerNode accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldServerSnap = oldViewCache.cachedServerSnap;
+ FIndexedNode *newServerCache;
+ id<FNodeFilter> serverFilter = filterServerNode ? self.filter : self.filter.indexedFilter;
+
+ if (changePath.isEmpty) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:changedSnap index:serverFilter.index];
+ newServerCache = [serverFilter updateFullNode:oldServerSnap.indexedNode withNewNode:indexed accumulator:nil];
+ } else if (serverFilter.filtersNodes && !oldServerSnap.isFiltered) {
+ // We want to filter the server node, but we didn't filter the server node yet, so simulate a full update
+ NSAssert(![changePath isEmpty], @"An empty path should been caught in the other branch");
+ NSString *childKey = [changePath getFront];
+ FPath *updatePath = [changePath popFront];
+ id<FNode> newChild = [[oldServerSnap.node getImmediateChild:childKey] updateChild:updatePath
+ withNewChild:changedSnap];
+ FIndexedNode *indexed = [oldServerSnap.indexedNode updateChild:childKey withNewChild:newChild];
+ newServerCache = [serverFilter updateFullNode:oldServerSnap.indexedNode withNewNode:indexed accumulator:nil];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if (![oldServerSnap isCompleteForPath:changePath] && changePath.length > 1) {
+ // We don't update incomplete nodes with updates intended for other listeners.
+ return oldViewCache;
+ }
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> childNode = [oldServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChildNode = [childNode updateChild:childChangePath withNewChild:changedSnap];
+ if ([childKey isEqualToString:@".priority"]) {
+ newServerCache = [serverFilter updatePriority:newChildNode forNode:oldServerSnap.indexedNode];
+ } else {
+ newServerCache = [serverFilter updateChildIn:oldServerSnap.indexedNode
+ forChildKey:childKey
+ newChild:newChildNode
+ affectedPath:childChangePath
+ fromSource:[FNoCompleteChildSource instance]
+ accumulator:nil];
+ }
+ }
+ FViewCache *newViewCache = [oldViewCache updateServerSnap:newServerCache
+ isComplete:(oldServerSnap.isFullyInitialized || changePath.isEmpty)
+ isFiltered:serverFilter.filtersNodes];
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:newViewCache
+ serverCache:optCompleteCache];
+ return [self generateEventCacheAfterServerEvent:newViewCache
+ path:changePath
+ writesCache:writesCache
+ source:source
+ accumulator:accumulator];
+}
+
+- (FViewCache *) applyUserOverwriteTo:(FViewCache *)oldViewCache
+ changePath:(FPath *)changePath
+ changedSnap:(id<FNode>)changedSnap
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldEventSnap = oldViewCache.cachedEventSnap;
+ FViewCache *newViewCache;
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:oldViewCache
+ serverCache:optCompleteCache];
+ if (changePath.isEmpty) {
+ FIndexedNode *newIndexed = [FIndexedNode indexedNodeWithNode:changedSnap index:self.filter.index];
+ FIndexedNode *newEventCache = [self.filter updateFullNode:oldEventSnap.indexedNode
+ withNewNode:newIndexed
+ accumulator:accumulator];
+ newViewCache = [oldViewCache updateEventSnap:newEventCache isComplete:YES isFiltered:self.filter.filtersNodes];
+ } else {
+ NSString *childKey = [changePath getFront];
+ if ([childKey isEqualToString:@".priority"]) {
+ FIndexedNode *newEventCache = [self.filter updatePriority:changedSnap
+ forNode:oldViewCache.cachedEventSnap.indexedNode];
+ newViewCache = [oldViewCache updateEventSnap:newEventCache
+ isComplete:oldEventSnap.isFullyInitialized
+ isFiltered:oldEventSnap.isFiltered];
+ } else {
+ FPath *childChangePath = [changePath popFront];
+ id<FNode> oldChild = [oldEventSnap.node getImmediateChild:childKey];
+ id<FNode> newChild;
+ if (childChangePath.isEmpty) {
+ // Child overwrite, we can replace the child
+ newChild = changedSnap;
+ } else {
+ id<FNode> childNode = [source completeChild:childKey];
+ if (childNode != nil) {
+ if ([[childChangePath getBack] isEqualToString:@".priority"] && [childNode getChild:[childChangePath parent]].isEmpty) {
+ // This is a priority update on an empty node. If this node exists on the server, the server
+ // will send down the priority in the update, so ignore for now
+ newChild = childNode;
+ } else {
+ newChild = [childNode updateChild:childChangePath withNewChild:changedSnap];
+ }
+ } else {
+ newChild = [FEmptyNode emptyNode];
+ }
+ }
+ if (![oldChild isEqual:newChild]) {
+ FIndexedNode *newEventSnap = [self.filter updateChildIn:oldEventSnap.indexedNode
+ forChildKey:childKey
+ newChild:newChild
+ affectedPath:childChangePath
+ fromSource:source
+ accumulator:accumulator];
+ newViewCache = [oldViewCache updateEventSnap:newEventSnap isComplete:oldEventSnap.isFullyInitialized isFiltered:self.filter.filtersNodes];
+ } else {
+ newViewCache = oldViewCache;
+ }
+ }
+ }
+ return newViewCache;
+}
+
++ (BOOL) cache:(FViewCache *)viewCache hasChild:(NSString *)childKey {
+ return [viewCache.cachedEventSnap isCompleteForChild:childKey];
+}
+
+/**
+* @param changedChildren NSDictionary of child name (NSString*) to child value (id<FNode>)
+*/
+- (FViewCache *) applyUserMergeTo:(FViewCache *)viewCache
+ path:(FPath *)path
+ changedChildren:(FCompoundWrite *)changedChildren
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)serverCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ // HACK: In the case of a limit query, there may be some changes that bump things out of the
+ // window leaving room for new items. It's important we process these changes first, so we
+ // iterate the changes twice, first processing any that affect items currently in view.
+ // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
+ // and event snap. I'm not sure if this will result in edge cases when a child is in one but
+ // not the other.
+ __block FViewCache *curViewCache = viewCache;
+
+ [changedChildren enumerateWrites:^(FPath *relativePath, id<FNode> childNode, BOOL *stop) {
+ FPath *writePath = [path child:relativePath];
+ if ([FViewProcessor cache:viewCache hasChild:[writePath getFront]]) {
+ curViewCache = [self applyUserOverwriteTo:curViewCache
+ changePath:writePath
+ changedSnap:childNode
+ writesCache:writesCache
+ completeCache:serverCache
+ accumulator:accumulator];
+ }
+ }];
+
+ [changedChildren enumerateWrites:^(FPath *relativePath, id<FNode> childNode, BOOL *stop) {
+ FPath *writePath = [path child:relativePath];
+ if (![FViewProcessor cache:viewCache hasChild:[writePath getFront]]) {
+ curViewCache = [self applyUserOverwriteTo:curViewCache
+ changePath:writePath
+ changedSnap:childNode
+ writesCache:writesCache
+ completeCache:serverCache
+ accumulator:accumulator];
+ }
+ }];
+
+ return curViewCache;
+}
+
+- (FViewCache *) applyServerMergeTo:(FViewCache *)viewCache
+ path:(FPath *)path
+ changedChildren:(FCompoundWrite *)changedChildren
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)serverCache
+ filterServerNode:(BOOL)filterServerNode
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ // If we don't have a cache yet, this merge was intended for a previously listen in the same location. Ignore it and
+ // wait for the complete data update coming soon.
+ if (viewCache.cachedServerSnap.node.isEmpty && !viewCache.cachedServerSnap.isFullyInitialized) {
+ return viewCache;
+ }
+
+ // HACK: In the case of a limit query, there may be some changes that bump things out of the
+ // window leaving room for new items. It's important we process these changes first, so we
+ // iterate the changes twice, first processing any that affect items currently in view.
+ // TODO: I consider an item "in view" if cacheHasChild is true, which checks both the server
+ // and event snap. I'm not sure if this will result in edge cases when a child is in one but
+ // not the other.
+ __block FViewCache *curViewCache = viewCache;
+ FCompoundWrite *actualMerge;
+ if (path.isEmpty) {
+ actualMerge = changedChildren;
+ } else {
+ actualMerge = [[FCompoundWrite emptyWrite] addCompoundWrite:changedChildren atPath:path];
+ }
+ id<FNode> serverNode = viewCache.cachedServerSnap.node;
+
+ NSDictionary *childCompoundWrites = actualMerge.childCompoundWrites;
+ [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) {
+ if ([serverNode hasChild:childKey]) {
+ id<FNode> serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChild = [childMerge applyToNode:serverChild];
+ curViewCache = [self applyServerOverwriteTo:curViewCache
+ changePath:[[FPath alloc] initWith:childKey]
+ snap:newChild
+ writesCache:writesCache
+ completeCache:serverCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ }];
+
+ [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) {
+ bool isUnknownDeepMerge = ![viewCache.cachedServerSnap isCompleteForChild:childKey] && childMerge.rootWrite == nil;
+ if (![serverNode hasChild:childKey] && !isUnknownDeepMerge) {
+ id<FNode> serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey];
+ id<FNode> newChild = [childMerge applyToNode:serverChild];
+ curViewCache = [self applyServerOverwriteTo:curViewCache
+ changePath:[[FPath alloc] initWith:childKey]
+ snap:newChild
+ writesCache:writesCache
+ completeCache:serverCache
+ filterServerNode:filterServerNode
+ accumulator:accumulator];
+ }
+ }];
+
+ return curViewCache;
+}
+
+- (FViewCache *) ackUserWriteOn:(FViewCache *)viewCache
+ ackPath:(FPath *)ackPath
+ affectedTree:(FImmutableTree *)affectedTree
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id <FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+
+ if ([writesCache shadowingWriteAtPath:ackPath] != nil) {
+ return viewCache;
+ }
+
+ // Only filter server node if it is currently filtered
+ BOOL filterServerNode = viewCache.cachedServerSnap.isFiltered;
+
+ // Essentially we'll just get our existing server cache for the affected paths and re-apply it as a server update
+ // now that it won't be shadowed.
+ FCacheNode *serverCache = viewCache.cachedServerSnap;
+ if (affectedTree.value != nil) {
+ // This is an overwrite.
+ if ((ackPath.isEmpty && serverCache.isFullyInitialized) || [serverCache isCompleteForPath:ackPath]) {
+ return [self applyServerOverwriteTo:viewCache changePath:ackPath snap:[serverCache.node getChild:ackPath]
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ } else if (ackPath.isEmpty) {
+ // This is a goofy edge case where we are acking data at this location but don't have full data. We
+ // should just re-apply whatever we have in our cache as a merge.
+ FCompoundWrite *changedChildren = [FCompoundWrite emptyWrite];
+ for(FNamedNode *child in serverCache.node.childEnumerator) {
+ changedChildren = [changedChildren addWrite:child.node atKey:child.name];
+ }
+ return [self applyServerMergeTo:viewCache path:ackPath changedChildren:changedChildren
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ } else {
+ return viewCache;
+ }
+ } else {
+ // This is a merge.
+ __block FCompoundWrite *changedChildren = [FCompoundWrite emptyWrite];
+ [affectedTree forEach:^(FPath *mergePath, id value) {
+ FPath *serverCachePath = [ackPath child:mergePath];
+ if ([serverCache isCompleteForPath:serverCachePath]) {
+ changedChildren = [changedChildren addWrite:[serverCache.node getChild:serverCachePath] atPath:mergePath];
+ }
+ }];
+ return [self applyServerMergeTo:viewCache path:ackPath changedChildren:changedChildren
+ writesCache:writesCache completeCache:optCompleteCache
+ filterServerNode:filterServerNode accumulator:accumulator];
+ }
+}
+
+- (FViewCache *) revertUserWriteOn:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ completeCache:(id<FNode>)optCompleteCache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ if ([writesCache shadowingWriteAtPath:path] != nil) {
+ return viewCache;
+ } else {
+ id<FCompleteChildSource> source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache
+ viewCache:viewCache
+ serverCache:optCompleteCache];
+ FIndexedNode *oldEventCache = viewCache.cachedEventSnap.indexedNode;
+ FIndexedNode *newEventCache;
+ if (path.isEmpty || [[path getFront] isEqualToString:@".priority"]) {
+ id<FNode> newNode;
+ if (viewCache.cachedServerSnap.isFullyInitialized) {
+ newNode = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ } else {
+ newNode = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:viewCache.cachedServerSnap.node];
+ }
+ FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:newNode index:self.filter.index];
+ newEventCache = [self.filter updateFullNode:oldEventCache withNewNode:indexedNode accumulator:accumulator];
+ } else {
+ NSString *childKey = [path getFront];
+ id<FNode> newChild = [writesCache calculateCompleteChild:childKey cache:viewCache.cachedServerSnap];
+ if (newChild == nil && [viewCache.cachedServerSnap isCompleteForChild:childKey]) {
+ newChild = [oldEventCache.node getImmediateChild:childKey];
+ }
+ if (newChild != nil) {
+ newEventCache = [self.filter updateChildIn:oldEventCache
+ forChildKey:childKey
+ newChild:newChild
+ affectedPath:[path popFront]
+ fromSource:source
+ accumulator:accumulator];
+ } else if (newChild == nil && [viewCache.cachedEventSnap.node hasChild:childKey]) {
+ // No complete child available, delete the existing one, if any
+ newEventCache = [self.filter updateChildIn:oldEventCache
+ forChildKey:childKey
+ newChild:[FEmptyNode emptyNode]
+ affectedPath:[path popFront]
+ fromSource:source
+ accumulator:accumulator];
+ } else {
+ newEventCache = oldEventCache;
+ }
+ if (newEventCache.node.isEmpty && viewCache.cachedServerSnap.isFullyInitialized) {
+ // We might have reverted all child writes. Maybe the old event was a leaf node.
+ id<FNode> complete = [writesCache calculateCompleteEventCacheWithCompleteServerCache:viewCache.completeServerSnap];
+ if (complete.isLeafNode) {
+ FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:complete];
+ newEventCache = [self.filter updateFullNode:newEventCache
+ withNewNode:indexed
+ accumulator:accumulator];
+ }
+ }
+ }
+ BOOL complete = viewCache.cachedServerSnap.isFullyInitialized || [writesCache shadowingWriteAtPath:[FPath empty]] != nil;
+ return [viewCache updateEventSnap:newEventCache isComplete:complete isFiltered:self.filter.filtersNodes];
+ }
+}
+
+- (FViewCache *) listenCompleteOldCache:(FViewCache *)viewCache
+ path:(FPath *)path
+ writesCache:(FWriteTreeRef *)writesCache
+ serverCache:(id<FNode>)servercache
+ accumulator:(FChildChangeAccumulator *)accumulator {
+ FCacheNode *oldServerNode = viewCache.cachedServerSnap;
+ FViewCache *newViewCache = [viewCache updateServerSnap:oldServerNode.indexedNode
+ isComplete:(oldServerNode.isFullyInitialized || path.isEmpty)
+ isFiltered:oldServerNode.isFiltered];
+ return [self generateEventCacheAfterServerEvent:newViewCache path:path writesCache:writesCache source:[FNoCompleteChildSource instance] accumulator:accumulator];
+}
+
+@end