459 lines
20 KiB
Objective-C
459 lines
20 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 "FWriteTree.h"
|
|
#import "FImmutableTree.h"
|
|
#import "FPath.h"
|
|
#import "FNode.h"
|
|
#import "FWriteTreeRef.h"
|
|
#import "FChildrenNode.h"
|
|
#import "FNamedNode.h"
|
|
#import "FWriteRecord.h"
|
|
#import "FEmptyNode.h"
|
|
#import "FIndex.h"
|
|
#import "FCompoundWrite.h"
|
|
#import "FCacheNode.h"
|
|
|
|
@interface FWriteTree ()
|
|
/**
|
|
* A tree tracking the results of applying all visible writes. This does not include transactions with
|
|
* applyLocally=false or writes that are completely shadowed by other writes.
|
|
* Contains id<FNode> as values.
|
|
*/
|
|
@property (nonatomic, strong) FCompoundWrite *visibleWrites;
|
|
/**
|
|
* A list of pending writes, regardless of visibility and shadowed-ness. Used to calcuate arbitrary
|
|
* sets of the changed data, such as hidden writes (from transactions) or changes with certain writes excluded (also
|
|
* used by transactions).
|
|
* Contains FWriteRecords.
|
|
*/
|
|
@property (nonatomic, strong) NSMutableArray *allWrites;
|
|
@property (nonatomic) NSInteger lastWriteId;
|
|
@end
|
|
|
|
/**
|
|
* FWriteTree tracks all pending user-initiated writes and has methods to calcuate the result of merging them with
|
|
* underlying server data (to create "event cache" data). Pending writes are added with addOverwriteAtPath: and
|
|
* addMergeAtPath: and removed with removeWriteId:.
|
|
*/
|
|
@implementation FWriteTree
|
|
|
|
@synthesize allWrites;
|
|
@synthesize lastWriteId;
|
|
|
|
- (id) init {
|
|
self = [super init];
|
|
if (self) {
|
|
self.visibleWrites = [FCompoundWrite emptyWrite];
|
|
self.allWrites = [[NSMutableArray alloc] init];
|
|
self.lastWriteId = -1;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
/**
|
|
* Create a new WriteTreeRef for the given path. For use with a new sync point at the given path.
|
|
*/
|
|
- (FWriteTreeRef *) childWritesForPath:(FPath *)path {
|
|
return [[FWriteTreeRef alloc] initWithPath:path writeTree:self];
|
|
}
|
|
|
|
/**
|
|
* Record a new overwrite from user code.
|
|
* @param visible Is set to false by some transactions. It should be excluded from event caches.
|
|
*/
|
|
- (void) addOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible {
|
|
NSAssert(writeId > self.lastWriteId, @"Stacking an older write on top of a newer one");
|
|
FWriteRecord *record = [[FWriteRecord alloc] initWithPath:path overwrite:newData writeId:writeId visible:visible];
|
|
[self.allWrites addObject:record];
|
|
|
|
if (visible) {
|
|
self.visibleWrites = [self.visibleWrites addWrite:newData atPath:path];
|
|
}
|
|
|
|
self.lastWriteId = writeId;
|
|
}
|
|
|
|
/**
|
|
* Record a new merge from user code.
|
|
* @param changedChildren maps NSString -> id<FNode>
|
|
*/
|
|
- (void) addMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId {
|
|
NSAssert(writeId > self.lastWriteId, @"Stacking an older merge on top of newer one");
|
|
FWriteRecord *record = [[FWriteRecord alloc] initWithPath:path merge:changedChildren writeId:writeId];
|
|
[self.allWrites addObject:record];
|
|
|
|
self.visibleWrites = [self.visibleWrites addCompoundWrite:changedChildren atPath:path];
|
|
self.lastWriteId = writeId;
|
|
}
|
|
|
|
- (FWriteRecord *)writeForId:(NSInteger)writeId {
|
|
NSUInteger index = [self.allWrites indexOfObjectPassingTest:^BOOL(FWriteRecord *write, NSUInteger idx, BOOL *stop) {
|
|
return write.writeId == writeId;
|
|
}];
|
|
return (index == NSNotFound) ? nil : self.allWrites[index];
|
|
}
|
|
|
|
/**
|
|
* Remove a write (either an overwrite or merge) that has been successfully acknowledged by the server. Recalculates the
|
|
* tree if necessary. We return the path of the write and whether it may have been visible, meaning views need to
|
|
* reevaluate.
|
|
*
|
|
* @return YES if the write may have been visible (meaning we'll need to reevaluate / raise events as a result).
|
|
*/
|
|
- (BOOL) removeWriteId:(NSInteger)writeId {
|
|
NSUInteger index = [self.allWrites indexOfObjectPassingTest:^BOOL(FWriteRecord *record, NSUInteger idx, BOOL *stop) {
|
|
if (record.writeId == writeId) {
|
|
return YES;
|
|
} else {
|
|
return NO;
|
|
}
|
|
}];
|
|
NSAssert(index != NSNotFound, @"[FWriteTree removeWriteId:] called with nonexistent writeId.");
|
|
FWriteRecord *writeToRemove = self.allWrites[index];
|
|
[self.allWrites removeObjectAtIndex:index];
|
|
|
|
BOOL removedWriteWasVisible = writeToRemove.visible;
|
|
BOOL removedWriteOverlapsWithOtherWrites = NO;
|
|
NSInteger i = [self.allWrites count] - 1;
|
|
|
|
while (removedWriteWasVisible && i >= 0) {
|
|
FWriteRecord *currentWrite = [self.allWrites objectAtIndex:i];
|
|
if (currentWrite.visible) {
|
|
if (i >= index && [self record:currentWrite containsPath:writeToRemove.path]) {
|
|
// The removed write was completely shadowed by a subsequent write.
|
|
removedWriteWasVisible = NO;
|
|
} else if ([writeToRemove.path contains:currentWrite.path]) {
|
|
// Either we're covering some writes or they're covering part of us (depending on which came first).
|
|
removedWriteOverlapsWithOtherWrites = YES;
|
|
}
|
|
}
|
|
i--;
|
|
}
|
|
|
|
if (!removedWriteWasVisible) {
|
|
return NO;
|
|
} else if (removedWriteOverlapsWithOtherWrites) {
|
|
// There's some shadowing going on. Just rebuild the visible writes from scratch.
|
|
[self resetTree];
|
|
return YES;
|
|
} else {
|
|
// There's no shadowing. We can safely just remove the write(s) from visibleWrites.
|
|
if ([writeToRemove isOverwrite]) {
|
|
self.visibleWrites = [self.visibleWrites removeWriteAtPath:writeToRemove.path];
|
|
} else {
|
|
FCompoundWrite *merge = writeToRemove.merge;
|
|
[merge enumerateWrites:^(FPath *path, id<FNode> node, BOOL *stop) {
|
|
self.visibleWrites = [self.visibleWrites removeWriteAtPath:[writeToRemove.path child:path]];
|
|
}];
|
|
}
|
|
return YES;
|
|
}
|
|
}
|
|
|
|
- (NSArray *) removeAllWrites {
|
|
NSArray *writes = self.allWrites;
|
|
self.visibleWrites = [FCompoundWrite emptyWrite];
|
|
self.allWrites = [NSMutableArray array];
|
|
return writes;
|
|
}
|
|
|
|
/**
|
|
* @return A complete snapshot for the given path if there's visible write data at that path, else nil.
|
|
* No server data is considered.
|
|
*/
|
|
- (id <FNode>) completeWriteDataAtPath:(FPath *)path {
|
|
return [self.visibleWrites completeNodeAtPath:path];
|
|
}
|
|
|
|
/**
|
|
* Given optional, underlying server data, and an optional set of constraints (exclude some sets, include hidden
|
|
* writes), attempt to calculate a complete snapshot for the given path
|
|
* @param includeHiddenWrites Defaults to false, whether or not to layer on writes with visible set to false
|
|
*/
|
|
- (id <FNode>) calculateCompleteEventCacheAtPath:(FPath *)treePath completeServerCache:(id <FNode>)completeServerCache
|
|
excludeWriteIds:(NSArray *)writeIdsToExclude includeHiddenWrites:(BOOL)includeHiddenWrites {
|
|
if (writeIdsToExclude == nil && !includeHiddenWrites) {
|
|
id<FNode> shadowingNode = [self.visibleWrites completeNodeAtPath:treePath];
|
|
if (shadowingNode != nil) {
|
|
return shadowingNode;
|
|
} else {
|
|
// No cache here. Can't claim complete knowledge.
|
|
FCompoundWrite *subMerge = [self.visibleWrites childCompoundWriteAtPath:treePath];
|
|
if (subMerge.isEmpty) {
|
|
return completeServerCache;
|
|
} else if (completeServerCache == nil && ![subMerge hasCompleteWriteAtPath:[FPath empty]]) {
|
|
// We wouldn't have a complete snapshot since there's no underlying data and no complete shadow
|
|
return nil;
|
|
} else {
|
|
id<FNode> layeredCache = completeServerCache != nil ? completeServerCache : [FEmptyNode emptyNode];
|
|
return [subMerge applyToNode:layeredCache];
|
|
}
|
|
}
|
|
} else {
|
|
FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
|
|
if (!includeHiddenWrites && merge.isEmpty) {
|
|
return completeServerCache;
|
|
} else {
|
|
// If the server cache is null and we don't have a complete cache, we need to return nil
|
|
if (!includeHiddenWrites && completeServerCache == nil && ![merge hasCompleteWriteAtPath:[FPath empty]]) {
|
|
return nil;
|
|
} else {
|
|
BOOL (^filter) (FWriteRecord *) = ^(FWriteRecord *record) {
|
|
return (BOOL) ((record.visible || includeHiddenWrites) &&
|
|
(writeIdsToExclude == nil || ![writeIdsToExclude containsObject:[NSNumber numberWithInteger:record.writeId]]) &&
|
|
([record.path contains:treePath] || [treePath contains:record.path]));
|
|
};
|
|
FCompoundWrite *mergeAtPath = [FWriteTree layerTreeFromWrites:self.allWrites filter:filter treeRoot:treePath];
|
|
id<FNode> layeredCache = completeServerCache ? completeServerCache : [FEmptyNode emptyNode];
|
|
return [mergeAtPath applyToNode:layeredCache];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* With optional, underlying server data, attempt to return a children node of children that we have complete data for.
|
|
* Used when creating new views, to pre-fill their complete event children snapshot.
|
|
*/
|
|
- (FChildrenNode *) calculateCompleteEventChildrenAtPath:(FPath *)treePath
|
|
completeServerChildren:(id<FNode>)completeServerChildren {
|
|
__block id<FNode> completeChildren = [FEmptyNode emptyNode];
|
|
id<FNode> topLevelSet = [self.visibleWrites completeNodeAtPath:treePath];
|
|
if (topLevelSet != nil) {
|
|
if (![topLevelSet isLeafNode]) {
|
|
// We're shadowing everything. Return the children.
|
|
FChildrenNode *topChildrenNode = topLevelSet;
|
|
[topChildrenNode enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
|
|
completeChildren = [completeChildren updateImmediateChild:key withNewChild:node];
|
|
}];
|
|
}
|
|
return completeChildren;
|
|
} else {
|
|
// Layer any children we have on top of this
|
|
// We know we don't have a top-level set, so just enumerate existing children, and apply any updates
|
|
FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
|
|
[completeServerChildren enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
|
|
FCompoundWrite *childMerge = [merge childCompoundWriteAtPath:[[FPath alloc] initWith:key]];
|
|
id<FNode> newChildNode = [childMerge applyToNode:node];
|
|
completeChildren = [completeChildren updateImmediateChild:key withNewChild:newChildNode];
|
|
}];
|
|
// Add any complete children we have from the set.
|
|
for (FNamedNode *node in merge.completeChildren) {
|
|
completeChildren = [completeChildren updateImmediateChild:node.name withNewChild:node.node];
|
|
}
|
|
return completeChildren;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Given that the underlying server data has updated, determine what, if anything, needs to be applied to the event cache.
|
|
*
|
|
* Possibilities
|
|
*
|
|
* 1. No write are shadowing. Events should be raised, the snap to be applied comes from the server data.
|
|
*
|
|
* 2. Some write is completely shadowing. No events to be raised.
|
|
*
|
|
* 3. Is partially shadowed. Events ..
|
|
*
|
|
* Either existingEventSnap or existingServerSnap must exist.
|
|
*/
|
|
- (id <FNode>) calculateEventCacheAfterServerOverwriteAtPath:(FPath *)treePath childPath:(FPath *)childPath existingEventSnap:(id <FNode>)existingEventSnap existingServerSnap:(id <FNode>)existingServerSnap {
|
|
NSAssert(existingEventSnap != nil || existingServerSnap != nil,
|
|
@"Either existingEventSnap or existingServerSanp must exist.");
|
|
|
|
FPath *path = [treePath child:childPath];
|
|
if ([self.visibleWrites hasCompleteWriteAtPath:path]) {
|
|
// At this point we can probably guarantee that we're in case 2, meaning no events
|
|
// May need to check visibility while doing the findRootMostValueAndPath call
|
|
return nil;
|
|
} else {
|
|
// This could be more efficient if the serverNode + updates doesn't change the eventSnap
|
|
// However this is tricky to find out, since user updates don't necessary change the server
|
|
// snap, e.g. priority updates on empty nodes, or deep deletes. Another special case is if the server
|
|
// adds nodes, but doesn't change any existing writes. It is therefore not enough to
|
|
// only check if the updates change the serverNode.
|
|
// Maybe check if the merge tree contains these special cases and only do a full overwrite in that case?
|
|
FCompoundWrite *childMerge = [self.visibleWrites childCompoundWriteAtPath:path];
|
|
if (childMerge.isEmpty) {
|
|
// We're not shadowing at all. Case 1
|
|
return [existingServerSnap getChild:childPath];
|
|
} else {
|
|
return [childMerge applyToNode:[existingServerSnap getChild:childPath]];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a complete child for a given server snap after applying all user writes or nil if there is no complete child
|
|
* for this child key.
|
|
*/
|
|
- (id<FNode>) calculateCompleteChildAtPath:(FPath *)treePath childKey:(NSString *)childKey cache:(FCacheNode *)existingServerCache {
|
|
FPath *path = [treePath childFromString:childKey];
|
|
id<FNode> shadowingNode = [self.visibleWrites completeNodeAtPath:path];
|
|
if (shadowingNode != nil) {
|
|
return shadowingNode;
|
|
} else {
|
|
if ([existingServerCache isCompleteForChild:childKey]) {
|
|
FCompoundWrite *childMerge = [self.visibleWrites childCompoundWriteAtPath:path];
|
|
return [childMerge applyToNode:[existingServerCache.node getImmediateChild:childKey]];
|
|
} else {
|
|
return nil;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a node if there is a complete overwrite for this path. More specifically, if there is a write at
|
|
* a higher path, this will return the child of that write relative to the write and this path.
|
|
* Returns null if there is no write at this path.
|
|
*/
|
|
- (id<FNode>) shadowingWriteAtPath:(FPath *)path {
|
|
return [self.visibleWrites completeNodeAtPath:path];
|
|
}
|
|
|
|
/**
|
|
* This method is used when processing child remove events on a query. If we can, we pull in children that were outside
|
|
* the window, but may now be in the window.
|
|
*/
|
|
- (FNamedNode *)calculateNextNodeAfterPost:(FNamedNode *)post
|
|
atPath:(FPath *)treePath
|
|
completeServerData:(id<FNode>)completeServerData
|
|
reverse:(BOOL)reverse
|
|
index:(id<FIndex>)index
|
|
{
|
|
__block id<FNode> toIterate;
|
|
FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath];
|
|
id<FNode> shadowingNode = [merge completeNodeAtPath:[FPath empty]];
|
|
if (shadowingNode != nil) {
|
|
toIterate = shadowingNode;
|
|
} else if (completeServerData != nil) {
|
|
toIterate = [merge applyToNode:completeServerData];
|
|
} else {
|
|
return nil;
|
|
}
|
|
|
|
__block NSString *currentNextKey = nil;
|
|
__block id<FNode> currentNextNode = nil;
|
|
[toIterate enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) {
|
|
if ([index compareKey:key andNode:node toOtherKey:post.name andNode:post.node reverse:reverse] > NSOrderedSame &&
|
|
(!currentNextKey || [index compareKey:key andNode:node toOtherKey:currentNextKey andNode:currentNextNode reverse:reverse] < NSOrderedSame)) {
|
|
currentNextKey = key;
|
|
currentNextNode = node;
|
|
}
|
|
}];
|
|
|
|
if (currentNextKey != nil) {
|
|
return [FNamedNode nodeWithName:currentNextKey node:currentNextNode];
|
|
} else {
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Private Methods
|
|
|
|
- (BOOL) record:(FWriteRecord *)record containsPath:(FPath *)path {
|
|
if ([record isOverwrite]) {
|
|
return [record.path contains:path];
|
|
} else {
|
|
__block BOOL contains = NO;
|
|
[record.merge enumerateWrites:^(FPath *childPath, id<FNode> node, BOOL *stop) {
|
|
contains = [[record.path child:childPath] contains:path];
|
|
*stop = contains;
|
|
}];
|
|
return contains;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Re-layer the writes and merges into a tree so we can efficiently calculate event snapshots
|
|
*/
|
|
- (void) resetTree {
|
|
self.visibleWrites = [FWriteTree layerTreeFromWrites:self.allWrites filter:[FWriteTree defaultFilter] treeRoot:[FPath empty]];
|
|
if ([self.allWrites count] > 0) {
|
|
FWriteRecord *lastRecord = self.allWrites[[self.allWrites count] - 1];
|
|
self.lastWriteId = lastRecord.writeId;
|
|
} else {
|
|
self.lastWriteId = -1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The default filter used when constructing the tree. Keep everything that's visible.
|
|
*/
|
|
+ (BOOL (^)(FWriteRecord *record)) defaultFilter {
|
|
static BOOL (^filter)(FWriteRecord *);
|
|
static dispatch_once_t filterToken;
|
|
dispatch_once(&filterToken, ^{
|
|
filter = ^(FWriteRecord *record) {
|
|
return YES;
|
|
};
|
|
});
|
|
return filter;
|
|
}
|
|
|
|
/**
|
|
* Static method. Given an array of WriteRecords, a filter for which ones to include, and a path, construct a merge
|
|
* at that path
|
|
* @return An FImmutableTree of id<FNode>s.
|
|
*/
|
|
+ (FCompoundWrite *) layerTreeFromWrites:(NSArray *)writes filter:(BOOL (^)(FWriteRecord *record))filter treeRoot:(FPath *)treeRoot {
|
|
__block FCompoundWrite *compoundWrite = [FCompoundWrite emptyWrite];
|
|
[writes enumerateObjectsUsingBlock:^(FWriteRecord *record, NSUInteger idx, BOOL *stop) {
|
|
// Theory, a later set will either:
|
|
// a) abort a relevant transaction, so no need to worry about excluding it from calculating that transaction
|
|
// b) not be relevant to a transaction (separate branch), so again will not affect the data for that transaction
|
|
if (filter(record)) {
|
|
FPath *writePath = record.path;
|
|
if ([record isOverwrite]) {
|
|
if ([treeRoot contains:writePath]) {
|
|
FPath *relativePath = [FPath relativePathFrom:treeRoot to:writePath];
|
|
compoundWrite = [compoundWrite addWrite:record.overwrite atPath:relativePath];
|
|
} else if ([writePath contains:treeRoot]) {
|
|
id<FNode> child = [record.overwrite getChild:[FPath relativePathFrom:writePath to:treeRoot]];
|
|
compoundWrite = [compoundWrite addWrite:child atPath:[FPath empty]];
|
|
} else {
|
|
// There is no overlap between root path and write path, ignore write
|
|
}
|
|
} else {
|
|
if ([treeRoot contains:writePath]) {
|
|
FPath *relativePath = [FPath relativePathFrom:treeRoot to:writePath];
|
|
compoundWrite = [compoundWrite addCompoundWrite:record.merge atPath:relativePath];
|
|
} else if ([writePath contains:treeRoot]) {
|
|
FPath *relativePath = [FPath relativePathFrom:writePath to:treeRoot];
|
|
if (relativePath.isEmpty) {
|
|
compoundWrite = [compoundWrite addCompoundWrite:record.merge atPath:[FPath empty]];
|
|
} else {
|
|
id<FNode> child = [record.merge completeNodeAtPath:relativePath];
|
|
if (child != nil) {
|
|
// There exists a child in this node that matches the root path
|
|
id<FNode> deepNode = [child getChild:[relativePath popFront]];
|
|
compoundWrite = [compoundWrite addWrite:deepNode atPath:[FPath empty]];
|
|
}
|
|
}
|
|
} else {
|
|
// There is no overlap between root path and write path, ignore write
|
|
}
|
|
}
|
|
}
|
|
}];
|
|
return compoundWrite;
|
|
}
|
|
|
|
@end
|