mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
832 lines
39 KiB
Objective-C
832 lines
39 KiB
Objective-C
/*
|
|
* 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 "FAckUserWrite.h"
|
|
#import "FCacheNode.h"
|
|
#import "FChange.h"
|
|
#import "FChildChangeAccumulator.h"
|
|
#import "FChildrenNode.h"
|
|
#import "FCompleteChildSource.h"
|
|
#import "FCompoundWrite.h"
|
|
#import "FEmptyNode.h"
|
|
#import "FIRDataEventType.h"
|
|
#import "FImmutableTree.h"
|
|
#import "FKeyIndex.h"
|
|
#import "FMerge.h"
|
|
#import "FNode.h"
|
|
#import "FNodeFilter.h"
|
|
#import "FOperation.h"
|
|
#import "FOperationSource.h"
|
|
#import "FOverwrite.h"
|
|
#import "FPath.h"
|
|
#import "FViewCache.h"
|
|
#import "FViewProcessorResult.h"
|
|
#import "FWriteTreeRef.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 %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<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
|