/* * 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 @end @implementation FNoCompleteChildSource + (FNoCompleteChildSource *) instance { static FNoCompleteChildSource *source = nil; static dispatch_once_t once; dispatch_once(&once, ^{ source = [[FNoCompleteChildSource alloc] init]; }); return source; } - (id) completeChild:(NSString *)childKey { return nil; } - (FNamedNode *) childByIndex:(id)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 @property (nonatomic, strong) FWriteTreeRef *writes; @property (nonatomic, strong) FViewCache *viewCache; @property (nonatomic, strong) id optCompleteServerCache; @end @implementation FWriteTreeCompleteChildSource - (id) initWithWrites:(FWriteTreeRef *)writes viewCache:(FViewCache *)viewCache serverCache:(id)optCompleteServerCache { self = [super init]; if (self) { self.writes = writes; self.viewCache = viewCache; self.optCompleteServerCache = optCompleteServerCache; } return self; } - (id) 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)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse { id 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 filter; @end @implementation FViewProcessor - (id)initWithFilter:(id)nodeFilter { self = [super init]; if (self) { self.filter = nodeFilter; } return self; } - (FViewProcessorResult *)applyOperationOn:(FViewCache *)oldViewCache operation:(id)operation writesCache:(FWriteTreeRef *)writesCache completeCache:(id )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 %ld.", (long)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)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 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 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 oldEventNode = oldEventSnap.node; id serverNode = viewCache.cachedServerSnap.node; // we might have overwrites for this priority id 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 newEventChild; if ([oldEventSnap isCompleteForChild:childKey]) { id serverNode = viewCache.cachedServerSnap.node; id 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)changedSnap writesCache:(FWriteTreeRef *)writesCache completeCache:(id)optCompleteCache filterServerNode:(BOOL)filterServerNode accumulator:(FChildChangeAccumulator *)accumulator { FCacheNode *oldServerSnap = oldViewCache.cachedServerSnap; FIndexedNode *newServerCache; id 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 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 childNode = [oldServerSnap.node getImmediateChild:childKey]; id 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 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)changedSnap writesCache:(FWriteTreeRef *)writesCache completeCache:(id)optCompleteCache accumulator:(FChildChangeAccumulator *)accumulator { FCacheNode *oldEventSnap = oldViewCache.cachedEventSnap; FViewCache *newViewCache; id 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 oldChild = [oldEventSnap.node getImmediateChild:childKey]; id newChild; if (childChangePath.isEmpty) { // Child overwrite, we can replace the child newChild = changedSnap; } else { id 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) */ - (FViewCache *) applyUserMergeTo:(FViewCache *)viewCache path:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writesCache:(FWriteTreeRef *)writesCache completeCache:(id)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 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 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)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 serverNode = viewCache.cachedServerSnap.node; NSDictionary *childCompoundWrites = actualMerge.childCompoundWrites; [childCompoundWrites enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FCompoundWrite *childMerge, BOOL *stop) { if ([serverNode hasChild:childKey]) { id serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey]; id 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 serverChild = [viewCache.cachedServerSnap.node getImmediateChild:childKey]; id 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 )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)optCompleteCache accumulator:(FChildChangeAccumulator *)accumulator { if ([writesCache shadowingWriteAtPath:path] != nil) { return viewCache; } else { id source = [[FWriteTreeCompleteChildSource alloc] initWithWrites:writesCache viewCache:viewCache serverCache:optCompleteCache]; FIndexedNode *oldEventCache = viewCache.cachedEventSnap.indexedNode; FIndexedNode *newEventCache; if (path.isEmpty || [[path getFront] isEqualToString:@".priority"]) { id 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 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 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)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