/* * 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 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 )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 */ - (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 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 ) 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 ) calculateCompleteEventCacheAtPath:(FPath *)treePath completeServerCache:(id )completeServerCache excludeWriteIds:(NSArray *)writeIdsToExclude includeHiddenWrites:(BOOL)includeHiddenWrites { if (writeIdsToExclude == nil && !includeHiddenWrites) { id 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 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 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)completeServerChildren { __block id completeChildren = [FEmptyNode emptyNode]; id 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 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 node, BOOL *stop) { FCompoundWrite *childMerge = [merge childCompoundWriteAtPath:[[FPath alloc] initWith:key]]; id 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 ) calculateEventCacheAfterServerOverwriteAtPath:(FPath *)treePath childPath:(FPath *)childPath existingEventSnap:(id )existingEventSnap existingServerSnap:(id )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) calculateCompleteChildAtPath:(FPath *)treePath childKey:(NSString *)childKey cache:(FCacheNode *)existingServerCache { FPath *path = [treePath childFromString:childKey]; id 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) 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)completeServerData reverse:(BOOL)reverse index:(id)index { __block id toIterate; FCompoundWrite *merge = [self.visibleWrites childCompoundWriteAtPath:treePath]; id 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 currentNextNode = nil; [toIterate enumerateChildrenUsingBlock:^(NSString *key, id 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 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 ids. */ + (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 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 child = [record.merge completeNodeAtPath:relativePath]; if (child != nil) { // There exists a child in this node that matches the root path id 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