/* * 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 "FLimitedFilter.h" #import "FChildChangeAccumulator.h" #import "FIndex.h" #import "FRangedFilter.h" #import "FQueryParams.h" #import "FQueryParams.h" #import "FNamedNode.h" #import "FEmptyNode.h" #import "FChildrenNode.h" #import "FCompleteChildSource.h" #import "FChange.h" #import "FTreeSortedDictionary.h" @interface FLimitedFilter () @property (nonatomic, strong) FRangedFilter *rangedFilter; @property (nonatomic, strong, readwrite) id index; @property (nonatomic) NSInteger limit; @property (nonatomic) BOOL reverse; @end @implementation FLimitedFilter - (id) initWithQueryParams:(FQueryParams *)params { self = [super init]; if (self) { self.rangedFilter = [[FRangedFilter alloc] initWithQueryParams:params]; self.index = params.index; self.limit = params.limit; self.reverse = !params.isViewFromLeft; } return self; } - (FIndexedNode *)updateChildIn:(FIndexedNode *)oldSnap forChildKey:(NSString *)childKey newChild:(id)newChildSnap affectedPath:(FPath *)affectedPath fromSource:(id)source accumulator:(FChildChangeAccumulator *)optChangeAccumulator { if (![self.rangedFilter matchesKey:childKey andNode:newChildSnap]) { newChildSnap = [FEmptyNode emptyNode]; } if ([[oldSnap.node getImmediateChild:childKey] isEqual:newChildSnap]) { // No change return oldSnap; } else if (oldSnap.node.numChildren < self.limit) { return [[self.rangedFilter indexedFilter] updateChildIn:oldSnap forChildKey:childKey newChild:newChildSnap affectedPath:affectedPath fromSource:source accumulator:optChangeAccumulator]; } else { return [self fullLimitUpdateNode:oldSnap forChildKey:childKey newChild:newChildSnap fromSource:source accumulator:optChangeAccumulator]; } } - (FIndexedNode *)fullLimitUpdateNode:(FIndexedNode *)oldIndexed forChildKey:(NSString *)childKey newChild:(id)newChildSnap fromSource:(id)source accumulator:(FChildChangeAccumulator *)optChangeAccumulator { NSAssert(oldIndexed.node.numChildren == self.limit, @"Should have number of children equal to limit."); FNamedNode *windowBoundary = self.reverse ? oldIndexed.firstChild : oldIndexed.lastChild; BOOL inRange = [self.rangedFilter matchesKey:childKey andNode:newChildSnap]; if ([oldIndexed.node hasChild:childKey]) { // `childKey` was already in `oldSnap`. Figure out if it remains in the window or needs to be replaced. id oldChildSnap = [oldIndexed.node getImmediateChild:childKey]; // In case the `newChildSnap` falls outside the window, get the `nextChild` that might replace it. FNamedNode *nextChild = [source childByIndex:self.index afterChild:windowBoundary isReverse:(BOOL)self.reverse]; if (nextChild != nil && ([nextChild.name isEqualToString:childKey] || [oldIndexed.node hasChild:nextChild.name])) { // There is a weird edge case where a node is updated as part of a merge in the write tree, but hasn't // been applied to the limited filter yet. Ignore this next child which will be updated later in // the limited filter... nextChild = [source childByIndex:self.index afterChild:nextChild isReverse:self.reverse]; } // Figure out if `newChildSnap` is in range and ordered before `nextChild` BOOL remainsInWindow = inRange && !newChildSnap.isEmpty; remainsInWindow = remainsInWindow && (!nextChild || [self.index compareKey:nextChild.name andNode:nextChild.node toOtherKey:childKey andNode:newChildSnap reverse:self.reverse] >= NSOrderedSame); if (remainsInWindow) { // `newChildSnap` is ordered before `nextChild`, so it's a child changed event if (optChangeAccumulator != nil) { FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap] childKey:childKey oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]]; [optChangeAccumulator trackChildChange:change]; } return [oldIndexed updateChild:childKey withNewChild:newChildSnap]; } else { // `newChildSnap` is ordered after `nextChild`, so it's a child removed event if (optChangeAccumulator != nil) { FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved indexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap] childKey:childKey]; [optChangeAccumulator trackChildChange:change]; } FIndexedNode *newIndexed = [oldIndexed updateChild:childKey withNewChild:[FEmptyNode emptyNode]]; // We need to check if the `nextChild` is actually in range before adding it BOOL nextChildInRange = (nextChild != nil) && [self.rangedFilter matchesKey:nextChild.name andNode:nextChild.node]; if (nextChildInRange) { if (optChangeAccumulator != nil) { FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded indexedNode:[FIndexedNode indexedNodeWithNode:nextChild.node] childKey:nextChild.name]; [optChangeAccumulator trackChildChange:change]; } return [newIndexed updateChild:nextChild.name withNewChild:nextChild.node]; } else { return newIndexed; } } } else if (newChildSnap.isEmpty) { // We're deleting a node, but it was not in the window, so ignore it. return oldIndexed; } else if (inRange) { // `newChildSnap` is in range, but was ordered after `windowBoundary`. If this has changed, we bump out the // `windowBoundary` and add the `newChildSnap` if ([self.index compareKey:windowBoundary.name andNode:windowBoundary.node toOtherKey:childKey andNode:newChildSnap reverse:self.reverse] >= NSOrderedSame) { if (optChangeAccumulator != nil) { FChange *removedChange = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved indexedNode:[FIndexedNode indexedNodeWithNode:windowBoundary.node] childKey:windowBoundary.name]; FChange *addedChange = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap] childKey:childKey]; [optChangeAccumulator trackChildChange:removedChange]; [optChangeAccumulator trackChildChange:addedChange]; } return [[oldIndexed updateChild:childKey withNewChild:newChildSnap] updateChild:windowBoundary.name withNewChild:[FEmptyNode emptyNode]]; } else { return oldIndexed; } } else { // `newChildSnap` was not in range and remains not in range, so ignore it. return oldIndexed; } } - (FIndexedNode *)updateFullNode:(FIndexedNode *)oldSnap withNewNode:(FIndexedNode *)newSnap accumulator:(FChildChangeAccumulator *)optChangeAccumulator { __block FIndexedNode *filtered; if (newSnap.node.isLeafNode || newSnap.node.isEmpty) { // Make sure we have a children node with the correct index, not a leaf node filtered = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:self.index]; } else { filtered = newSnap; // Don't support priorities on queries. filtered = [filtered updatePriority:[FEmptyNode emptyNode]]; FNamedNode *startPost = nil; FNamedNode *endPost = nil; if (self.reverse) { startPost = self.rangedFilter.endPost; endPost = self.rangedFilter.startPost; } else { startPost = self.rangedFilter.startPost; endPost = self.rangedFilter.endPost; } __block BOOL foundStartPost = NO; __block NSUInteger count = 0; [newSnap enumerateChildrenReverse:self.reverse usingBlock:^(NSString *childKey, id childNode, BOOL *stop) { if (!foundStartPost && [self.index compareKey:startPost.name andNode:startPost.node toOtherKey:childKey andNode:childNode reverse:self.reverse] <= NSOrderedSame) { // Start adding foundStartPost = YES; } BOOL inRange = foundStartPost && count < self.limit; inRange = inRange && [self.index compareKey:childKey andNode:childNode toOtherKey:endPost.name andNode:endPost.node reverse:self.reverse] <= NSOrderedSame; if (inRange) { count++; } else { filtered = [filtered updateChild:childKey withNewChild:[FEmptyNode emptyNode]]; } }]; } return [self.indexedFilter updateFullNode:oldSnap withNewNode:filtered accumulator:optChangeAccumulator]; } - (FIndexedNode *)updatePriority:(id)priority forNode:(FIndexedNode *)oldSnap { // Don't support priorities on queries. return oldSnap; } - (BOOL) filtersNodes { return YES; } - (id) indexedFilter { return self.rangedFilter.indexedFilter; } @end