diff options
Diffstat (limited to 'Firebase/Database/Core')
84 files changed, 9679 insertions, 0 deletions
diff --git a/Firebase/Database/Core/FCompoundHash.h b/Firebase/Database/Core/FCompoundHash.h new file mode 100644 index 0000000..cd5240e --- /dev/null +++ b/Firebase/Database/Core/FCompoundHash.h @@ -0,0 +1,40 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FNode.h" + + +@interface FCompoundHashBuilder : NSObject + +- (FPath *)currentPath; + +@end + + +typedef BOOL (^FCompoundHashSplitStrategy) (FCompoundHashBuilder *builder); + + +@interface FCompoundHash : NSObject + +@property (nonatomic, strong, readonly) NSArray *posts; +@property (nonatomic, strong, readonly) NSArray *hashes; + ++ (FCompoundHash *)fromNode:(id<FNode>)node; ++ (FCompoundHash *)fromNode:(id<FNode>)node splitStrategy:(FCompoundHashSplitStrategy)strategy; + +@end diff --git a/Firebase/Database/Core/FCompoundHash.m b/Firebase/Database/Core/FCompoundHash.m new file mode 100644 index 0000000..b4f72cd --- /dev/null +++ b/Firebase/Database/Core/FCompoundHash.m @@ -0,0 +1,236 @@ +/* + * 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 "FCompoundHash.h" +#import "FLeafNode.h" +#import "FStringUtilities.h" +#import "FSnapshotUtilities.h" +#import "FChildrenNode.h" + +@interface FCompoundHashBuilder () + +@property (nonatomic, strong) FCompoundHashSplitStrategy splitStrategy; + +@property (nonatomic, strong) NSMutableArray *currentPaths; +@property (nonatomic, strong) NSMutableArray *currentHashes; + +@end + +@implementation FCompoundHashBuilder { + + // NOTE: We use the existence of this to know if we've started building a range (i.e. encountered a leaf node). + NSMutableString *optHashValueBuilder; + + // The current path as a stack. This is used in combination with currentPathDepth to simultaneously store the + // last leaf node path. The depth is changed when descending and ascending, at the same time the current key + // is set for the current depth. Because the keys are left unchanged for ascending the path will also contain + // the path of the last visited leaf node (using lastLeafDepth elements) + NSMutableArray *currentPath; + NSInteger lastLeafDepth; + NSInteger currentPathDepth; + + BOOL needsComma; +} + +- (instancetype)initWithSplitStrategy:(FCompoundHashSplitStrategy)strategy { + self = [super init]; + if (self != nil) { + self->_splitStrategy = strategy; + self->optHashValueBuilder = nil; + self->currentPath = [NSMutableArray array]; + self->lastLeafDepth = -1; + self->currentPathDepth = 0; + self->needsComma = YES; + self->_currentPaths = [NSMutableArray array]; + self->_currentHashes = [NSMutableArray array]; + } + return self; +} + +- (BOOL)isBuildingRange { + return self->optHashValueBuilder != nil; +} + +- (NSUInteger)currentHashLength { + return self->optHashValueBuilder.length; +} + +- (FPath *)currentPath { + return [self currentPathWithDepth:self->currentPathDepth]; +} + +- (FPath *)currentPathWithDepth:(NSInteger)depth { + NSArray *pieces = [self->currentPath subarrayWithRange:NSMakeRange(0, depth)]; + return [[FPath alloc] initWithPieces:pieces andPieceNum:0]; +} + +- (void)enumerateCurrentPathToDepth:(NSInteger)depth withBlock:(void (^) (NSString *key))block { + for (NSInteger i = 0; i < depth; i++) { + block(self->currentPath[i]); + } +} + +- (void)appendKey:(NSString *)key toString:(NSMutableString *)string { + [FSnapshotUtilities appendHashV2RepresentationForString:key toString:string]; +} + +- (void)ensureRange { + if (![self isBuildingRange]) { + optHashValueBuilder = [NSMutableString string]; + [optHashValueBuilder appendString:@"("]; + [self enumerateCurrentPathToDepth:self->currentPathDepth withBlock:^(NSString *key) { + [self appendKey:key toString:self->optHashValueBuilder]; + [self->optHashValueBuilder appendString:@":("]; + }]; + self->needsComma = NO; + } +} + +- (void)processLeaf:(FLeafNode *)leafNode { + [self ensureRange]; + + self->lastLeafDepth = self->currentPathDepth; + [FSnapshotUtilities appendHashRepresentationForLeafNode:leafNode + toString:self->optHashValueBuilder + hashVersion:FDataHashVersionV2]; + self->needsComma = YES; + if (self.splitStrategy(self)) { + [self endRange]; + } +} + +- (void)startChild:(NSString *)key { + [self ensureRange]; + + if (self->needsComma) { + [self->optHashValueBuilder appendString:@","]; + } + [self appendKey:key toString:self->optHashValueBuilder]; + [self->optHashValueBuilder appendString:@":("]; + if (self->currentPathDepth == currentPath.count) { + [self->currentPath addObject:key]; + } else { + self->currentPath[self->currentPathDepth] = key; + } + self->currentPathDepth++; + self->needsComma = NO; +} + +- (void)endChild { + self->currentPathDepth--; + if ([self isBuildingRange]) { + [self->optHashValueBuilder appendString:@")"]; + } + self->needsComma = YES; +} + +- (void)finishHashing { + NSAssert(self->currentPathDepth == 0, @"Can't finish hashing in the middle of processing a child"); + if ([self isBuildingRange] ) { + [self endRange]; + } + + // Always close with the empty hash for the remaining range to allow simple appending + [self.currentHashes addObject:@""]; +} + +- (void)endRange { + NSAssert([self isBuildingRange], @"Can't end range without starting a range!"); + // Add closing parenthesis for current depth + for (NSUInteger i = 0; i < currentPathDepth; i++) { + [self->optHashValueBuilder appendString:@")"]; + } + [self->optHashValueBuilder appendString:@")"]; + + FPath *lastLeafPath = [self currentPathWithDepth:self->lastLeafDepth]; + NSString *hash = [FStringUtilities base64EncodedSha1:self->optHashValueBuilder]; + [self.currentHashes addObject:hash]; + [self.currentPaths addObject:lastLeafPath]; + + self->optHashValueBuilder = nil; +} + +@end + + +@interface FCompoundHash () + +@property (nonatomic, strong, readwrite) NSArray *posts; +@property (nonatomic, strong, readwrite) NSArray *hashes; + +@end + +@implementation FCompoundHash + +- (id)initWithPosts:(NSArray *)posts hashes:(NSArray *)hashes { + self = [super init]; + if (self != nil) { + if (posts.count != hashes.count - 1) { + [NSException raise:NSInvalidArgumentException format:@"Number of posts need to be n-1 for n hashes in FCompoundHash"]; + } + self.posts = posts; + self.hashes = hashes; + } + return self; +} + ++ (FCompoundHashSplitStrategy)simpleSizeSplitStrategyForNode:(id<FNode>)node { + NSUInteger estimatedSize = [FSnapshotUtilities estimateSerializedNodeSize:node]; + + // Splits for + // 1k -> 512 (2 parts) + // 5k -> 715 (7 parts) + // 100k -> 3.2k (32 parts) + // 500k -> 7k (71 parts) + // 5M -> 23k (228 parts) + NSUInteger splitThreshold = MAX(512, (NSUInteger)sqrt(estimatedSize * 100)); + + return ^BOOL(FCompoundHashBuilder *builder) { + // Never split on priorities + return [builder currentHashLength] > splitThreshold && ![[[builder currentPath] getBack] isEqualToString:@".priority"]; + }; +} + ++ (FCompoundHash *)fromNode:(id<FNode>)node { + return [FCompoundHash fromNode:node splitStrategy:[FCompoundHash simpleSizeSplitStrategyForNode:node]]; +} + ++ (FCompoundHash *)fromNode:(id<FNode>)node splitStrategy:(FCompoundHashSplitStrategy)strategy { + if ([node isEmpty]) { + return [[FCompoundHash alloc] initWithPosts:@[] hashes:@[@""]]; + } else { + FCompoundHashBuilder *builder = [[FCompoundHashBuilder alloc] initWithSplitStrategy:strategy]; + [FCompoundHash processNode:node builder:builder]; + [builder finishHashing]; + return [[FCompoundHash alloc] initWithPosts:builder.currentPaths hashes:builder.currentHashes]; + } +} + ++ (void)processNode:(id<FNode>)node builder:(FCompoundHashBuilder *)builder { + if ([node isLeafNode]) { + [builder processLeaf:node]; + } else { + NSAssert(![node isEmpty], @"Can't calculate hash on empty node!"); + FChildrenNode *childrenNode = (FChildrenNode *)node; + [childrenNode enumerateChildrenAndPriorityUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) { + [builder startChild:key]; + [self processNode:node builder:builder]; + [builder endChild]; + }]; + } +} + +@end diff --git a/Firebase/Database/Core/FListenProvider.h b/Firebase/Database/Core/FListenProvider.h new file mode 100644 index 0000000..7a41754 --- /dev/null +++ b/Firebase/Database/Core/FListenProvider.h @@ -0,0 +1,33 @@ +/* + * 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 "FTypedefs_Private.h" + +@class FQuerySpec; +@protocol FSyncTreeHash; + +typedef NSArray* (^fbt_startListeningBlock)(FQuerySpec *query, + NSNumber *tagId, + id<FSyncTreeHash> hash, + fbt_nsarray_nsstring onComplete); +typedef void (^fbt_stopListeningBlock)(FQuerySpec *query, NSNumber *tagId); + +@interface FListenProvider : NSObject + +@property (nonatomic, copy) fbt_startListeningBlock startListening; +@property (nonatomic, copy) fbt_stopListeningBlock stopListening; + +@end diff --git a/Firebase/Database/Core/FListenProvider.m b/Firebase/Database/Core/FListenProvider.m new file mode 100644 index 0000000..7a49609 --- /dev/null +++ b/Firebase/Database/Core/FListenProvider.m @@ -0,0 +1,26 @@ +/* + * 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 "FListenProvider.h" +#import "FIRDatabaseQuery.h" + + +@implementation FListenProvider + +@synthesize startListening; +@synthesize stopListening; + +@end diff --git a/Firebase/Database/Core/FPersistentConnection.h b/Firebase/Database/Core/FPersistentConnection.h new file mode 100644 index 0000000..412c874 --- /dev/null +++ b/Firebase/Database/Core/FPersistentConnection.h @@ -0,0 +1,78 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FConnection.h" +#import "FRepoInfo.h" +#import "FTypedefs.h" +#import "FTypedefs_Private.h" + +@protocol FPersistentConnectionDelegate; +@protocol FSyncTreeHash; +@class FQuerySpec; +@class FIRDatabaseConfig; + +@interface FPersistentConnection : NSObject <FConnectionDelegate> + +@property (nonatomic, weak) id <FPersistentConnectionDelegate> delegate; +@property (nonatomic) BOOL pauseWrites; + +- (id)initWithRepoInfo:(FRepoInfo *)repoInfo + dispatchQueue:(dispatch_queue_t)queue + config:(FIRDatabaseConfig *)config; + +- (void)open; + +- (void) putData:(id)data forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete; +- (void) mergeData:(id)data forPath:(NSString *)pathString withCallback:(fbt_void_nsstring_nsstring)onComplete; + +- (void) listen:(FQuerySpec *)query + tagId:(NSNumber *)tagId + hash:(id<FSyncTreeHash>)hash + onComplete:(fbt_void_nsstring)onComplete; + +- (void) unlisten:(FQuerySpec *)query tagId:(NSNumber *)tagId; +- (void) refreshAuthToken:(NSString *)token; +- (void) onDisconnectPutData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback; +- (void) onDisconnectMergeData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback; +- (void) onDisconnectCancelPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback; +- (void) ackPuts; +- (void) purgeOutstandingWrites; + +- (void) interruptForReason:(NSString *)reason; +- (void) resumeForReason:(NSString *)reason; +- (BOOL) isInterruptedForReason:(NSString *)reason; + +// FConnection delegate methods +- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID; +- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message; +- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason; +- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason; + +// Testing methods +- (NSDictionary *) dumpListens; + +@end + +@protocol FPersistentConnectionDelegate <NSObject> + +- (void)onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)message isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId; +- (void)onRangeMerge:(NSArray *)ranges forPath:(NSString *)path tagId:(NSNumber *)tag; +- (void)onConnect:(FPersistentConnection *)fpconnection; +- (void)onDisconnect:(FPersistentConnection *)fpconnection; +- (void)onServerInfoUpdate:(FPersistentConnection *)fpconnection updates:(NSDictionary *)updates; + +@end diff --git a/Firebase/Database/Core/FPersistentConnection.m b/Firebase/Database/Core/FPersistentConnection.m new file mode 100644 index 0000000..0eb1f9f --- /dev/null +++ b/Firebase/Database/Core/FPersistentConnection.m @@ -0,0 +1,945 @@ +/* + * 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 <SystemConfiguration/SystemConfiguration.h> +#import <netinet/in.h> +#import <dlfcn.h> +#import "FIRDatabaseReference.h" +#import "FPersistentConnection.h" +#import "FConstants.h" +#import "FAtomicNumber.h" +#import "FQueryParams.h" +#import "FTupleOnDisconnect.h" +#import "FTupleCallbackStatus.h" +#import "FQuerySpec.h" +#import "FIndex.h" +#import "FIRDatabaseConfig.h" +#import "FIRDatabaseConfig_Private.h" +#import "FSnapshotUtilities.h" +#import "FRangeMerge.h" +#import "FCompoundHash.h" +#import "FSyncTree.h" +#import "FIRRetryHelper.h" +#import "FAuthTokenProvider.h" +#import "FUtilities.h" + +@interface FOutstandingQuery : NSObject + +@property (nonatomic, strong) FQuerySpec* query; +@property (nonatomic, strong) NSNumber *tagId; +@property (nonatomic, strong) id<FSyncTreeHash> syncTreeHash; +@property (nonatomic, copy) fbt_void_nsstring onComplete; + +@end + +@implementation FOutstandingQuery + +@end + + +@interface FOutstandingPut : NSObject + +@property (nonatomic, strong) NSString *action; +@property (nonatomic, strong) NSDictionary *request; +@property (nonatomic, copy) fbt_void_nsstring_nsstring onCompleteBlock; +@property (nonatomic) BOOL sent; + +@end + +@implementation FOutstandingPut + +@end + + +typedef enum { + ConnectionStateDisconnected, + ConnectionStateGettingToken, + ConnectionStateConnecting, + ConnectionStateAuthenticating, + ConnectionStateConnected +} ConnectionState; + +@interface FPersistentConnection () { + ConnectionState connectionState; + BOOL firstConnection; + NSTimeInterval reconnectDelay; + NSTimeInterval lastConnectionAttemptTime; + NSTimeInterval lastConnectionEstablishedTime; + SCNetworkReachabilityRef reachability; +} + +- (int) getNextRequestNumber; +- (void) onDataPushWithAction:(NSString *)action andBody:(NSDictionary *)body; +- (void) handleTimestamp:(NSNumber *)timestamp; +- (void) sendOnDisconnectAction:(NSString *)action forPath:(NSString *)pathString withData:(id)data andCallback:(fbt_void_nsstring_nsstring)callback; + +@property (nonatomic, strong) FConnection* realtime; +@property (nonatomic, strong) NSMutableDictionary* listens; +@property (nonatomic, strong) NSMutableDictionary* outstandingPuts; +@property (nonatomic, strong) NSMutableArray* onDisconnectQueue; +@property (nonatomic, strong) FRepoInfo* repoInfo; +@property (nonatomic, strong) FAtomicNumber* putCounter; +@property (nonatomic, strong) FAtomicNumber* requestNumber; +@property (nonatomic, strong) NSMutableDictionary* requestCBHash; +@property (nonatomic, strong) FIRDatabaseConfig *config; +@property (nonatomic) NSUInteger unackedListensCount; +@property (nonatomic, strong) NSMutableArray *putsToAck; +@property (nonatomic, strong) dispatch_queue_t dispatchQueue; +@property (nonatomic, strong) NSString* lastSessionID; +@property (nonatomic, strong) NSMutableSet *interruptReasons; +@property (nonatomic, strong) FIRRetryHelper *retryHelper; +@property (nonatomic, strong) id<FAuthTokenProvider> authTokenProvider; +@property (nonatomic, strong) NSString *authToken; +@property (nonatomic) BOOL forceAuthTokenRefresh; +@property (nonatomic) NSUInteger currentFetchTokenAttempt; + +@end + + +@implementation FPersistentConnection + +- (id)initWithRepoInfo:(FRepoInfo *)repoInfo dispatchQueue:(dispatch_queue_t)dispatchQueue config:(FIRDatabaseConfig *)config { + self = [super init]; + if (self) { + self->_config = config; + self->_repoInfo = repoInfo; + self->_dispatchQueue = dispatchQueue; + self->_authTokenProvider = config.authTokenProvider; + NSAssert(self->_authTokenProvider != nil, @"Expected auth token provider"); + self.interruptReasons = [NSMutableSet set]; + + self.listens = [[NSMutableDictionary alloc] init]; + self.outstandingPuts = [[NSMutableDictionary alloc] init]; + self.onDisconnectQueue = [[NSMutableArray alloc] init]; + self.putCounter = [[FAtomicNumber alloc] init]; + self.requestNumber = [[FAtomicNumber alloc] init]; + self.requestCBHash = [[NSMutableDictionary alloc] init]; + self.unackedListensCount = 0; + self.putsToAck = [NSMutableArray array]; + connectionState = ConnectionStateDisconnected; + firstConnection = YES; + reconnectDelay = kPersistentConnReconnectMinDelay; + + self->_retryHelper = [[FIRRetryHelper alloc] initWithDispatchQueue:dispatchQueue + minRetryDelayAfterFailure:kPersistentConnReconnectMinDelay + maxRetryDelay:kPersistentConnReconnectMaxDelay + retryExponent:kPersistentConnReconnectMultiplier + jitterFactor:0.7]; + + [self setupNotifications]; + // Make sure we don't actually connect until open is called + [self interruptForReason:kFInterruptReasonWaitingForOpen]; + } + // nb: The reason establishConnection isn't called here like the JS version is because + // callers need to set the delegate first. The ctor can be modified to accept the delegate + // but that deviates from normal ios conventions. After the delegate has been set, the caller + // is responsible for calling establishConnection: + return self; +} + +- (void) dealloc { + if (reachability) { + // Unschedule the notifications + SCNetworkReachabilitySetDispatchQueue(reachability, NULL); + CFRelease(reachability); + } +} + +#pragma mark - +#pragma mark Public methods + +- (void) open { + [self resumeForReason:kFInterruptReasonWaitingForOpen]; +} + +/** +* Note that the listens dictionary has a type of Map[String (pathString), Map[FQueryParams, FOutstandingQuery]] +* +* This means, for each path we care about, there are sets of queryParams that correspond to an FOutstandingQuery object. +* There can be multiple sets at a path since we overlap listens for a short time while adding or removing a query from a +* location in the tree. +*/ +- (void) listen:(FQuerySpec *)query + tagId:(NSNumber *)tagId + hash:(id<FSyncTreeHash>)hash + onComplete:(fbt_void_nsstring)onComplete { + FFLog(@"I-RDB034001", @"Listen called for %@", query); + + NSAssert(self.listens[query] == nil, @"listen() called twice for the same query"); + NSAssert(query.isDefault || !query.loadsAllData, @"listen called for non-default but complete query"); + FOutstandingQuery* outstanding = [[FOutstandingQuery alloc] init]; + outstanding.query = query; + outstanding.tagId = tagId; + outstanding.syncTreeHash = hash; + outstanding.onComplete = onComplete; + [self.listens setObject:outstanding forKey:query]; + if ([self connected]) { + [self sendListen:outstanding]; + } +} + +- (void) putData:(id)data forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete { + [self putInternal:data forAction:kFWPRequestActionPut forPath:pathString withHash:hash withCallback:onComplete]; +} + +- (void) mergeData:(id)data forPath:(NSString *)pathString withCallback:(fbt_void_nsstring_nsstring)onComplete { + [self putInternal:data forAction:kFWPRequestActionMerge forPath:pathString withHash:nil withCallback:onComplete]; +} + +- (void) onDisconnectPutData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback { + if ([self canSendWrites]) { + [self sendOnDisconnectAction:kFWPRequestActionDisconnectPut forPath:[path description] withData:data andCallback:callback]; + } else { + FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init]; + tuple.pathString = [path description]; + tuple.action = kFWPRequestActionDisconnectPut; + tuple.data = data; + tuple.onComplete = callback; + [self.onDisconnectQueue addObject:tuple]; + } +} + +- (void) onDisconnectMergeData:(id)data forPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback { + if ([self canSendWrites]) { + [self sendOnDisconnectAction:kFWPRequestActionDisconnectMerge forPath:[path description] withData:data andCallback:callback]; + } else { + FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init]; + tuple.pathString = [path description]; + tuple.action = kFWPRequestActionDisconnectMerge; + tuple.data = data; + tuple.onComplete = callback; + [self.onDisconnectQueue addObject:tuple]; + } +} + +- (void) onDisconnectCancelPath:(FPath *)path withCallback:(fbt_void_nsstring_nsstring)callback { + if ([self canSendWrites]) { + [self sendOnDisconnectAction:kFWPRequestActionDisconnectCancel forPath:[path description] withData:[NSNull null] andCallback:callback]; + } else { + FTupleOnDisconnect* tuple = [[FTupleOnDisconnect alloc] init]; + tuple.pathString = [path description]; + tuple.action = kFWPRequestActionDisconnectCancel; + tuple.data = [NSNull null]; + tuple.onComplete = callback; + [self.onDisconnectQueue addObject:tuple]; + } +} + +- (void) unlisten:(FQuerySpec *)query tagId:(NSNumber *)tagId { + FPath *path = query.path; + FFLog(@"I-RDB034002", @"Unlistening for %@", query); + + NSArray *outstanding = [self removeListen:query]; + if (outstanding.count > 0 && [self connected]) { + [self sendUnlisten:path queryParams:query.params tagId:tagId]; + } +} + +- (void) refreshAuthToken:(NSString *)token { + self.authToken = token; + if ([self connected]) { + if (token != nil) { + [self sendAuthAndRestoreStateAfterComplete:NO]; + } else { + [self sendUnauth]; + } + } +} + +#pragma mark - +#pragma mark Connection status + +- (BOOL)connected { + return self->connectionState == ConnectionStateAuthenticating || self->connectionState == ConnectionStateConnected; +} + +- (BOOL)canSendWrites { + return self->connectionState == ConnectionStateConnected; +} + +#pragma mark - +#pragma mark FConnection delegate methods + +- (void)onReady:(FConnection *)fconnection atTime:(NSNumber *)timestamp sessionID:(NSString *)sessionID { + FFLog(@"I-RDB034003", @"On ready"); + lastConnectionEstablishedTime = [[NSDate date] timeIntervalSince1970]; + [self handleTimestamp:timestamp]; + + if (firstConnection) { + [self sendConnectStats]; + } + + [self restoreAuth]; + firstConnection = NO; + self.lastSessionID = sessionID; + dispatch_async(self.dispatchQueue, ^{ + [self.delegate onConnect:self]; + }); +} + +- (void)onDataMessage:(FConnection *)fconnection withMessage:(NSDictionary *)message { + if (message[kFWPRequestNumber] != nil) { + // this is a response to a request we sent + NSNumber* rn = [NSNumber numberWithInt:[[message objectForKey:kFWPRequestNumber] intValue]]; + if ([self.requestCBHash objectForKey:rn]) { + void (^callback)(NSDictionary*) = [self.requestCBHash objectForKey:rn]; + [self.requestCBHash removeObjectForKey:rn]; + + if (callback) { + //dispatch_async(self.dispatchQueue, ^{ + callback([message objectForKey:kFWPResponseForRNData]); + //}); + } + } + } else if (message[kFWPRequestError] != nil) { + NSString* error = [message objectForKey:kFWPRequestError]; + @throw [[NSException alloc] initWithName:@"FirebaseDatabaseServerError" reason:error userInfo:nil]; + } else if (message[kFWPAsyncServerAction] != nil) { + // this is a server push of some sort + NSString* action = [message objectForKey:kFWPAsyncServerAction]; + NSDictionary* body = [message objectForKey:kFWPAsyncServerPayloadBody]; + [self onDataPushWithAction:action andBody:body]; + } +} + +- (void)onDisconnect:(FConnection *)fconnection withReason:(FDisconnectReason)reason { + FFLog(@"I-RDB034004", @"Got on disconnect due to %s", (reason == DISCONNECT_REASON_SERVER_RESET) ? "server_reset" : "other"); + connectionState = ConnectionStateDisconnected; + // Drop the realtime connection + self.realtime = nil; + [self cancelSentTransactions]; + [self.requestCBHash removeAllObjects]; + self.unackedListensCount = 0; + if ([self shouldReconnect]) { + NSTimeInterval timeSinceLastConnectSucceeded = [[NSDate date] timeIntervalSince1970] - lastConnectionEstablishedTime; + BOOL lastConnectionWasSuccessful; + if (lastConnectionEstablishedTime > 0) { + lastConnectionWasSuccessful = timeSinceLastConnectSucceeded > kPersistentConnSuccessfulConnectionEstablishedDelay; + } else { + lastConnectionWasSuccessful = NO; + } + + if (reason == DISCONNECT_REASON_SERVER_RESET || lastConnectionWasSuccessful) { + [self.retryHelper signalSuccess]; + } + [self tryScheduleReconnect]; + } + lastConnectionEstablishedTime = 0; + [self.delegate onDisconnect:self]; +} + +- (void)onKill:(FConnection *)fconnection withReason:(NSString *)reason { + FFWarn(@"I-RDB034005", @"Firebase Database connection was forcefully killed by the server. Will not attempt reconnect. Reason: %@", reason); + [self interruptForReason:kFInterruptReasonServerKill]; +} + +#pragma mark - +#pragma mark Connection handling methods + +- (void) interruptForReason:(NSString *)reason { + FFLog(@"I-RDB034006", @"Connection interrupted for: %@", reason); + + [self.interruptReasons addObject:reason]; + if (self.realtime) { + // Will call onDisconnect and set the connection state to Disconnected + [self.realtime close]; + self.realtime = nil; + } else { + [self.retryHelper cancel]; + self->connectionState = ConnectionStateDisconnected; + } + // Reset timeouts + [self.retryHelper signalSuccess]; +} + +- (void) resumeForReason:(NSString *)reason { + FFLog(@"I-RDB034007", @"Connection no longer interrupted for: %@", reason); + [self.interruptReasons removeObject:reason]; + + if ([self shouldReconnect] && connectionState == ConnectionStateDisconnected) { + [self tryScheduleReconnect]; + } +} + +- (BOOL) shouldReconnect { + return self.interruptReasons.count == 0; +} + +- (BOOL) isInterruptedForReason:(NSString *)reason { + return [self.interruptReasons containsObject:reason]; +} + +#pragma mark - +#pragma mark Private methods + +- (void) tryScheduleReconnect { + if ([self shouldReconnect]) { + NSAssert(self->connectionState == ConnectionStateDisconnected, + @"Not in disconnected state: %d", self->connectionState); + BOOL forceRefresh = self.forceAuthTokenRefresh; + self.forceAuthTokenRefresh = NO; + FFLog(@"I-RDB034008", @"Scheduling connection attempt"); + [self.retryHelper retry:^{ + FFLog(@"I-RDB034009", @"Trying to fetch auth token"); + NSAssert(self->connectionState == ConnectionStateDisconnected, + @"Not in disconnected state: %d", self->connectionState); + self->connectionState = ConnectionStateGettingToken; + self.currentFetchTokenAttempt++; + NSUInteger thisFetchTokenAttempt = self.currentFetchTokenAttempt; + [self.authTokenProvider fetchTokenForcingRefresh:forceRefresh withCallback:^(NSString *token, NSError *error) { + if (thisFetchTokenAttempt == self.currentFetchTokenAttempt) { + if (error != nil) { + self->connectionState = ConnectionStateDisconnected; + FFLog(@"I-RDB034010", @"Error fetching token: %@", error); + [self tryScheduleReconnect]; + } else { + // Someone could have interrupted us while fetching the token, + // marking the connection as Disconnected + if (self->connectionState == ConnectionStateGettingToken) { + FFLog(@"I-RDB034011", @"Successfully fetched token, opening connection"); + [self openNetworkConnectionWithToken:token]; + } else { + NSAssert(self->connectionState == ConnectionStateDisconnected, + @"Expected connection state disconnected, but got %d", self->connectionState); + FFLog(@"I-RDB034012", @"Not opening connection after token refresh, because connection was set to disconnected."); + } + } + } else { + FFLog(@"I-RDB034013", @"Ignoring fetch token result, because this was not the latest attempt."); + } + }]; + }]; + + } +} + +- (void) openNetworkConnectionWithToken:(NSString *)token { + NSAssert(self->connectionState == ConnectionStateGettingToken, + @"Trying to open network connection while in wrong state: %d", self->connectionState); + self.authToken = token; + self->connectionState = ConnectionStateConnecting; + self.realtime = [[FConnection alloc] initWith:self.repoInfo + andDispatchQueue:self.dispatchQueue + lastSessionID:self.lastSessionID]; + self.realtime.delegate = self; + [self.realtime open]; +} + +static void reachabilityCallback(SCNetworkReachabilityRef ref, SCNetworkReachabilityFlags flags, void* info) { + if (flags & kSCNetworkReachabilityFlagsReachable) { + FFLog(@"I-RDB034014", @"Network became reachable. Trigger a connection attempt"); + FPersistentConnection* self = (__bridge FPersistentConnection *)info; + // Reset reconnect delay + [self.retryHelper signalSuccess]; + if (self->connectionState == ConnectionStateDisconnected) { + [self tryScheduleReconnect]; + } + } else { + FFLog(@"I-RDB034015", @"Network is not reachable"); + } +} + +- (void) enteringForeground { + dispatch_async(self.dispatchQueue, ^{ + // Reset reconnect delay + [self.retryHelper signalSuccess]; + if (self->connectionState == ConnectionStateDisconnected) { + [self tryScheduleReconnect]; + } + }); +} + +- (void) setupNotifications { + + NSString * const* foregroundConstant = (NSString * const *) dlsym(RTLD_DEFAULT, "UIApplicationWillEnterForegroundNotification"); + if (foregroundConstant) { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(enteringForeground) + name:*foregroundConstant + object:nil]; + } + // An empty address is interpreted a generic internet access + struct sockaddr_in zeroAddress; + bzero(&zeroAddress, sizeof(zeroAddress)); + zeroAddress.sin_len = sizeof(zeroAddress); + zeroAddress.sin_family = AF_INET; + reachability = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr *)&zeroAddress); + SCNetworkReachabilityContext ctx = {0, (__bridge void *)(self), NULL, NULL, NULL}; + if (SCNetworkReachabilitySetCallback(reachability, reachabilityCallback, &ctx)) { + SCNetworkReachabilitySetDispatchQueue(reachability, self.dispatchQueue); + } else { + FFLog(@"I-RDB034016", @"Failed to set up network reachability monitoring"); + CFRelease(reachability); + reachability = NULL; + } +} + +- (void) sendAuthAndRestoreStateAfterComplete:(BOOL)restoreStateAfterComplete { + NSAssert([self connected], @"Must be connected to send auth"); + NSAssert(self.authToken != nil, @"Can't send auth if there is no credential"); + + NSDictionary* requestData = @{kFWPRequestCredential: self.authToken}; + [self sendAction:kFWPRequestActionAuth body:requestData sensitive:YES callback:^(NSDictionary *data) { + self->connectionState = ConnectionStateConnected; + NSString* status = [data objectForKey:kFWPResponseForActionStatus]; + id responseData = [data objectForKey:kFWPResponseForActionData]; + if (responseData == nil) { + responseData = @"error"; + } + + BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk]; + if (statusOk) { + if (restoreStateAfterComplete) { + [self restoreState]; + } + } else { + self.authToken = nil; + self.forceAuthTokenRefresh = YES; + if ([status isEqualToString:@"expired_token"]) { + FFLog(@"I-RDB034017", @"Authentication failed: %@ (%@)", status, responseData); + } else { + FFWarn(@"I-RDB034018", @"Authentication failed: %@ (%@)", status, responseData); + } + [self.realtime close]; + } + }]; +} + +- (void) sendUnauth { + [self sendAction:kFWPRequestActionUnauth body:@{} sensitive:NO callback:nil]; +} + +- (void) onAuthRevokedWithStatus:(NSString *)status andReason:(NSString *)reason { + // This might be for an earlier token than we just recently sent. But since we need to close the connection anyways, + // we can set it to null here and we will refresh the token later on reconnect + if ([status isEqualToString:@"expired_token"]) { + FFLog(@"I-RDB034019", @"Auth token revoked: %@ (%@)", status, reason); + } else { + FFWarn(@"I-RDB034020", @"Auth token revoked: %@ (%@)", status, reason); + } + self.authToken = nil; + self.forceAuthTokenRefresh = YES; + // Try reconnecting on auth revocation + [self.realtime close]; +} + +- (void) onListenRevoked:(FPath *)path { + NSArray *queries = [self removeAllListensAtPath:path]; + for (FOutstandingQuery* query in queries) { + query.onComplete(@"permission_denied"); + } +} + +- (void) sendOnDisconnectAction:(NSString *)action forPath:(NSString *)pathString withData:(id)data andCallback:(fbt_void_nsstring_nsstring)callback { + + NSDictionary* request = @{kFWPRequestPath: pathString, kFWPRequestData: data}; + FFLog(@"I-RDB034021", @"onDisconnect %@: %@", action, request); + + [self sendAction:action + body:request + sensitive:NO + callback:^(NSDictionary *data) { + NSString* status = [data objectForKey:kFWPResponseForActionStatus]; + NSString* errorReason = [data objectForKey:kFWPResponseForActionData]; + callback(status, errorReason); + }]; +} + +- (void) sendPut:(NSNumber *) index { + NSAssert([self canSendWrites], @"sendPut called when not able to send writes"); + FOutstandingPut* put = self.outstandingPuts[index]; + assert(put != nil); + fbt_void_nsstring_nsstring onComplete = put.onCompleteBlock; + + // Do not async this block; copying the block insinde sendAction: doesn't happen in time (or something) so coredumps + put.sent = YES; + [self sendAction:put.action + body:put.request + sensitive:NO + callback:^(NSDictionary* data) { + + FOutstandingPut *currentPut = self.outstandingPuts[index]; + if (currentPut == put) { + [self.outstandingPuts removeObjectForKey:index]; + + if (onComplete != nil) { + NSString *status = [data objectForKey:kFWPResponseForActionStatus]; + NSString *errorReason = [data objectForKey:kFWPResponseForActionData]; + if (self.unackedListensCount == 0) { + onComplete(status, errorReason); + } else { + FTupleCallbackStatus *putToAck = [[FTupleCallbackStatus alloc] init]; + putToAck.block = onComplete; + putToAck.status = status; + putToAck.errorReason = errorReason; + [self.putsToAck addObject:putToAck]; + } + } + } else { + FFLog(@"I-RDB034022", @"Ignoring on complete for put %@ because it was already removed", index); + } + }]; +} + +- (void) sendUnlisten:(FPath *)path queryParams:(FQueryParams *)queryParams tagId:(NSNumber *)tagId { + FFLog(@"I-RDB034023", @"Unlisten on %@ for %@", path, queryParams); + + NSMutableDictionary* request = [NSMutableDictionary dictionaryWithObjectsAndKeys:[path toString], kFWPRequestPath, nil]; + if (tagId) { + [request setObject:queryParams.wireProtocolParams forKey:kFWPRequestQueries]; + [request setObject:tagId forKey:kFWPRequestTag]; + } + + [self sendAction:kFWPRequestActionTaggedUnlisten + body:request + sensitive:NO + callback:nil]; +} + +- (void) putInternal:(id)data forAction:(NSString *)action forPath:(NSString *)pathString withHash:(NSString *)hash withCallback:(fbt_void_nsstring_nsstring)onComplete { + + NSMutableDictionary *request = [NSMutableDictionary dictionaryWithObjectsAndKeys: + pathString, kFWPRequestPath, + data, kFWPRequestData, nil]; + if(hash) { + [request setObject:hash forKey:kFWPRequestHash]; + } + + FOutstandingPut *put = [[FOutstandingPut alloc] init]; + put.action = action; + put.request = request; + put.onCompleteBlock = onComplete; + put.sent = NO; + + NSNumber* index = [self.putCounter getAndIncrement]; + self.outstandingPuts[index] = put; + + if ([self canSendWrites]) { + FFLog(@"I-RDB034024", @"Was connected, and added as index: %@", index); + [self sendPut:index]; + } + else { + FFLog(@"I-RDB034025", @"Wasn't connected or writes paused, so added to outstanding puts only. Path: %@", pathString); + } +} + +- (void) sendListen:(FOutstandingQuery *)listenSpec { + FQuerySpec *query = listenSpec.query; + FFLog(@"I-RDB034026", @"Listen for %@", query); + NSMutableDictionary *request = [NSMutableDictionary dictionaryWithObject:[query.path toString] forKey:kFWPRequestPath]; + + // Only bother to send query if it's non-default + if (listenSpec.tagId != nil) { + [request setObject:[query.params wireProtocolParams] forKey:kFWPRequestQueries]; + [request setObject:listenSpec.tagId forKey:kFWPRequestTag]; + } + + [request setObject:[listenSpec.syncTreeHash simpleHash] forKey:kFWPRequestHash]; + if ([listenSpec.syncTreeHash includeCompoundHash]) { + FCompoundHash *compoundHash = [listenSpec.syncTreeHash compoundHash]; + NSMutableArray *posts = [NSMutableArray array]; + for (FPath *path in compoundHash.posts) { + [posts addObject:path.wireFormat]; + } + request[kFWPRequestCompoundHash] = @{ kFWPRequestCompoundHashHashes: compoundHash.hashes, + kFWPRequestCompoundHashPaths: posts }; + } + + fbt_void_nsdictionary onResponse = ^(NSDictionary *response) { + FFLog(@"I-RDB034027", @"Listen response %@", response); + // warn in any case, even if the listener was removed + [self warnOnListenWarningsForQuery:query payload:response[kFWPResponseForActionData]]; + + FOutstandingQuery *currentListenSpec = self.listens[query]; + + // only trigger actions if the listen hasn't been removed (and maybe readded) + if (currentListenSpec == listenSpec) { + NSString *status = [response objectForKey:kFWPRequestStatus]; + if (![status isEqualToString:@"ok"]) { + [self removeListen:query]; + } + + if (listenSpec.onComplete) { + listenSpec.onComplete(status); + } + } + + self.unackedListensCount--; + NSAssert(self.unackedListensCount >= 0, @"unackedListensCount decremented to be negative."); + if (self.unackedListensCount == 0) { + [self ackPuts]; + } + }; + + [self sendAction:kFWPRequestActionTaggedListen + body:request + sensitive:NO + callback:onResponse]; + + self.unackedListensCount++; +} + +- (void) warnOnListenWarningsForQuery:(FQuerySpec *)query payload:(id)payload { + if (payload != nil && [payload isKindOfClass:[NSDictionary class]]) { + NSDictionary *payloadDict = payload; + id warnings = payloadDict[kFWPResponseDataWarnings]; + if (warnings != nil && [warnings isKindOfClass:[NSArray class]]) { + NSArray *warningsArr = warnings; + if ([warningsArr containsObject:@"no_index"]) { + NSString *indexSpec = [NSString stringWithFormat:@"\".indexOn\": \"%@\"", [query.params.index queryDefinition]]; + NSString *indexPath = [query.path description]; + FFWarn(@"I-RDB034028", @"Using an unspecified index. Consider adding %@ at %@ to your security rules for better performance", indexSpec, indexPath); + } + } + } +} + +- (int) getNextRequestNumber { + return [[self.requestNumber getAndIncrement] intValue]; +} + +- (void)sendAction:(NSString *)action + body:(NSDictionary *)message + sensitive:(BOOL)sensitive + callback:(void (^)(NSDictionary* data))onMessage { + // Hold onto the onMessage callback for this request before firing it off + NSNumber* rn = [NSNumber numberWithInt:[self getNextRequestNumber]]; + NSDictionary* msg = [NSDictionary dictionaryWithObjectsAndKeys: + rn, kFWPRequestNumber, + action, kFWPRequestAction, + message, kFWPRequestPayloadBody, + nil]; + + [self.realtime sendRequest:msg sensitive:sensitive]; + + if (onMessage) { + // Debug message without a callback; bump the rn, but don't hold onto the cb + [self.requestCBHash setObject:[onMessage copy] forKey:rn]; + } +} + +- (void) cancelSentTransactions { + NSMutableArray* toPrune = [[NSMutableArray alloc] init]; + for (NSNumber* index in self.outstandingPuts) { + FOutstandingPut* put = self.outstandingPuts[index]; + if (put.request[kFWPRequestHash] && put.sent) { + // This is a sent transaction put + put.onCompleteBlock(kFTransactionDisconnect, @"Client was disconnected while running a transaction"); + [toPrune addObject:index]; + } + } + for (NSNumber* index in toPrune) { + [self.outstandingPuts removeObjectForKey:index]; + } +} + +- (void) onDataPushWithAction:(NSString *)action andBody:(NSDictionary *)body { + FFLog(@"I-RDB034029", @"handleServerMessage: %@, %@", action, body); + id<FPersistentConnectionDelegate> delegate = self.delegate; + if ([action isEqualToString:kFWPAsyncServerDataUpdate] || [action isEqualToString:kFWPAsyncServerDataMerge]) { + BOOL isMerge = [action isEqualToString:kFWPAsyncServerDataMerge]; + + if ([body objectForKey:kFWPAsyncServerDataUpdateBodyPath] && [body objectForKey:kFWPAsyncServerDataUpdateBodyData]) { + NSString* path = [body objectForKey:kFWPAsyncServerDataUpdateBodyPath]; + id payloadData = [body objectForKey:kFWPAsyncServerDataUpdateBodyData]; + if (isMerge && [payloadData isKindOfClass:[NSDictionary class]] && [payloadData count] == 0) { + // ignore empty merge + } else { + [delegate onDataUpdate:self forPath:path message:payloadData isMerge:isMerge tagId:[body objectForKey:kFWPAsyncServerDataUpdateBodyTag]]; + } + } + else { + FFLog(@"I-RDB034030", @"Malformed data response from server missing path or data: %@", body); + } + } else if ([action isEqualToString:kFWPAsyncServerDataRangeMerge]) { + NSString *path = body[kFWPAsyncServerDataUpdateBodyPath]; + NSArray *ranges = body[kFWPAsyncServerDataUpdateBodyData]; + NSNumber *tag = body[kFWPAsyncServerDataUpdateBodyTag]; + NSMutableArray *rangeMerges = [NSMutableArray array]; + for (NSDictionary *range in ranges) { + NSString *startString = range[kFWPAsyncServerDataUpdateStartPath]; + NSString *endString = range[kFWPAsyncServerDataUpdateEndPath]; + id updateData = range[kFWPAsyncServerDataUpdateRangeMerge]; + id<FNode> updates = [FSnapshotUtilities nodeFrom:updateData]; + FPath *start = (startString != nil) ? [[FPath alloc] initWith:startString] : nil; + FPath *end = (endString != nil) ? [[FPath alloc] initWith:endString] : nil; + FRangeMerge *merge = [[FRangeMerge alloc] initWithStart:start end:end updates:updates]; + [rangeMerges addObject:merge]; + } + [delegate onRangeMerge:rangeMerges forPath:path tagId:tag]; + } else if ([action isEqualToString:kFWPAsyncServerAuthRevoked]) { + NSString* status = [body objectForKey:kFWPResponseForActionStatus]; + NSString* reason = [body objectForKey:kFWPResponseForActionData]; + [self onAuthRevokedWithStatus:status andReason:reason]; + } else if ([action isEqualToString:kFWPASyncServerListenCancelled]) { + NSString* pathString = [body objectForKey:kFWPAsyncServerDataUpdateBodyPath]; + [self onListenRevoked:[[FPath alloc] initWith:pathString]]; + } else if ([action isEqualToString:kFWPAsyncServerSecurityDebug]) { + NSString* msg = [body objectForKey:@"msg"]; + if (msg != nil) { + NSArray *msgs = [msg componentsSeparatedByString:@"\n"]; + for (NSString* m in msgs) { + FFWarn(@"I-RDB034031", @"%@", m); + } + } + } else { + // TODO: revoke listens, auth, security debug + FFLog(@"I-RDB034032", @"Unsupported action from server: %@", action); + } +} + +- (void) restoreAuth { + FFLog(@"I-RDB034033", @"Calling restore state"); + + NSAssert(self->connectionState == ConnectionStateConnecting, + @"Wanted to restore auth, but was in wrong state: %d", self->connectionState); + if (self.authToken == nil) { + FFLog(@"I-RDB034034", @"Not restoring auth because token is nil"); + self->connectionState = ConnectionStateConnected; + [self restoreState]; + } else { + FFLog(@"I-RDB034035", @"Restoring auth"); + self->connectionState = ConnectionStateAuthenticating; + [self sendAuthAndRestoreStateAfterComplete:YES]; + } +} + +- (void) restoreState { + NSAssert(self->connectionState == ConnectionStateConnected, + @"Should be connected if we're restoring state, but we are: %d", self->connectionState); + + [self.listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *query, FOutstandingQuery *outstandingListen, BOOL *stop) { + FFLog(@"I-RDB034036", @"Restoring listen for %@", query); + [self sendListen:outstandingListen]; + }]; + + NSArray* keys = [[self.outstandingPuts allKeys] sortedArrayUsingSelector:@selector(compare:)]; + for(int i = 0; i < [keys count]; i++) { + if([self.outstandingPuts objectForKey:[keys objectAtIndex:i]] != nil) { + FFLog(@"I-RDB034037", @"Restoring put: %d", i); + [self sendPut:[keys objectAtIndex:i]]; + } + else { + FFLog(@"I-RDB034038", @"Restoring put: skipped nil: %d", i); + } + } + + for (FTupleOnDisconnect* tuple in self.onDisconnectQueue) { + [self sendOnDisconnectAction:tuple.action forPath:tuple.pathString withData:tuple.data andCallback:tuple.onComplete]; + } + [self.onDisconnectQueue removeAllObjects]; +} + +- (NSArray *) removeListen:(FQuerySpec *)query { + NSAssert(query.isDefault || !query.loadsAllData, @"removeListen called for non-default but complete query"); + + FOutstandingQuery* outstanding = self.listens[query]; + if (!outstanding) { + FFLog(@"I-RDB034039", @"Trying to remove listener for query %@ but no listener exists", query); + return @[]; + } else { + [self.listens removeObjectForKey:query]; + return @[outstanding]; + } +} + +- (NSArray *) removeAllListensAtPath:(FPath *)path { + FFLog(@"I-RDB034040", @"Removing all listens at path %@", path); + NSMutableArray *removed = [NSMutableArray array]; + NSMutableArray *toRemove = [NSMutableArray array]; + [self.listens enumerateKeysAndObjectsUsingBlock:^(FQuerySpec *spec, FOutstandingQuery *outstanding, BOOL *stop) { + if ([spec.path isEqual:path]) { + [removed addObject:outstanding]; + [toRemove addObject:spec]; + } + }]; + [self.listens removeObjectsForKeys:toRemove]; + return removed; +} + +- (void) purgeOutstandingWrites { + // We might have unacked puts in our queue that we need to ack now before we send out any cancels... + [self ackPuts]; + // Cancel in order + NSArray* keys = [[self.outstandingPuts allKeys] sortedArrayUsingSelector:@selector(compare:)]; + for (NSNumber *key in keys) { + FOutstandingPut *put = self.outstandingPuts[key]; + if (put.onCompleteBlock != nil) { + put.onCompleteBlock(kFErrorWriteCanceled, nil); + } + } + for (FTupleOnDisconnect *onDisconnect in self.onDisconnectQueue) { + if (onDisconnect.onComplete != nil) { + onDisconnect.onComplete(kFErrorWriteCanceled, nil); + } + } + [self.outstandingPuts removeAllObjects]; + [self.onDisconnectQueue removeAllObjects]; +} + +- (void) ackPuts { + for (FTupleCallbackStatus *put in self.putsToAck) { + put.block(put.status, put.errorReason); + } + [self.putsToAck removeAllObjects]; +} + +- (void) handleTimestamp:(NSNumber *)timestamp { + FFLog(@"I-RDB034041", @"Handling timestamp: %@", timestamp); + double timestampDeltaMs = [timestamp doubleValue] - ([[NSDate date] timeIntervalSince1970] * 1000); + [self.delegate onServerInfoUpdate:self updates:@{kDotInfoServerTimeOffset: [NSNumber numberWithDouble:timestampDeltaMs]}]; +} + +- (void) sendStats:(NSDictionary *)stats { + if ([stats count] > 0) { + NSDictionary *request = @{ kFWPRequestCounters: stats }; + [self sendAction:kFWPRequestActionStats body:request sensitive:NO callback:^(NSDictionary *data) { + NSString* status = [data objectForKey:kFWPResponseForActionStatus]; + NSString* errorReason = [data objectForKey:kFWPResponseForActionData]; + BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk]; + if (!statusOk) { + FFLog(@"I-RDB034042", @"Failed to send stats: %@", errorReason); + } + }]; + } else { + FFLog(@"I-RDB034043", @"Not sending stats because stats are empty"); + } +} + +- (void) sendConnectStats { + NSMutableDictionary *stats = [NSMutableDictionary dictionary]; + +#if TARGET_OS_IPHONE + if (self.config.persistenceEnabled) { + stats[@"persistence.ios.enabled"] = @1; + } +#else // this must be OSX then + if (self.config.persistenceEnabled) { + stats[@"persistence.osx.enabled"] = @1; + } +#endif + NSString *sdkVersion = [[FIRDatabase sdkVersion] stringByReplacingOccurrencesOfString:@"." withString:@"-"]; + NSString *sdkStatName = [NSString stringWithFormat:@"sdk.objc.%@", sdkVersion]; + stats[sdkStatName] = @1; + FFLog(@"I-RDB034044", @"Sending first connection stats"); + [self sendStats:stats]; +} + +- (NSDictionary *) dumpListens { + return self.listens; +} + +@end diff --git a/Firebase/Database/Core/FQueryParams.h b/Firebase/Database/Core/FQueryParams.h new file mode 100644 index 0000000..e9728e7 --- /dev/null +++ b/Firebase/Database/Core/FQueryParams.h @@ -0,0 +1,59 @@ +/* + * 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 <Foundation/Foundation.h> + +@protocol FIndex, FNodeFilter, FNode; + +@interface FQueryParams : NSObject <NSCopying> + +@property (nonatomic, readonly) BOOL limitSet; +@property (nonatomic, readonly) NSInteger limit; + +@property (nonatomic, strong, readonly) NSString *viewFrom; +@property (nonatomic, strong, readonly) id<FNode> indexStartValue; +@property (nonatomic, strong, readonly) NSString *indexStartKey; +@property (nonatomic, strong, readonly) id<FNode> indexEndValue; +@property (nonatomic, strong, readonly) NSString *indexEndKey; + +@property (nonatomic, strong, readonly) id<FIndex> index; + +- (BOOL)loadsAllData; +- (BOOL)isDefault; +- (BOOL)isValid; +- (BOOL)hasAnchoredLimit; + +- (FQueryParams *) limitTo:(NSInteger) limit; +- (FQueryParams *) limitToFirst:(NSInteger) newLimit; +- (FQueryParams *) limitToLast:(NSInteger) newLimit; + +- (FQueryParams *) startAt:(id<FNode>)indexValue childKey:(NSString *)key; +- (FQueryParams *) startAt:(id<FNode>)indexValue; +- (FQueryParams *) endAt:(id<FNode>)indexValue childKey:(NSString *)key; +- (FQueryParams *) endAt:(id<FNode>)indexValue; + +- (FQueryParams *) orderBy:(id<FIndex>) index; + ++ (FQueryParams *) defaultInstance; ++ (FQueryParams *) fromQueryObject:(NSDictionary *)dict; + +- (BOOL)hasStart; +- (BOOL)hasEnd; + +- (NSDictionary *) wireProtocolParams; +- (BOOL) isViewFromLeft; +- (id<FNodeFilter>) nodeFilter; +@end diff --git a/Firebase/Database/Core/FQueryParams.m b/Firebase/Database/Core/FQueryParams.m new file mode 100644 index 0000000..7920358 --- /dev/null +++ b/Firebase/Database/Core/FQueryParams.m @@ -0,0 +1,372 @@ +/* + * 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 "FQueryParams.h" +#import "FValidation.h" +#import "FConstants.h" +#import "FIndex.h" +#import "FPriorityIndex.h" +#import "FUtilities.h" +#import "FNodeFilter.h" +#import "FIndexedFilter.h" +#import "FLimitedFilter.h" +#import "FRangedFilter.h" +#import "FNode.h" +#import "FSnapshotUtilities.h" + +@interface FQueryParams () + +@property (nonatomic, readwrite) BOOL limitSet; +@property (nonatomic, readwrite) NSInteger limit; + +@property (nonatomic, strong, readwrite) NSString *viewFrom; +/** +* indexStartValue is anything you can store as a priority / value. +*/ +@property (nonatomic, strong, readwrite) id<FNode> indexStartValue; +@property (nonatomic, strong, readwrite) NSString *indexStartKey; +/** +* indexStartValue is anything you can store as a priority / value. +*/ +@property (nonatomic, strong, readwrite) id<FNode> indexEndValue; +@property (nonatomic, strong, readwrite) NSString *indexEndKey; + +@property (nonatomic, strong, readwrite) id<FIndex> index; + +@end + +@implementation FQueryParams + ++ (FQueryParams *) defaultInstance { + static FQueryParams *defaultParams = nil; + static dispatch_once_t defaultParamsToken; + dispatch_once(&defaultParamsToken, ^{ + defaultParams = [[FQueryParams alloc] init]; + }); + return defaultParams; +} + + +- (id)init { + self = [super init]; + if (self) { + self->_limitSet = NO; + self->_limit = 0; + + self->_viewFrom = nil; + self->_indexStartValue = nil; + self->_indexStartKey = nil; + self->_indexEndValue = nil; + self->_indexEndKey = nil; + + self->_index = [FPriorityIndex priorityIndex]; + } + return self; +} + +/** +* Only valid if hasStart is true +*/ +- (id) indexStartValue { + NSAssert([self hasStart], @"Only valid if start has been set"); + return _indexStartValue; +} + +/** +* Only valid if hasStart is true. +* @return The starting key name for the range defined by these query parameters +*/ +- (NSString *) indexStartKey { + NSAssert([self hasStart], @"Only valid if start has been set"); + if (_indexStartKey == nil) { + return [FUtilities minName]; + } else { + return _indexStartKey; + } +} + +/** +* Only valid if hasEnd is true. +*/ +- (id) indexEndValue { + NSAssert([self hasEnd], @"Only valid if end has been set"); + return _indexEndValue; +} + +/** +* Only valid if hasEnd is true. +* @return The end key name for the range defined by these query parameters +*/ +- (NSString *) indexEndKey { + NSAssert([self hasEnd], @"Only valid if end has been set"); + if (_indexEndKey == nil) { + return [FUtilities maxName]; + } else { + return _indexEndKey; + } +} + +/** +* @return true if a limit has been set and has been explicitly anchored +*/ +- (BOOL) hasAnchoredLimit { + return self.limitSet && self.viewFrom != nil; +} + +/** +* Only valid to call if limitSet returns true +*/ +- (NSInteger) limit { + NSAssert(self.limitSet, @"Only valid if limit has been set"); + return _limit; +} + +- (BOOL)hasStart { + return self->_indexStartValue != nil; +} + +- (BOOL)hasEnd { + return self->_indexEndValue != nil; +} + +- (id) copyWithZone:(NSZone *)zone { + // Immutable + return self; +} + +- (id) mutableCopy { + FQueryParams* other = [[[self class] alloc] init]; + // Maybe need to do extra copying here + other->_limitSet = _limitSet; + other->_limit = _limit; + other->_indexStartValue = _indexStartValue; + other->_indexStartKey = _indexStartKey; + other->_indexEndValue = _indexEndValue; + other->_indexEndKey = _indexEndKey; + other->_viewFrom = _viewFrom; + other->_index = _index; + return other; +} + +- (FQueryParams *) limitTo:(NSInteger)newLimit { + FQueryParams *newParams = [self mutableCopy]; + newParams->_limitSet = YES; + newParams->_limit = newLimit; + newParams->_viewFrom = nil; + return newParams; +} + +- (FQueryParams *) limitToFirst:(NSInteger)newLimit { + FQueryParams *newParams = [self mutableCopy]; + newParams->_limitSet = YES; + newParams->_limit = newLimit; + newParams->_viewFrom = kFQPViewFromLeft; + return newParams; +} + +- (FQueryParams *) limitToLast:(NSInteger)newLimit { + FQueryParams *newParams = [self mutableCopy]; + newParams->_limitSet = YES; + newParams->_limit = newLimit; + newParams->_viewFrom = kFQPViewFromRight; + return newParams; +} + +- (FQueryParams *) startAt:(id<FNode>)indexValue childKey:(NSString *)key { + NSAssert([indexValue isLeafNode] || [indexValue isEmpty], nil); + FQueryParams *newParams = [self mutableCopy]; + newParams->_indexStartValue = indexValue; + newParams->_indexStartKey = key; + return newParams; +} + +- (FQueryParams *) startAt:(id<FNode>)indexValue { + return [self startAt:indexValue childKey:nil]; +} + +- (FQueryParams *) endAt:(id<FNode>)indexValue childKey:(NSString *)key { + NSAssert([indexValue isLeafNode] || [indexValue isEmpty], nil); + FQueryParams *newParams = [self mutableCopy]; + newParams->_indexEndValue = indexValue; + newParams->_indexEndKey = key; + return newParams; +} + +- (FQueryParams *) endAt:(id<FNode>)indexValue { + return [self endAt:indexValue childKey:nil]; +} + +- (FQueryParams *) orderBy:(id)newIndex { + FQueryParams *newParams = [self mutableCopy]; + newParams->_index = newIndex; + return newParams; +} + +- (NSDictionary *) wireProtocolParams { + NSMutableDictionary* dict = [[NSMutableDictionary alloc] init]; + if ([self hasStart]) { + [dict setObject:[self.indexStartValue valForExport:YES] forKey:kFQPIndexStartValue]; + + // Don't use property as it will be [MIN-NAME] + if (self->_indexStartKey != nil) { + [dict setObject:self->_indexStartKey forKey:kFQPIndexStartName]; + } + } + + if ([self hasEnd]) { + [dict setObject:[self.indexEndValue valForExport:YES] forKey:kFQPIndexEndValue]; + + // Don't use property as it will be [MAX-NAME] + if (self->_indexEndKey != nil) { + [dict setObject:self->_indexEndKey forKey:kFQPIndexEndName]; + } + } + + if (self.limitSet) { + [dict setObject:[NSNumber numberWithInteger:self.limit] forKey:kFQPLimit]; + NSString *vf = self.viewFrom; + if (vf == nil) { + // limit() rather than limitToFirst or limitToLast was called. + // This means that only one of startSet or endSet is true. Use them + // to calculate which side of the view to anchor to. If neither is set, + // Anchor to end + if ([self hasStart]) { + vf = kFQPViewFromLeft; + } else { + vf = kFQPViewFromRight; + } + } + [dict setObject:vf forKey:kFQPViewFrom]; + } + + // For now, priority index is the default, so we only specify if it's some other index. + if (![self.index isEqual:[FPriorityIndex priorityIndex]]) { + [dict setObject:[self.index queryDefinition] forKey:kFQPIndex]; + } + + return dict; +} + ++ (FQueryParams *)fromQueryObject:(NSDictionary *)dict { + if (dict.count == 0) { + return [FQueryParams defaultInstance]; + } + + FQueryParams *params = [[FQueryParams alloc] init]; + if (dict[kFQPLimit] != nil) { + params->_limitSet = YES; + params->_limit = [dict[kFQPLimit] integerValue]; + } + + if (dict[kFQPIndexStartValue] != nil) { + params->_indexStartValue = [FSnapshotUtilities nodeFrom:dict[kFQPIndexStartValue]]; + if (dict[kFQPIndexStartName] != nil) { + params->_indexStartKey = dict[kFQPIndexStartName]; + } + } + + if (dict[kFQPIndexEndValue] != nil) { + params->_indexEndValue = [FSnapshotUtilities nodeFrom:dict[kFQPIndexEndValue]]; + if (dict[kFQPIndexEndName] != nil) { + params->_indexEndKey = dict[kFQPIndexEndName]; + } + } + + if (dict[kFQPViewFrom] != nil) { + NSString *viewFrom = dict[kFQPViewFrom]; + if (![viewFrom isEqualToString:kFQPViewFromLeft] && ![viewFrom isEqualToString:kFQPViewFromRight]) { + [NSException raise:NSInvalidArgumentException format:@"Unknown view from paramter: %@", viewFrom]; + } + params->_viewFrom = viewFrom; + } + + NSString *index = dict[kFQPIndex]; + if (index != nil) { + params->_index = [FIndex indexFromQueryDefinition:index]; + } + + return params; +} + +- (BOOL) isViewFromLeft { + if (self.viewFrom != nil) { + // Not null, we can just check + return [self.viewFrom isEqualToString:kFQPViewFromLeft]; + } else { + // If start is set, it's view from left. Otherwise not. + return self.hasStart; + } +} + +- (id<FNodeFilter>) nodeFilter { + if (self.loadsAllData) { + return [[FIndexedFilter alloc] initWithIndex:self.index]; + } else if (self.limitSet) { + return [[FLimitedFilter alloc] initWithQueryParams:self]; + } else { + return [[FRangedFilter alloc] initWithQueryParams:self]; + } +} + + +- (BOOL) isValid { + return !(self.hasStart && self.hasEnd && self.limitSet && !self.hasAnchoredLimit); +} + +- (BOOL) loadsAllData { + return !(self.hasStart || self.hasEnd || self.limitSet); +} + +- (BOOL) isDefault { + return [self loadsAllData] && [self.index isEqual:[FPriorityIndex priorityIndex]]; +} + +- (NSString *) description { + return [[self wireProtocolParams] description]; +} + +- (BOOL) isEqual:(id)obj { + if (self == obj) { + return YES; + } + if (![obj isKindOfClass:[self class]]) { + return NO; + } + FQueryParams *other = (FQueryParams *)obj; + if (self->_limitSet != other->_limitSet) return NO; + if (self->_limit != other->_limit) return NO; + if ((self->_index != other->_index) && ![self->_index isEqual:other->_index]) return NO; + if ((self->_indexStartKey != other->_indexStartKey) && ![self->_indexStartKey isEqualToString:other->_indexStartKey]) return NO; + if ((self->_indexStartValue != other->_indexStartValue) && ![self->_indexStartValue isEqual:other->_indexStartValue]) return NO; + if ((self->_indexEndKey != other->_indexEndKey) && ![self->_indexEndKey isEqualToString:other->_indexEndKey]) return NO; + if ((self->_indexEndValue != other->_indexEndValue) && ![self->_indexEndValue isEqual:other->_indexEndValue]) return NO; + if ([self isViewFromLeft] != [other isViewFromLeft]) return NO; + + return YES; +} + +- (NSUInteger) hash { + NSUInteger result = _limitSet ? _limit : 0; + result = 31 * result + ([self isViewFromLeft] ? 1231 : 1237); + result = 31 * result + [_indexStartKey hash]; + result = 31 * result + [_indexStartValue hash]; + result = 31 * result + [_indexEndKey hash]; + result = 31 * result + [_indexEndValue hash]; + result = 31 * result + [_index hash]; + return result; +} + +@end diff --git a/Firebase/Database/Core/FQuerySpec.h b/Firebase/Database/Core/FQuerySpec.h new file mode 100644 index 0000000..49ed536 --- /dev/null +++ b/Firebase/Database/Core/FQuerySpec.h @@ -0,0 +1,36 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FQueryParams.h" +#import "FPath.h" +#import "FIndex.h" + +@interface FQuerySpec : NSObject<NSCopying> + +@property (nonatomic, strong, readonly) FPath* path; +@property (nonatomic, strong, readonly) FQueryParams *params; + +- (id)initWithPath:(FPath *)path params:(FQueryParams *)params; + ++ (FQuerySpec *)defaultQueryAtPath:(FPath *)path; + +- (id<FIndex>)index; +- (BOOL)isDefault; +- (BOOL)loadsAllData; + +@end diff --git a/Firebase/Database/Core/FQuerySpec.m b/Firebase/Database/Core/FQuerySpec.m new file mode 100644 index 0000000..24be433 --- /dev/null +++ b/Firebase/Database/Core/FQuerySpec.m @@ -0,0 +1,85 @@ +/* + * 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 "FQuerySpec.h" + +@interface FQuerySpec () + +@property (nonatomic, strong, readwrite) FPath* path; +@property (nonatomic, strong, readwrite) FQueryParams *params; + + +@end + +@implementation FQuerySpec + +- (id)initWithPath:(FPath *)path params:(FQueryParams *)params { + self = [super init]; + if (self != nil) { + self->_path = path; + self->_params = params; + } + return self; +} + ++ (FQuerySpec *)defaultQueryAtPath:(FPath *)path { + return [[FQuerySpec alloc] initWithPath:path params:[FQueryParams defaultInstance]]; +} + +- (id)copyWithZone:(NSZone *)zone { + // Immutable + return self; +} + +- (id<FIndex>)index { + return self.params.index; +} + +- (BOOL)isDefault { + return self.params.isDefault; +} + +- (BOOL)loadsAllData { + return self.params.loadsAllData; +} + +- (BOOL)isEqual:(id)object { + if (self == object) { + return YES; + } + + if (![object isKindOfClass:[FQuerySpec class]]) { + return NO; + } + + FQuerySpec *other = (FQuerySpec *)object; + + if (![self.path isEqual:other.path]) { + return NO; + } + + return [self.params isEqual:other.params]; +} + +- (NSUInteger)hash { + return self.path.hash * 31 + self.params.hash; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"FQuerySpec (path: %@, params: %@)", self.path, self.params]; +} + +@end diff --git a/Firebase/Database/Core/FRangeMerge.h b/Firebase/Database/Core/FRangeMerge.h new file mode 100644 index 0000000..8825e0e --- /dev/null +++ b/Firebase/Database/Core/FRangeMerge.h @@ -0,0 +1,35 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FNode.h" + +/** + * Applies a merge of a snap for a given interval of paths. + * Each leaf in the current node which the relative path lies *after* (the optional) start and lies *before or at* + * (the optional) end will be deleted. Each leaf in snap that lies in the interval will be added to the resulting node. + * Nodes outside of the range are ignored. nil for start and end are sentinel values that represent -infinity and + * +infinity respectively (aka includes any path). + * Priorities of children nodes are treated as leaf children of that node. + */ +@interface FRangeMerge : NSObject + +- (instancetype)initWithStart:(FPath *)start end:(FPath *)end updates:(id<FNode>)updates; + +- (id<FNode>)applyToNode:(id<FNode>)node; + +@end diff --git a/Firebase/Database/Core/FRangeMerge.m b/Firebase/Database/Core/FRangeMerge.m new file mode 100644 index 0000000..8bc67bf --- /dev/null +++ b/Firebase/Database/Core/FRangeMerge.m @@ -0,0 +1,107 @@ +/* + * 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 "FRangeMerge.h" + +#import "FEmptyNode.h" + +@interface FRangeMerge () + +@property (nonatomic, strong) FPath *optExclusiveStart; +@property (nonatomic, strong) FPath *optInclusiveEnd; +@property (nonatomic, strong) id<FNode> updates; + +@end + +@implementation FRangeMerge + +- (instancetype)initWithStart:(FPath *)start end:(FPath *)end updates:(id<FNode>)updates { + self = [super init]; + if (self != nil) { + self->_optExclusiveStart = start; + self->_optInclusiveEnd = end; + self->_updates = updates; + } + return self; +} + +- (id<FNode>)applyToNode:(id<FNode>)node { + return [self updateRangeInNode:[FPath empty] node:node updates:self.updates]; +} + +- (id<FNode>)updateRangeInNode:(FPath *)currentPath node:(id<FNode>)node updates:(id<FNode>)updates { + NSComparisonResult startComparison = (self.optExclusiveStart == nil) ? NSOrderedDescending : [currentPath compare:self.optExclusiveStart]; + NSComparisonResult endComparison = (self.optInclusiveEnd == nil) ? NSOrderedAscending : [currentPath compare:self.optInclusiveEnd]; + BOOL startInNode = self.optExclusiveStart != nil && [currentPath contains:self.optExclusiveStart]; + BOOL endInNode = self.optInclusiveEnd != nil && [currentPath contains:self.optInclusiveEnd]; + if (startComparison == NSOrderedDescending && endComparison == NSOrderedAscending && !endInNode) { + // child is completly contained + return updates; + } else if (startComparison == NSOrderedDescending && endInNode && [updates isLeafNode]) { + return updates; + } else if (startComparison == NSOrderedDescending && endComparison == NSOrderedSame) { + NSAssert(endInNode, @"End not in node"); + NSAssert(![updates isLeafNode], @"Found leaf node update, this case should have been handled above."); + if ([node isLeafNode]) { + // Update node was not a leaf node, so we can delete it + return [FEmptyNode emptyNode]; + } else { + // Unaffected by range, ignore + return node; + } + } else if (startInNode || endInNode) { + // There is a partial update we need to do, so collect all relevant children + NSMutableSet *allChildren = [NSMutableSet set]; + [node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) { + [allChildren addObject:key]; + }]; + [updates enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) { + [allChildren addObject:key]; + }]; + + __block id<FNode> newNode = node; + void (^action)(id, BOOL *) = ^void(NSString *key, BOOL *stop) { + id<FNode> currentChild = [node getImmediateChild:key]; + id<FNode> updatedChild = [self updateRangeInNode:[currentPath childFromString:key] + node:currentChild + updates:[updates getImmediateChild:key]]; + // Only need to update if the node changed + if (updatedChild != currentChild) { + newNode = [newNode updateImmediateChild:key withNewChild:updatedChild]; + } + }; + + [allChildren enumerateObjectsUsingBlock:action]; + + // Add priority last, so the node is not empty when applying + if (!updates.getPriority.isEmpty || !node.getPriority.isEmpty) { + BOOL stop = NO; + action(@".priority", &stop); + } + return newNode; + } else { + // Unaffected by this range + NSAssert(endComparison == NSOrderedDescending || startComparison <= NSOrderedSame, @"Invalid range for update"); + return node; + } +} + +- (NSString *)description { + return [NSString stringWithFormat:@"RangeMerge (optExclusiveStart = %@, optExclusiveEng = %@, updates = %@)", + self.optExclusiveStart, self.optInclusiveEnd, self.updates]; +} + +@end diff --git a/Firebase/Database/Core/FRepo.h b/Firebase/Database/Core/FRepo.h new file mode 100644 index 0000000..69ec6bf --- /dev/null +++ b/Firebase/Database/Core/FRepo.h @@ -0,0 +1,76 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FRepoInfo.h" +#import "FPersistentConnection.h" +#import "FIRDataEventType.h" +#import "FTupleUserCallback.h" + +@class FQuerySpec; +@class FPersistence; +@class FAuthenticationManager; +@class FIRDatabaseConfig; +@protocol FEventRegistration; +@class FCompoundWrite; +@protocol FClock; +@class FIRDatabase; + +@interface FRepo : NSObject <FPersistentConnectionDelegate> + +@property (nonatomic, strong) FIRDatabaseConfig *config; + +- (id)initWithRepoInfo:(FRepoInfo *)info config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database; + +- (void) set:(FPath *)path withNode:(id)node withCallback:(fbt_void_nserror_ref)onComplete; +- (void) update:(FPath *)path withNodes:(FCompoundWrite *)compoundWrite withCallback:(fbt_void_nserror_ref)callback; +- (void) purgeOutstandingWrites; + +- (void) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query; +- (void) removeEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query; +- (void) keepQuery:(FQuerySpec *)query synced:(BOOL)synced; + +- (NSString*)name; +- (NSTimeInterval)serverTime; + +- (void) onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)message isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId; +- (void) onConnect:(FPersistentConnection *)fpconnection; +- (void) onDisconnect:(FPersistentConnection *)fpconnection; + +// Disconnect methods +- (void) onDisconnectCancel:(FPath *)path withCallback:(fbt_void_nserror_ref)callback; +- (void) onDisconnectSet:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)callback; +- (void) onDisconnectUpdate:(FPath *)path withNodes:(FCompoundWrite *)compoundWrite withCallback:(fbt_void_nserror_ref)callback; + +// Connection Management. +- (void) interrupt; +- (void) resume; + +// Transactions +- (void) startTransactionOnPath:(FPath *)path + update:(fbt_transactionresult_mutabledata)update + onComplete:(fbt_void_nserror_bool_datasnapshot)onComplete + withLocalEvents:(BOOL)applyLocally; + +// Testing methods +- (NSDictionary *) dumpListens; +- (void) dispose; +- (void) setHijackHash:(BOOL)hijack; + +@property (nonatomic, strong, readonly) FAuthenticationManager *auth; +@property (nonatomic, strong, readonly) FIRDatabase *database; + +@end diff --git a/Firebase/Database/Core/FRepo.m b/Firebase/Database/Core/FRepo.m new file mode 100644 index 0000000..06cc253 --- /dev/null +++ b/Firebase/Database/Core/FRepo.m @@ -0,0 +1,1116 @@ +/* + * 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 <dlfcn.h> +#import "FRepo.h" +#import "FSnapshotUtilities.h" +#import "FConstants.h" +#import "FIRDatabaseQuery_Private.h" +#import "FQuerySpec.h" +#import "FTupleNodePath.h" +#import "FRepo_Private.h" +#import "FRepoManager.h" +#import "FServerValues.h" +#import "FTupleSetIdPath.h" +#import "FSyncTree.h" +#import "FEventRegistration.h" +#import "FAtomicNumber.h" +#import "FSyncTree.h" +#import "FListenProvider.h" +#import "FEventRaiser.h" +#import "FSnapshotHolder.h" +#import "FIRDatabaseConfig_Private.h" +#import "FLevelDBStorageEngine.h" +#import "FPersistenceManager.h" +#import "FWriteRecord.h" +#import "FCachePolicy.h" +#import "FClock.h" +#import "FIRDatabase_Private.h" +#import "FTree.h" +#import "FTupleTransaction.h" +#import "FIRTransactionResult.h" +#import "FIRTransactionResult_Private.h" +#import "FIRMutableData.h" +#import "FIRMutableData_Private.h" +#import "FIRDataSnapshot.h" +#import "FIRDataSnapshot_Private.h" +#import "FValueEventRegistration.h" +#import "FEmptyNode.h" + +#ifdef TARGET_OS_IPHONE +#import <UIKit/UIKit.h> +#endif + +@interface FRepo() + +@property (nonatomic, strong) FOffsetClock *serverClock; +@property (nonatomic, strong) FPersistenceManager* persistenceManager; +@property (nonatomic, strong) FIRDatabase *database; +@property (nonatomic, strong, readwrite) FAuthenticationManager *auth; +@property (nonatomic, strong) FSyncTree *infoSyncTree; +@property (nonatomic) NSInteger writeIdCounter; +@property (nonatomic) BOOL hijackHash; +@property (nonatomic, strong) FTree *transactionQueueTree; +@property (nonatomic) BOOL loggedTransactionPersistenceWarning; + +/** +* Test only. For load testing the server. +*/ +@property (nonatomic, strong) id (^interceptServerDataCallback)(NSString *pathString, id data); +@end + + +@implementation FRepo + +- (id)initWithRepoInfo:(FRepoInfo*)info config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database { + self = [super init]; + if (self) { + self.repoInfo = info; + self.config = config; + self.database = database; + + // Access can occur outside of shared queue, so the clock needs to be initialized here + self.serverClock = [[FOffsetClock alloc] initWithClock:[FSystemClock clock] offset:0]; + + self.connection = [[FPersistentConnection alloc] initWithRepoInfo:self.repoInfo dispatchQueue:[FIRDatabaseQuery sharedQueue] config:self.config]; + + // Needs to be called before authentication manager is instantiated + self.eventRaiser = [[FEventRaiser alloc] initWithQueue:self.config.callbackQueue]; + + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + [self deferredInit]; + }); + } + return self; +} + +- (void)deferredInit { + // TODO: cleanup on dealloc + __weak FRepo *weakSelf = self; + [self.config.authTokenProvider listenForTokenChanges:^(NSString *token) { + [weakSelf.connection refreshAuthToken:token]; + }]; + + // Open connection now so that by the time we are connected the deferred init has run + // This relies on the fact that all callbacks run on repos queue + self.connection.delegate = self; + [self.connection open]; + + self.dataUpdateCount = 0; + self.rangeMergeUpdateCount = 0; + self.interceptServerDataCallback = nil; + + if (self.config.persistenceEnabled) { + NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", self.repoInfo.host, self.repoInfo.namespace]; + NSString* persistencePrefix = [NSString stringWithFormat:@"%@/%@", self.config.sessionIdentifier, repoHashString]; + + id<FCachePolicy> cachePolicy = [[FLRUCachePolicy alloc] initWithMaxSize:self.config.persistenceCacheSizeBytes]; + + id<FStorageEngine> engine; + if (self.config.forceStorageEngine != nil) { + engine = self.config.forceStorageEngine; + } else { + FLevelDBStorageEngine *levelDBEngine = [[FLevelDBStorageEngine alloc] initWithPath:persistencePrefix]; + // We need the repo info to run the legacy migration. Future migrations will be managed by the database itself + // Remove this once we are confident that no-one is using legacy migration anymore... + [levelDBEngine runLegacyMigration:self.repoInfo]; + engine = levelDBEngine; + } + + self.persistenceManager = [[FPersistenceManager alloc] initWithStorageEngine:engine cachePolicy:cachePolicy]; + } else { + self.persistenceManager = nil; + } + + [self initTransactions]; + + // A list of data pieces and paths to be set when this client disconnects + self.onDisconnect = [[FSparseSnapshotTree alloc] init]; + self.infoData = [[FSnapshotHolder alloc] init]; + + FListenProvider *infoListenProvider = [[FListenProvider alloc] init]; + infoListenProvider.startListening = ^(FQuerySpec *query, + NSNumber *tagId, + id<FSyncTreeHash> hash, + fbt_nsarray_nsstring onComplete) { + NSArray *infoEvents = @[]; + FRepo *strongSelf = weakSelf; + id<FNode> node = [strongSelf.infoData getNode:query.path]; + // This is possibly a hack, but we have different semantics for .info endpoints. We don't raise null events + // on initial data... + if (![node isEmpty]) { + infoEvents = [strongSelf.infoSyncTree applyServerOverwriteAtPath:query.path newData:node]; + [strongSelf.eventRaiser raiseCallback:^{ + onComplete(kFWPResponseForActionStatusOk); + }]; + } + return infoEvents; + }; + infoListenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tagId) {}; + self.infoSyncTree = [[FSyncTree alloc] initWithListenProvider:infoListenProvider]; + + FListenProvider *serverListenProvider = [[FListenProvider alloc] init]; + serverListenProvider.startListening = ^(FQuerySpec *query, + NSNumber *tagId, + id<FSyncTreeHash> hash, + fbt_nsarray_nsstring onComplete) { + [weakSelf.connection listen:query tagId:tagId hash:hash onComplete:^(NSString *status) { + NSArray *events = onComplete(status); + [weakSelf.eventRaiser raiseEvents:events]; + }]; + // No synchronous events for network-backed sync trees + return @[]; + }; + serverListenProvider.stopListening = ^(FQuerySpec *query, NSNumber *tag) { + [weakSelf.connection unlisten:query tagId:tag]; + }; + self.serverSyncTree = [[FSyncTree alloc] initWithPersistenceManager:self.persistenceManager + listenProvider:serverListenProvider]; + + [self restoreWrites]; + + [self updateInfo:kDotInfoConnected withValue:@NO]; + + [self setupNotifications]; +} + + +- (void) restoreWrites { + NSArray *writes = self.persistenceManager.userWrites; + + NSDictionary *serverValues = [FServerValues generateServerValues:self.serverClock]; + __block NSInteger lastWriteId = NSIntegerMin; + [writes enumerateObjectsUsingBlock:^(FWriteRecord *write, NSUInteger idx, BOOL *stop) { + NSInteger writeId = write.writeId; + fbt_void_nsstring_nsstring callback = ^(NSString *status, NSString *errorReason) { + [self warnIfWriteFailedAtPath:write.path status:status message:@"Persisted write"]; + [self ackWrite:writeId rerunTransactionsAtPath:write.path status:status]; + }; + if (lastWriteId >= writeId) { + [NSException raise:NSInternalInconsistencyException format:@"Restored writes were not in order!"]; + } + lastWriteId = writeId; + self.writeIdCounter = writeId + 1; + if ([write isOverwrite]) { + FFLog(@"I-RDB038001", @"Restoring overwrite with id %ld", (long)write.writeId); + [self.connection putData:[write.overwrite valForExport:YES] + forPath:[write.path toString] + withHash:nil + withCallback:callback]; + id<FNode> resolved = [FServerValues resolveDeferredValueSnapshot:write.overwrite withServerValues:serverValues]; + [self.serverSyncTree applyUserOverwriteAtPath:write.path newData:resolved writeId:writeId isVisible:YES]; + } else { + FFLog(@"I-RDB038002", @"Restoring merge with id %ld", (long)write.writeId); + [self.connection mergeData:[write.merge valForExport:YES] + forPath:[write.path toString] + withCallback:callback]; + FCompoundWrite *resolved = [FServerValues resolveDeferredValueCompoundWrite:write.merge withServerValues:serverValues]; + [self.serverSyncTree applyUserMergeAtPath:write.path changedChildren:resolved writeId:writeId]; + } + }]; +} + +- (NSString*)name { + return self.repoInfo.namespace; +} + +- (NSString *) description { + return [self.repoInfo description]; +} + +- (void) interrupt { + [self.connection interruptForReason:kFInterruptReasonRepoInterrupt]; +} + +- (void) resume { + [self.connection resumeForReason:kFInterruptReasonRepoInterrupt]; +} + +// NOTE: Typically if you're calling this, you should be in an @autoreleasepool block to make sure that ARC kicks +// in and cleans up things no longer referenced (i.e. pendingPutsDB). +- (void) dispose { + [self.connection interruptForReason:kFInterruptReasonRepoInterrupt]; + + // We need to nil out any references to LevelDB, to make sure the + // LevelDB exclusive locks are released. + [self.persistenceManager close]; +} + +- (NSInteger) nextWriteId { + return self->_writeIdCounter++; +} + +- (NSTimeInterval) serverTime { + return [self.serverClock currentTime]; +} + +- (void) set:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)onComplete { + id value = [node valForExport:YES]; + FFLog(@"I-RDB038003", @"Setting: %@ with %@ pri: %@", [path toString], [value description], [[node getPriority] val]); + + // TODO: Optimize this behavior to either (a) store flag to skip resolving where possible and / or + // (b) store unresolved paths on JSON parse + NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock]; + id<FNode> newNode = [FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues]; + + NSInteger writeId = [self nextWriteId]; + [self.persistenceManager saveUserOverwrite:node atPath:path writeId:writeId]; + NSArray *events = [self.serverSyncTree applyUserOverwriteAtPath:path newData:newNode writeId:writeId isVisible:YES]; + [self.eventRaiser raiseEvents:events]; + + [self.connection putData:value forPath:[path toString] withHash:nil withCallback:^(NSString *status, NSString *errorReason) { + [self warnIfWriteFailedAtPath:path status:status message:@"setValue: or removeValue:"]; + [self ackWrite:writeId rerunTransactionsAtPath:path status:status]; + [self callOnComplete:onComplete withStatus:status errorReason:errorReason andPath:path]; + }]; + + FPath* affectedPath = [self abortTransactionsAtPath:path error:kFTransactionSet]; + [self rerunTransactionsForPath:affectedPath]; +} + +- (void) update:(FPath *)path withNodes:(FCompoundWrite *)nodes withCallback:(fbt_void_nserror_ref)callback { + NSDictionary *values = [nodes valForExport:YES]; + + FFLog(@"I-RDB038004", @"Updating: %@ with %@", [path toString], [values description]); + NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock]; + FCompoundWrite *resolved = [FServerValues resolveDeferredValueCompoundWrite:nodes withServerValues:serverValues]; + + if (!resolved.isEmpty) { + NSInteger writeId = [self nextWriteId]; + [self.persistenceManager saveUserMerge:nodes atPath:path writeId:writeId]; + NSArray *events = [self.serverSyncTree applyUserMergeAtPath:path changedChildren:resolved writeId:writeId]; + [self.eventRaiser raiseEvents:events]; + + [self.connection mergeData:values forPath:[path description] withCallback:^(NSString *status, NSString *errorReason) { + [self warnIfWriteFailedAtPath:path status:status message:@"updateChildValues:"]; + [self ackWrite:writeId rerunTransactionsAtPath:path status:status]; + [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path]; + }]; + + [nodes enumerateWrites:^(FPath *childPath, id<FNode> node, BOOL *stop) { + FPath* pathFromRoot = [path child:childPath]; + FFLog(@"I-RDB038005", @"Cancelling transactions at path: %@", pathFromRoot); + FPath *affectedPath = [self abortTransactionsAtPath:pathFromRoot error:kFTransactionSet]; + [self rerunTransactionsForPath:affectedPath]; + }]; + } else { + FFLog(@"I-RDB038006", @"update called with empty data. Doing nothing"); + // Do nothing, just call the callback + [self callOnComplete:callback withStatus:@"ok" errorReason:nil andPath:path]; + } +} + +- (void) onDisconnectCancel:(FPath *)path withCallback:(fbt_void_nserror_ref)callback { + [self.connection onDisconnectCancelPath:path withCallback:^(NSString *status, NSString *errorReason) { + BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk]; + if (success) { + [self.onDisconnect forgetPath:path]; + } else { + FFLog(@"I-RDB038007", @"cancelDisconnectOperations: at %@ failed: %@", path, status); + } + + [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path]; + }]; +} + +- (void) onDisconnectSet:(FPath *)path withNode:(id<FNode>)node withCallback:(fbt_void_nserror_ref)callback { + [self.connection onDisconnectPutData:[node valForExport:YES] forPath:path withCallback:^(NSString *status, NSString *errorReason) { + BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk]; + if (success) { + [self.onDisconnect rememberData:node onPath:path]; + } else { + FFWarn(@"I-RDB038008", @"onDisconnectSetValue: or onDisconnectRemoveValue: at %@ failed: %@", path, status); + } + + [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path]; + }]; +} + +- (void) onDisconnectUpdate:(FPath *)path withNodes:(FCompoundWrite *)nodes withCallback:(fbt_void_nserror_ref)callback { + if (!nodes.isEmpty) { + NSDictionary *values = [nodes valForExport:YES]; + + [self.connection onDisconnectMergeData:values forPath:path withCallback:^(NSString *status, NSString *errorReason) { + BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk]; + if (success) { + [nodes enumerateWrites:^(FPath *relativePath, id<FNode> nodeUnresolved, BOOL *stop) { + FPath* childPath = [path child:relativePath]; + [self.onDisconnect rememberData:nodeUnresolved onPath:childPath]; + }]; + } else { + FFWarn(@"I-RDB038009", @"onDisconnectUpdateChildValues: at %@ failed %@", path, status); + } + + [self callOnComplete:callback withStatus:status errorReason:errorReason andPath:path]; + }]; + } else { + // Do nothing, just call the callback + [self callOnComplete:callback withStatus:@"ok" errorReason:nil andPath:path]; + } +} + +- (void) purgeOutstandingWrites { + FFLog(@"I-RDB038010", @"Purging outstanding writes"); + NSArray *events = [self.serverSyncTree removeAllWrites]; + [self.eventRaiser raiseEvents:events]; + // Abort any transactions + [self abortTransactionsAtPath:[FPath empty] error:kFErrorWriteCanceled]; + // Remove outstanding writes from connection + [self.connection purgeOutstandingWrites]; +} + +- (void) addEventRegistration:(id <FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query { + NSArray *events = nil; + if ([[query.path getFront] isEqualToString:kDotInfoPrefix]) { + events = [self.infoSyncTree addEventRegistration:eventRegistration forQuery:query]; + } else { + events = [self.serverSyncTree addEventRegistration:eventRegistration forQuery:query]; + } + [self.eventRaiser raiseEvents:events]; +} + +- (void) removeEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query { + // These are guaranteed not to raise events, since we're not passing in a cancelError. However we can future-proof + // a little bit by handling the return values anyways. + FFLog(@"I-RDB038011", @"Removing event registration with hande: %lu", (unsigned long)eventRegistration.handle); + NSArray *events = nil; + if ([[query.path getFront] isEqualToString:kDotInfoPrefix]) { + events = [self.infoSyncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil]; + } else { + events = [self.serverSyncTree removeEventRegistration:eventRegistration forQuery:query cancelError:nil]; + } + [self.eventRaiser raiseEvents:events]; +} + +- (void) keepQuery:(FQuerySpec *)query synced:(BOOL)synced { + NSAssert(![[query.path getFront] isEqualToString:kDotInfoPrefix], @"Can't keep .info tree synced!"); + [self.serverSyncTree keepQuery:query synced:synced]; +} + +- (void) updateInfo:(NSString *) pathString withValue:(id)value { + // hack to make serverTimeOffset available in a threadsafe way. Property is marked as atomic + if ([pathString isEqualToString:kDotInfoServerTimeOffset]) { + NSTimeInterval offset = [(NSNumber *)value doubleValue]/1000.0; + self.serverClock = [[FOffsetClock alloc] initWithClock:[FSystemClock clock] offset:offset]; + } + + FPath* path = [[FPath alloc] initWith:[NSString stringWithFormat:@"%@/%@", kDotInfoPrefix, pathString]]; + id<FNode> newNode = [FSnapshotUtilities nodeFrom:value]; + [self.infoData updateSnapshot:path withNewSnapshot:newNode]; + NSArray *events = [self.infoSyncTree applyServerOverwriteAtPath:path newData:newNode]; + [self.eventRaiser raiseEvents:events]; +} + +- (void) callOnComplete:(fbt_void_nserror_ref)onComplete withStatus:(NSString *)status errorReason:(NSString *)errorReason andPath:(FPath *)path { + if (onComplete) { + FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithRepo:self path:path]; + BOOL statusOk = [status isEqualToString:kFWPResponseForActionStatusOk]; + NSError* err = nil; + if (!statusOk) { + err = [FUtilities errorForStatus:status andReason:errorReason]; + } + [self.eventRaiser raiseCallback:^{ + onComplete(err, ref); + }]; + } +} + +- (void)ackWrite:(NSInteger)writeId rerunTransactionsAtPath:(FPath *)path status:(NSString *)status { + if ([status isEqualToString:kFErrorWriteCanceled]) { + // This write was already removed, we just need to ignore it... + } else { + BOOL success = [status isEqualToString:kFWPResponseForActionStatusOk]; + NSArray *clearEvents = [self.serverSyncTree ackUserWriteWithWriteId:writeId revert:!success persist:YES clock:self.serverClock]; + if ([clearEvents count] > 0) { + [self rerunTransactionsForPath:path]; + } + [self.eventRaiser raiseEvents:clearEvents]; + } +} + +- (void) warnIfWriteFailedAtPath:(FPath *)path status:(NSString *)status message:(NSString *)message { + if (!([status isEqualToString:kFWPResponseForActionStatusOk] || [status isEqualToString:kFErrorWriteCanceled])) { + FFWarn(@"I-RDB038012", @"%@ at %@ failed: %@", message, path, status); + } +} + +#pragma mark - +#pragma mark FPersistentConnectionDelegate methods + +- (void) onDataUpdate:(FPersistentConnection *)fpconnection forPath:(NSString *)pathString message:(id)data isMerge:(BOOL)isMerge tagId:(NSNumber *)tagId { + FFLog(@"I-RDB038013", @"onDataUpdateForPath: %@ withMessage: %@", pathString, data); + + // For testing. + self.dataUpdateCount++; + + FPath* path = [[FPath alloc] initWith:pathString]; + data = self.interceptServerDataCallback ? self.interceptServerDataCallback(pathString, data) : data; + NSArray *events = nil; + + if (tagId != nil) { + if (isMerge) { + NSDictionary *message = data; + FCompoundWrite *taggedChildren = [FCompoundWrite compoundWriteWithValueDictionary:message]; + events = [self.serverSyncTree applyTaggedQueryMergeAtPath:path changedChildren:taggedChildren tagId:tagId]; + } else { + id<FNode> taggedSnap = [FSnapshotUtilities nodeFrom:data]; + events = [self.serverSyncTree applyTaggedQueryOverwriteAtPath:path newData:taggedSnap tagId:tagId]; + } + } else if (isMerge) { + NSDictionary *message = data; + FCompoundWrite *changedChildren = [FCompoundWrite compoundWriteWithValueDictionary:message]; + events = [self.serverSyncTree applyServerMergeAtPath:path changedChildren:changedChildren]; + } else { + id<FNode> snap = [FSnapshotUtilities nodeFrom:data]; + events = [self.serverSyncTree applyServerOverwriteAtPath:path newData:snap]; + } + + if ([events count] > 0) { + // Since we have a listener outstanding for each transaction, receiving any events + // is a proxy for some change having occurred. + [self rerunTransactionsForPath:path]; + } + + [self.eventRaiser raiseEvents:events]; +} + +- (void)onRangeMerge:(NSArray *)ranges forPath:(NSString *)pathString tagId:(NSNumber *)tag { + FFLog(@"I-RDB038014", @"onRangeMerge: %@ => %@", pathString, ranges); + + // For testing + self.rangeMergeUpdateCount++; + + FPath* path = [[FPath alloc] initWith:pathString]; + NSArray *events; + if (tag != nil) { + events = [self.serverSyncTree applyTaggedServerRangeMergeAtPath:path updates:ranges tagId:tag]; + } else { + events = [self.serverSyncTree applyServerRangeMergeAtPath:path updates:ranges]; + } + if (events.count > 0) { + // Since we have a listener outstanding for each transaction, receiving any events + // is a proxy for some change having occurred. + [self rerunTransactionsForPath:path]; + } + + [self.eventRaiser raiseEvents:events]; +} + +- (void)onConnect:(FPersistentConnection *)fpconnection { + [self updateInfo:kDotInfoConnected withValue:@true]; +} + +- (void)onDisconnect:(FPersistentConnection *)fpconnection { + [self updateInfo:kDotInfoConnected withValue:@false]; + [self runOnDisconnectEvents]; +} + +- (void)onServerInfoUpdate:(FPersistentConnection *)fpconnection updates:(NSDictionary *)updates { + for (NSString* key in updates) { + id val = [updates objectForKey:key]; + [self updateInfo:key withValue:val]; + } +} + +- (void) setupNotifications { + NSString * const *backgroundConstant = (NSString * const *) dlsym(RTLD_DEFAULT, "UIApplicationDidEnterBackgroundNotification"); + if (backgroundConstant) { + FFLog(@"I-RDB038015", @"Registering for background notification."); + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didEnterBackground) + name:*backgroundConstant + object:nil]; + } else { + FFLog(@"I-RDB038016", @"Skipped registering for background notification."); + } +} + +- (void) didEnterBackground { + if (!self.config.persistenceEnabled) + return; + + // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual release build. +#if TARGET_OS_IPHONE + // The idea is to wait until any outstanding sets get written to disk. Since the sets might still be in our + // dispatch queue, we wait for the dispatch queue to catch up and for persistence to catch up. + // This may be undesirable though. The dispatch queue might just be processing a bunch of incoming data or + // something. We might want to keep track of whether there are any unpersisted sets or something. + FFLog(@"I-RDB038017", @"Entering background. Starting background task to finish work."); + Class uiApplicationClass = NSClassFromString(@"UIApplication"); + assert(uiApplicationClass); // If we are here, we should be on iOS and UIApplication should be available. + + UIApplication *application = [uiApplicationClass sharedApplication]; + __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{ + [application endBackgroundTask:bgTask]; + }]; + + NSDate *start = [NSDate date]; + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + NSTimeInterval finishTime = [start timeIntervalSinceNow]*-1; + FFLog(@"I-RDB038018", @"Background task completed. Queue time: %f", finishTime); + [application endBackgroundTask:bgTask]; + }); +#endif +} + +#pragma mark - +#pragma mark Internal methods + +/** +* Applies all the changes stored up in the onDisconnect tree +*/ +- (void) runOnDisconnectEvents { + FFLog(@"I-RDB038019", @"Running onDisconnectEvents"); + NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock]; + FSparseSnapshotTree* resolvedTree = [FServerValues resolveDeferredValueTree:self.onDisconnect withServerValues:serverValues]; + NSMutableArray *events = [[NSMutableArray alloc] init]; + + [resolvedTree forEachTreeAtPath:[FPath empty] do:^(FPath *path, id<FNode> node) { + [events addObjectsFromArray:[self.serverSyncTree applyServerOverwriteAtPath:path newData:node]]; + FPath* affectedPath = [self abortTransactionsAtPath:path error:kFTransactionSet]; + [self rerunTransactionsForPath:affectedPath]; + }]; + + self.onDisconnect = [[FSparseSnapshotTree alloc] init]; + [self.eventRaiser raiseEvents:events]; +} + +- (NSDictionary *) dumpListens { + return [self.connection dumpListens]; +} + +#pragma mark - +#pragma mark Transactions + +/** + * Setup the transaction data structures + */ +- (void) initTransactions { + self.transactionQueueTree = [[FTree alloc] init]; + self.hijackHash = NO; + self.loggedTransactionPersistenceWarning = NO; +} + +/** + * Creates a new transaction, add its to the transactions we're tracking, and sends it to the server if possible + */ +- (void) startTransactionOnPath:(FPath *)path update:(fbt_transactionresult_mutabledata)update onComplete:(fbt_void_nserror_bool_datasnapshot)onComplete withLocalEvents:(BOOL)applyLocally { + if (self.config.persistenceEnabled && !self.loggedTransactionPersistenceWarning) { + self.loggedTransactionPersistenceWarning = YES; + FFInfo(@"I-RDB038020", @"runTransactionBlock: usage detected while persistence is enabled. Please be aware that transactions " + @"*will not* be persisted across app restarts. " + @"See https://www.firebase.com/docs/ios/guide/offline-capabilities.html#section-handling-transactions-offline for more details."); + } + + FIRDatabaseReference * watchRef = [[FIRDatabaseReference alloc] initWithRepo:self path:path]; + // make sure we're listening on this node + // Note: we can't do this asynchronously. To preserve event ordering, it has to be done in this block. + // This is ok, this block is guaranteed to be our own event loop + NSUInteger handle = [[FUtilities LUIDGenerator] integerValue]; + fbt_void_datasnapshot cb = ^(FIRDataSnapshot *snapshot) {}; + FValueEventRegistration *registration = [[FValueEventRegistration alloc] initWithRepo:self + handle:handle + callback:cb + cancelCallback:nil]; + [watchRef.repo addEventRegistration:registration forQuery:watchRef.querySpec]; + fbt_void_void unwatcher = ^{ [watchRef removeObserverWithHandle:handle]; }; + + // Save all the data that represents this transaction + FTupleTransaction* transaction = [[FTupleTransaction alloc] init]; + transaction.path = path; + transaction.update = update; + transaction.onComplete = onComplete; + transaction.status = FTransactionInitializing; + transaction.order = [FUtilities LUIDGenerator]; + transaction.applyLocally = applyLocally; + transaction.retryCount = 0; + transaction.unwatcher = unwatcher; + transaction.currentWriteId = nil; + transaction.currentInputSnapshot = nil; + transaction.currentOutputSnapshotRaw = nil; + transaction.currentOutputSnapshotResolved = nil; + + // Run transaction initially + id<FNode> currentState = [self latestStateAtPath:path excludeWriteIds:nil]; + transaction.currentInputSnapshot = currentState; + FIRMutableData * mutableCurrent = [[FIRMutableData alloc] initWithNode:currentState]; + FIRTransactionResult * result = transaction.update(mutableCurrent); + + if (!result.isSuccess) { + // Abort the transaction + transaction.unwatcher(); + transaction.currentOutputSnapshotRaw = nil; + transaction.currentOutputSnapshotResolved = nil; + if (transaction.onComplete) { + FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path]; + FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:transaction.currentInputSnapshot]; + FIRDataSnapshot *snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:indexedNode]; + [self.eventRaiser raiseCallback:^{ + transaction.onComplete(nil, NO, snap); + }]; + } + } else { + // Note: different from js. We don't need to validate, FIRMutableData does validation. + // We also don't have to worry about priorities. Just mark as run and add to queue. + transaction.status = FTransactionRun; + FTree* queueNode = [self.transactionQueueTree subTree:transaction.path]; + NSMutableArray* nodeQueue = [queueNode getValue]; + if (nodeQueue == nil) { + nodeQueue = [[NSMutableArray alloc] init]; + } + [nodeQueue addObject:transaction]; + [queueNode setValue:nodeQueue]; + + // Update visibleData and raise events + // Note: We intentionally raise events after updating all of our transaction state, since the user could + // start new transactions from the event callbacks + NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock]; + id<FNode> newValUnresolved = [result.update nodeValue]; + id<FNode> newVal = [FServerValues resolveDeferredValueSnapshot:newValUnresolved withServerValues:serverValues]; + transaction.currentOutputSnapshotRaw = newValUnresolved; + transaction.currentOutputSnapshotResolved = newVal; + transaction.currentWriteId = [NSNumber numberWithInteger:[self nextWriteId]]; + + NSArray *events = [self.serverSyncTree applyUserOverwriteAtPath:path newData:newVal + writeId:[transaction.currentWriteId integerValue] + isVisible:transaction.applyLocally]; + [self.eventRaiser raiseEvents:events]; + + [self sendAllReadyTransactions]; + } +} + +/** + * @param writeIdsToExclude A specific set to exclude + */ +- (id<FNode>) latestStateAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude { + id<FNode> latestState = [self.serverSyncTree calcCompleteEventCacheAtPath:path excludeWriteIds:writeIdsToExclude]; + return latestState ? latestState : [FEmptyNode emptyNode]; +} + +/** + * Sends any already-run transactions that aren't waiting for outstanding transactions to complete. + * + * Externally, call the version with no arguments. + * Internally, calls itself recursively with a particular transactionQueueTree node to recurse through the tree + */ +- (void) sendAllReadyTransactions { + FTree* node = self.transactionQueueTree; + + [self pruneCompletedTransactionsBelowNode:node]; + [self sendReadyTransactionsForTree:node]; +} + +- (void) sendReadyTransactionsForTree:(FTree *)node { + NSMutableArray* queue = [node getValue]; + if (queue != nil) { + queue = [self buildTransactionQueueAtNode:node]; + NSAssert([queue count] > 0, @"Sending zero length transaction queue"); + + NSUInteger notRunIndex = [queue indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { + return ((FTupleTransaction*)obj).status != FTransactionRun; + }]; + + // If they're all run (and not sent), we can send them. Else, we must wait. + if (notRunIndex == NSNotFound) { + [self sendTransactionQueue:queue atPath:node.path]; + } + } else if ([node hasChildren]) { + [node forEachChild:^(FTree *child) { + [self sendReadyTransactionsForTree:child]; + }]; + } +} + +/** + * Given a list of run transactions, send them to the server and then handle the result (success or failure). + */ +- (void) sendTransactionQueue:(NSMutableArray *)queue atPath:(FPath *)path { + // Mark transactions as sent and bump the retry count + NSMutableArray *writeIdsToExclude = [[NSMutableArray alloc] init]; + for (FTupleTransaction *transaction in queue) { + [writeIdsToExclude addObject:transaction.currentWriteId]; + } + id<FNode> latestState = [self latestStateAtPath:path excludeWriteIds:writeIdsToExclude]; + id<FNode> snapToSend = latestState; + NSString *latestHash = [latestState dataHash]; + for (FTupleTransaction* transaction in queue) { + NSAssert(transaction.status == FTransactionRun, @"[FRepo sendTransactionQueue:] items in queue should all be run."); + FFLog(@"I-RDB038021", @"Transaction at %@ set to SENT", transaction.path); + transaction.status = FTransactionSent; + transaction.retryCount++; + FPath *relativePath = [FPath relativePathFrom:path to:transaction.path]; + // If we've gotten to this point, the output snapshot must be defined. + snapToSend = [snapToSend updateChild:relativePath withNewChild:transaction.currentOutputSnapshotRaw]; + } + + id dataToSend = [snapToSend valForExport:YES]; + NSString *pathToSend = [path description]; + latestHash = self.hijackHash ? @"badhash" : latestHash; + + // Send the put + [self.connection putData:dataToSend forPath:pathToSend withHash:latestHash withCallback:^(NSString *status, NSString *errorReason) { + FFLog(@"I-RDB038022", @"Transaction put response: %@ : %@", pathToSend, status); + + NSMutableArray *events = [[NSMutableArray alloc] init]; + if ([status isEqualToString:kFWPResponseForActionStatusOk]) { + // Queue up the callbacks and fire them after cleaning up all of our transaction state, since + // the callback could trigger more transactions or sets. + NSMutableArray *callbacks = [[NSMutableArray alloc] init]; + for (FTupleTransaction *transaction in queue) { + transaction.status = FTransactionCompleted; + [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue] + revert:NO + persist:NO + clock:self.serverClock]]; + if (transaction.onComplete) { + // We never unset the output snapshot, and given that this transaction is complete, it should be set + id <FNode> node = transaction.currentOutputSnapshotResolved; + FIndexedNode *indexedNode = [FIndexedNode indexedNodeWithNode:node]; + FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path]; + FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:indexedNode]; + fbt_void_void cb = ^{ + transaction.onComplete(nil, YES, snapshot); + }; + [callbacks addObject:[cb copy]]; + } + transaction.unwatcher(); + } + + // Now remove the completed transactions. + [self pruneCompletedTransactionsBelowNode:[self.transactionQueueTree subTree:path]]; + // There may be pending transactions that we can now send. + [self sendAllReadyTransactions]; + + // Finally, trigger onComplete callbacks + [self.eventRaiser raiseCallbacks:callbacks]; + } else { + // transactions are no longer sent. Update their status appropriately. + if ([status isEqualToString:kFWPResponseForActionStatusDataStale]) { + for (FTupleTransaction *transaction in queue) { + if (transaction.status == FTransactionSentNeedsAbort) { + transaction.status = FTransactionNeedsAbort; + } else { + transaction.status = FTransactionRun; + } + } + } else { + FFWarn(@"I-RDB038023", @"runTransactionBlock: at %@ failed: %@", path, status); + for (FTupleTransaction *transaction in queue) { + transaction.status = FTransactionNeedsAbort; + [transaction setAbortStatus:status reason:errorReason]; + } + } + } + + [self rerunTransactionsForPath:path]; + [self.eventRaiser raiseEvents:events]; + }]; +} + +/** + * Finds all transactions dependent on the data at changed Path and reruns them. + * + * Should be called any time cached data changes. + * + * Return the highest path that was affected by rerunning transactions. This is the path at which events need to + * be raised for. + */ +- (FPath *) rerunTransactionsForPath:(FPath *)changedPath { + // For the common case that there are no transactions going on, skip all this! + if ([self.transactionQueueTree isEmpty]) { + return changedPath; + } else { + FTree* rootMostTransactionNode = [self getAncestorTransactionNodeForPath:changedPath]; + FPath* path = rootMostTransactionNode.path; + + NSArray* queue = [self buildTransactionQueueAtNode:rootMostTransactionNode]; + [self rerunTransactionQueue:queue atPath:path]; + + return path; + } +} + +/** + * Does all the work of rerunning transactions (as well as cleans up aborted transactions and whatnot). + */ +- (void) rerunTransactionQueue:(NSArray *)queue atPath:(FPath *)path { + if (queue.count == 0) { + return; // nothing to do + } + + // Queue up the callbacks and fire them after cleaning up all of our transaction state, since + // the callback could trigger more transactions or sets. + NSMutableArray *events = [[NSMutableArray alloc] init]; + NSMutableArray *callbacks = [[NSMutableArray alloc] init]; + + // Ignore, by default, all of the sets in this queue, since we're re-running all of them. However, we want to include + // the results of new sets triggered as part of this re-run, so we don't want to ignore a range, just these specific + // sets. + NSMutableArray *writeIdsToExclude = [[NSMutableArray alloc] init]; + for (FTupleTransaction *transaction in queue) { + [writeIdsToExclude addObject:transaction.currentWriteId]; + } + + for (FTupleTransaction* transaction in queue) { + FPath* relativePath __unused = [FPath relativePathFrom:path to:transaction.path]; + BOOL abortTransaction = NO; + NSAssert(relativePath != nil, @"[FRepo rerunTransactionsQueue:] relativePath should not be null."); + + if (transaction.status == FTransactionNeedsAbort) { + abortTransaction = YES; + if (![transaction.abortStatus isEqualToString:kFErrorWriteCanceled]) { + NSArray *ackEvents = [self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue] + revert:YES + persist:NO + clock:self.serverClock]; + [events addObjectsFromArray:ackEvents]; + } + } else if (transaction.status == FTransactionRun) { + if (transaction.retryCount >= kFTransactionMaxRetries) { + abortTransaction = YES; + [transaction setAbortStatus:kFTransactionTooManyRetries reason:nil]; + [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue] + revert:YES + persist:NO + clock:self.serverClock]]; + } else { + // This code reruns a transaction + id<FNode> currentNode = [self latestStateAtPath:transaction.path excludeWriteIds:writeIdsToExclude]; + transaction.currentInputSnapshot = currentNode; + FIRMutableData * mutableCurrent = [[FIRMutableData alloc] initWithNode:currentNode]; + FIRTransactionResult * result = transaction.update(mutableCurrent); + if (result.isSuccess) { + NSNumber *oldWriteId = transaction.currentWriteId; + NSDictionary* serverValues = [FServerValues generateServerValues:self.serverClock]; + + id<FNode> newVal = [result.update nodeValue]; + id<FNode> newValResolved = [FServerValues resolveDeferredValueSnapshot:newVal withServerValues:serverValues]; + + transaction.currentOutputSnapshotRaw = newVal; + transaction.currentOutputSnapshotResolved = newValResolved; + + transaction.currentWriteId = [NSNumber numberWithInteger:[self nextWriteId]]; + // Mutates writeIdsToExclude in place + [writeIdsToExclude removeObject:oldWriteId]; + [events addObjectsFromArray:[self.serverSyncTree applyUserOverwriteAtPath:transaction.path + newData:transaction.currentOutputSnapshotResolved + writeId:[transaction.currentWriteId integerValue] + isVisible:transaction.applyLocally]]; + [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[oldWriteId integerValue] + revert:YES + persist:NO + clock:self.serverClock]]; + } else { + abortTransaction = YES; + // The user aborted the transaction. JS treats ths as a "nodata" abort, but it's not an error, so we don't send them an error. + [transaction setAbortStatus:nil reason:nil]; + [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue] + revert:YES + persist:NO + clock:self.serverClock]]; + } + } + } + + [self.eventRaiser raiseEvents:events]; + events = nil; + + if (abortTransaction) { + // Abort + transaction.status = FTransactionCompleted; + transaction.unwatcher(); + if (transaction.onComplete) { + FIRDatabaseReference * ref = [[FIRDatabaseReference alloc] initWithRepo:self path:transaction.path]; + FIndexedNode *lastInput = [FIndexedNode indexedNodeWithNode:transaction.currentInputSnapshot]; + FIRDataSnapshot * snap = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:lastInput]; + fbt_void_void cb = ^{ + // Unlike JS, no need to check for "nodata" because ObjC has abortError = nil + transaction.onComplete(transaction.abortError, NO, snap); + }; + [callbacks addObject:[cb copy]]; + } + } + } + + // Note: unlike current js client, we don't need to preserve priority. Users can set priority via FIRMutableData + + // Clean up completed transactions. + [self pruneCompletedTransactionsBelowNode:self.transactionQueueTree]; + + // Now fire callbacks, now that we're in a good, known state. + [self.eventRaiser raiseCallbacks:callbacks]; + + // Try to send the transaction result to the server + [self sendAllReadyTransactions]; +} + +- (FTree *) getAncestorTransactionNodeForPath:(FPath *)path { + FTree* transactionNode = self.transactionQueueTree; + + while (![path isEmpty] && [transactionNode getValue] == nil) { + NSString* front = [path getFront]; + transactionNode = [transactionNode subTree:[[FPath alloc] initWith:front]]; + path = [path popFront]; + } + + return transactionNode; +} + +- (NSMutableArray *) buildTransactionQueueAtNode:(FTree *)node { + NSMutableArray* queue = [[NSMutableArray alloc] init]; + [self aggregateTransactionQueuesForNode:node andQueue:queue]; + + [queue sortUsingComparator:^NSComparisonResult(FTupleTransaction* obj1, FTupleTransaction* obj2) { + return [obj1.order compare:obj2.order]; + }]; + + return queue; +} + +- (void) aggregateTransactionQueuesForNode:(FTree *)node andQueue:(NSMutableArray *)queue { + NSArray* nodeQueue = [node getValue]; + [queue addObjectsFromArray:nodeQueue]; + + [node forEachChild:^(FTree *child) { + [self aggregateTransactionQueuesForNode:child andQueue:queue]; + }]; +} + +/** + * Remove COMPLETED transactions at or below this node in the transactionQueueTree + */ +- (void) pruneCompletedTransactionsBelowNode:(FTree *)node { + NSMutableArray* queue = [node getValue]; + if (queue != nil) { + int i = 0; + // remove all of the completed transactions from the queue + while (i < queue.count) { + FTupleTransaction* transaction = [queue objectAtIndex:i]; + if (transaction.status == FTransactionCompleted) { + [queue removeObjectAtIndex:i]; + } else { + i++; + } + } + if (queue.count > 0) { + [node setValue:queue]; + } else { + [node setValue:nil]; + } + } + + [node forEachChildMutationSafe:^(FTree *child) { + [self pruneCompletedTransactionsBelowNode:child]; + }]; +} + +/** + * Aborts all transactions on ancestors or descendants of the specified path. Called when doing a setValue: or + * updateChildValues: since we consider them incompatible with transactions + * + * @param path path for which we want to abort related transactions. + */ +- (FPath *) abortTransactionsAtPath:(FPath *)path error:(NSString *)error { + // For the common case that there are no transactions going on, skip all this! + if ([self.transactionQueueTree isEmpty]) { + return path; + } else { + FPath* affectedPath = [self getAncestorTransactionNodeForPath:path].path; + + FTree* transactionNode = [self.transactionQueueTree subTree:path]; + [transactionNode forEachAncestor:^BOOL(FTree *ancestor) { + [self abortTransactionsAtNode:ancestor error:error]; + return NO; + }]; + + [self abortTransactionsAtNode:transactionNode error:error]; + + [transactionNode forEachDescendant:^(FTree *child) { + [self abortTransactionsAtNode:child error:error]; + }]; + + return affectedPath; + } +} + +/** + * Abort transactions stored in this transactions queue node. + * + * @param node Node to abort transactions for. + */ +- (void) abortTransactionsAtNode:(FTree *)node error:(NSString *)error { + NSMutableArray* queue = [node getValue]; + if (queue != nil) { + + // Queue up the callbacks and fire them after cleaning up all of our transaction state, since + // can be immediately aborted and removed. + NSMutableArray* callbacks = [[NSMutableArray alloc] init]; + + // Go through queue. Any already-sent transactions must be marked for abort, while the unsent ones + // can be immediately aborted and removed + NSMutableArray *events = [[NSMutableArray alloc] init]; + int lastSent = -1; + // Note: all of the sent transactions will be at the front of the queue, so safe to increment lastSent + for (FTupleTransaction* transaction in queue) { + if (transaction.status == FTransactionSentNeedsAbort) { + // No-op. already marked. + } else if (transaction.status == FTransactionSent) { + // Mark this transaction for abort when it returns + lastSent++; + transaction.status = FTransactionSentNeedsAbort; + [transaction setAbortStatus:error reason:nil]; + } else { + // we can abort this immediately + transaction.unwatcher(); + if ([error isEqualToString:kFTransactionSet]) { + [events addObjectsFromArray:[self.serverSyncTree ackUserWriteWithWriteId:[transaction.currentWriteId integerValue] + revert:YES + persist:NO + clock:self.serverClock]]; + } else { + // If it was cancelled it was already removed from the sync tree, no need to ack + NSAssert([error isEqualToString:kFErrorWriteCanceled], nil); + } + + if (transaction.onComplete) { + NSError* abortReason = [FUtilities errorForStatus:error andReason:nil]; + FIRDataSnapshot * snapshot = nil; + fbt_void_void cb = ^{ + transaction.onComplete(abortReason, NO, snapshot); + }; + [callbacks addObject:[cb copy]]; + } + } + } + if (lastSent == -1) { + // We're not waiting for any sent transactions. We can clear the queue. + [node setValue:nil]; + } else { + // Remove the transactions we aborted + NSRange theRange; + theRange.location = lastSent + 1; + theRange.length = queue.count - theRange.location; + [queue removeObjectsInRange:theRange]; + } + + // Now fire the callbacks + [self.eventRaiser raiseEvents:events]; + [self.eventRaiser raiseCallbacks:callbacks]; + } +} + +@end diff --git a/Firebase/Database/Core/FRepoInfo.h b/Firebase/Database/Core/FRepoInfo.h new file mode 100644 index 0000000..dace937 --- /dev/null +++ b/Firebase/Database/Core/FRepoInfo.h @@ -0,0 +1,34 @@ +/* + * 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 <Foundation/Foundation.h> + +@interface FRepoInfo : NSObject + +@property (nonatomic, readonly, strong) NSString* host; +@property (nonatomic, readonly, strong) NSString* namespace; +@property (nonatomic, strong) NSString* internalHost; +@property (nonatomic, readonly) bool secure; + +- (id) initWithHost:(NSString*)host isSecure:(bool)secure withNamespace:(NSString*)namespace; + +- (NSString *) connectionURLWithLastSessionID:(NSString*)lastSessionID; +- (NSString *) connectionURL; +- (void) clearInternalHostCache; +- (BOOL) isDemoHost; +- (BOOL) isCustomHost; + +@end diff --git a/Firebase/Database/Core/FRepoInfo.m b/Firebase/Database/Core/FRepoInfo.m new file mode 100644 index 0000000..6b15fe5 --- /dev/null +++ b/Firebase/Database/Core/FRepoInfo.m @@ -0,0 +1,115 @@ +/* + * 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 "FRepoInfo.h" +#import "FConstants.h" + +@interface FRepoInfo () + +@property (nonatomic, strong) NSString *domain; + +@end + + +@implementation FRepoInfo + +@synthesize namespace; +@synthesize host; +@synthesize internalHost; +@synthesize secure; +@synthesize domain; + +- (id) initWithHost:(NSString*)aHost isSecure:(bool)isSecure withNamespace:(NSString*)aNamespace { + self = [super init]; + if (self) { + host = aHost; + domain = [host substringFromIndex:[host rangeOfString:@"."].location+1]; + secure = isSecure; + namespace = aNamespace; + + // Get cached internal host if it exists + NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host]; + NSString* cachedInternalHost = [[NSUserDefaults standardUserDefaults] stringForKey:internalHostKey]; + if (cachedInternalHost != nil) { + internalHost = cachedInternalHost; + } else { + internalHost = self.host; + } + } + return self; +} + +- (NSString *)description { + // The namespace is encoded in the hostname, so we can just return this. + return [NSString stringWithFormat:@"http%@://%@", (self.secure ? @"s" : @""), self.host]; +} + +- (void) setInternalHost:(NSString *)newHost { + if (![internalHost isEqualToString:newHost]) { + internalHost = newHost; + + // Cache the internal host so we don't need to redirect later on + NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host]; + NSUserDefaults* cache = [NSUserDefaults standardUserDefaults]; + [cache setObject:internalHost forKey:internalHostKey]; + [cache synchronize]; + } +} + +- (void) clearInternalHostCache { + internalHost = self.host; + + // Remove the cached entry + NSString* internalHostKey = [NSString stringWithFormat:@"firebase:host:%@", self.host]; + NSUserDefaults* cache = [NSUserDefaults standardUserDefaults]; + [cache removeObjectForKey:internalHostKey]; + [cache synchronize]; +} + +- (BOOL) isDemoHost { + return [self.domain isEqualToString:@"firebaseio-demo.com"]; +} + +- (BOOL) isCustomHost { + return ![self.domain isEqualToString:@"firebaseio-demo.com"] && ![self.domain isEqualToString:@"firebaseio.com"]; +} + + +- (NSString *) connectionURL { + return [self connectionURLWithLastSessionID:nil]; +} + +- (NSString *) connectionURLWithLastSessionID:(NSString*)lastSessionID { + NSString *scheme; + if (self.secure) { + scheme = @"wss"; + } else { + scheme = @"ws"; + } + NSString *url = [NSString stringWithFormat:@"%@://%@/.ws?%@=%@&ns=%@", + scheme, + self.internalHost, + kWireProtocolVersionParam, + kWebsocketProtocolVersion, + self.namespace]; + + if (lastSessionID != nil) { + url = [NSString stringWithFormat:@"%@&ls=%@", url, lastSessionID]; + } + return url; +} + +@end diff --git a/Firebase/Database/Core/FRepoManager.h b/Firebase/Database/Core/FRepoManager.h new file mode 100644 index 0000000..c492861 --- /dev/null +++ b/Firebase/Database/Core/FRepoManager.h @@ -0,0 +1,32 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FRepoInfo.h" +#import "FRepo.h" +#import "FIRDatabaseConfig.h" + +@interface FRepoManager : NSObject + ++ (FRepo *) getRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config; ++ (FRepo *) createRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database; ++ (void) interruptAll; ++ (void) interrupt:(FIRDatabaseConfig *)config; ++ (void) resumeAll; ++ (void) resume:(FIRDatabaseConfig *)config; ++ (void) disposeRepos:(FIRDatabaseConfig *)config; + +@end diff --git a/Firebase/Database/Core/FRepoManager.m b/Firebase/Database/Core/FRepoManager.m new file mode 100644 index 0000000..6dccf7e --- /dev/null +++ b/Firebase/Database/Core/FRepoManager.m @@ -0,0 +1,131 @@ +/* + * 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 "FRepoManager.h" +#import "FRepo.h" +#import "FIRDatabaseQuery_Private.h" +#import "FAtomicNumber.h" +#import "FIRDatabaseConfig_Private.h" +#import "FIRDatabase_Private.h" + +@implementation FRepoManager + ++ (NSMutableDictionary *)configs { + static dispatch_once_t pred = 0; + static NSMutableDictionary *configs; + dispatch_once(&pred, ^{ + configs = [NSMutableDictionary dictionary]; + }); + return configs; +} + +/** + * Used for legacy unit tests. The public API should go through FirebaseDatabase which + * calls createRepo. + */ ++ (FRepo *) getRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config { + [config freeze]; + NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", repoInfo.host, repoInfo.namespace]; + NSMutableDictionary *configs = [FRepoManager configs]; + @synchronized(configs) { + NSMutableDictionary *repos = configs[config.sessionIdentifier]; + if (!repos || repos[repoHashString] == nil) { + // Calling this should create the repo. + [FIRDatabase createDatabaseForTests:repoInfo config:config]; + } + + return configs[config.sessionIdentifier][repoHashString]; + } +} + ++ (FRepo *) createRepo:(FRepoInfo *)repoInfo config:(FIRDatabaseConfig *)config database:(FIRDatabase *)database { + [config freeze]; + NSString* repoHashString = [NSString stringWithFormat:@"%@_%@", repoInfo.host, repoInfo.namespace]; + NSMutableDictionary *configs = [FRepoManager configs]; + @synchronized(configs) { + NSMutableDictionary *repos = configs[config.sessionIdentifier]; + if (!repos) { + repos = [NSMutableDictionary dictionary]; + configs[config.sessionIdentifier] = repos; + } + FRepo *repo = repos[repoHashString]; + if (repo == nil) { + repo = [[FRepo alloc] initWithRepoInfo:repoInfo config:config database:database]; + repos[repoHashString] = repo; + return repo; + } else { + [NSException raise:@"RepoExists" format:@"createRepo called for Repo that already exists."]; + return nil; + } + } +} + ++ (void) interrupt:(FIRDatabaseConfig *)config { + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + NSMutableDictionary *configs = [FRepoManager configs]; + NSMutableDictionary *repos = configs[config.sessionIdentifier]; + for (FRepo* repo in [repos allValues]) { + [repo interrupt]; + } + }); +} + ++ (void) interruptAll { + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + NSMutableDictionary *configs = [FRepoManager configs]; + for (NSMutableDictionary *repos in [configs allValues]) { + for (FRepo* repo in [repos allValues]) { + [repo interrupt]; + } + } + }); +} + ++ (void) resume:(FIRDatabaseConfig *)config { + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + NSMutableDictionary *configs = [FRepoManager configs]; + NSMutableDictionary *repos = configs[config.sessionIdentifier]; + for (FRepo* repo in [repos allValues]) { + [repo resume]; + } + }); +} + ++ (void) resumeAll { + dispatch_async([FIRDatabaseQuery sharedQueue], ^{ + NSMutableDictionary *configs = [FRepoManager configs]; + for (NSMutableDictionary *repos in [configs allValues]) { + for (FRepo* repo in [repos allValues]) { + [repo resume]; + } + } + }); +} + ++ (void)disposeRepos:(FIRDatabaseConfig *)config { + // Do this synchronously to make sure we release our references to LevelDB before returning, allowing LevelDB + // to close and release its exclusive locks. + dispatch_sync([FIRDatabaseQuery sharedQueue], ^{ + FFLog(@"I-RDB040001", @"Disposing all repos for Config with name %@", config.sessionIdentifier); + NSMutableDictionary *configs = [FRepoManager configs]; + for (FRepo* repo in [configs[config.sessionIdentifier] allValues]) { + [repo dispose]; + } + [configs removeObjectForKey:config.sessionIdentifier]; + }); +} + +@end diff --git a/Firebase/Database/Core/FRepo_Private.h b/Firebase/Database/Core/FRepo_Private.h new file mode 100644 index 0000000..109edac --- /dev/null +++ b/Firebase/Database/Core/FRepo_Private.h @@ -0,0 +1,42 @@ +/* + * 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 "FRepo.h" +#import "FSparseSnapshotTree.h" + +@class FSyncTree; +@class FAtomicNumber; +@class FEventRaiser; +@class FSnapshotHolder; + +@interface FRepo () + +- (void) runOnDisconnectEvents; + +@property (nonatomic, strong) FRepoInfo* repoInfo; +@property (nonatomic, strong) FPersistentConnection* connection; +@property (nonatomic, strong) FSnapshotHolder* infoData; +@property (nonatomic, strong) FSparseSnapshotTree* onDisconnect; +@property (nonatomic, strong) FEventRaiser *eventRaiser; +@property (nonatomic, strong) FSyncTree *serverSyncTree; + +// For testing. +@property (nonatomic) long dataUpdateCount; +@property (nonatomic) long rangeMergeUpdateCount; + +- (NSInteger)nextWriteId; + +@end diff --git a/Firebase/Database/Core/FServerValues.h b/Firebase/Database/Core/FServerValues.h new file mode 100644 index 0000000..2540c12 --- /dev/null +++ b/Firebase/Database/Core/FServerValues.h @@ -0,0 +1,30 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FSparseSnapshotTree.h" +#import "FNode.h" +#import "FCompoundWrite.h" +#import "FClock.h" + +@interface FServerValues : NSObject + ++ (NSDictionary*) generateServerValues:(id<FClock>)clock; ++ (id) resolveDeferredValueCompoundWrite:(FCompoundWrite*)write withServerValues:(NSDictionary*)serverValues; ++ (id<FNode>) resolveDeferredValueSnapshot:(id<FNode>)node withServerValues:(NSDictionary*)serverValues; ++ (id) resolveDeferredValueTree:(FSparseSnapshotTree*)tree withServerValues:(NSDictionary*)serverValues; + +@end diff --git a/Firebase/Database/Core/FServerValues.m b/Firebase/Database/Core/FServerValues.m new file mode 100644 index 0000000..89ee5d0 --- /dev/null +++ b/Firebase/Database/Core/FServerValues.m @@ -0,0 +1,93 @@ +/* + * 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 "FServerValues.h" +#import "FConstants.h" +#import "FLeafNode.h" +#import "FChildrenNode.h" +#import "FSnapshotUtilities.h" + +@implementation FServerValues + ++ (NSDictionary*) generateServerValues:(id<FClock>)clock { + long long millis = (long long)([clock currentTime] * 1000); + return @{ @"timestamp": [NSNumber numberWithLongLong:millis] }; +} + ++ (id) resolveDeferredValue:(id)val withServerValues:(NSDictionary*)serverValues { + if ([val isKindOfClass:[NSDictionary class]]) { + NSDictionary* dict = val; + if (dict[kServerValueSubKey] != nil) { + NSString* serverValueType = [dict objectForKey:kServerValueSubKey]; + if (serverValues[serverValueType] != nil) { + return [serverValues objectForKey:serverValueType]; + } else { + // TODO: Throw unrecognizedServerValue error here + } + } + } + return val; +} + ++ (FCompoundWrite *) resolveDeferredValueCompoundWrite:(FCompoundWrite *)write withServerValues:(NSDictionary *)serverValues { + __block FCompoundWrite *resolved = write; + [write enumerateWrites:^(FPath *path, id<FNode> node, BOOL *stop) { + id<FNode> resolvedNode = [FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues]; + // Node actually changed, use pointer inequality here + if (resolvedNode != node) { + resolved = [resolved addWrite:resolvedNode atPath:path]; + } + }]; + return resolved; +} + ++ (id) resolveDeferredValueTree:(FSparseSnapshotTree*)tree withServerValues:(NSDictionary*)serverValues { + FSparseSnapshotTree* resolvedTree = [[FSparseSnapshotTree alloc] init]; + [tree forEachTreeAtPath:[FPath empty] do:^(FPath* path, id<FNode> node) { + [resolvedTree rememberData:[FServerValues resolveDeferredValueSnapshot:node withServerValues:serverValues] onPath:path]; + }]; + return resolvedTree; +} + ++ (id<FNode>) resolveDeferredValueSnapshot:(id<FNode>)node withServerValues:(NSDictionary*)serverValues { + id priorityVal = [FServerValues resolveDeferredValue:[[node getPriority] val] withServerValues:serverValues]; + id<FNode> priority = [FSnapshotUtilities nodeFrom:priorityVal]; + + if ([node isLeafNode]) { + id value = [self resolveDeferredValue:[node val] withServerValues:serverValues]; + if (![value isEqual:[node val]] || ![priority isEqual:[node getPriority]]) { + return [[FLeafNode alloc] initWithValue:value withPriority:priority]; + } else { + return node; + } + } else { + __block FChildrenNode* newNode = node; + if (![priority isEqual:[node getPriority]]) { + newNode = [newNode updatePriority:priority]; + } + + [node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) { + id newChildNode = [FServerValues resolveDeferredValueSnapshot:childNode withServerValues:serverValues]; + if (![newChildNode isEqual:childNode]) { + newNode = [newNode updateImmediateChild:childKey withNewChild:newChildNode]; + } + }]; + return newNode; + } +} + +@end + diff --git a/Firebase/Database/Core/FSnapshotHolder.h b/Firebase/Database/Core/FSnapshotHolder.h new file mode 100644 index 0000000..9a1d871 --- /dev/null +++ b/Firebase/Database/Core/FSnapshotHolder.h @@ -0,0 +1,27 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FNode.h" + +@interface FSnapshotHolder : NSObject + +- (id<FNode>) getNode:(FPath *)path; +- (void) updateSnapshot:(FPath *)path withNewSnapshot:(id<FNode>)newSnapshotNode; + +@property (nonatomic, strong) id<FNode> rootNode; + +@end diff --git a/Firebase/Database/Core/FSnapshotHolder.m b/Firebase/Database/Core/FSnapshotHolder.m new file mode 100644 index 0000000..25c4625 --- /dev/null +++ b/Firebase/Database/Core/FSnapshotHolder.m @@ -0,0 +1,46 @@ +/* + * 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 "FSnapshotHolder.h" +#import "FEmptyNode.h" + +@interface FSnapshotHolder() + + +@end + +@implementation FSnapshotHolder + +@synthesize rootNode; + +- (id)init +{ + self = [super init]; + if (self) { + self.rootNode = [FEmptyNode emptyNode]; + } + return self; +} + +- (id<FNode>) getNode:(FPath *)path { + return [self.rootNode getChild:path]; +} + +- (void) updateSnapshot:(FPath *)path withNewSnapshot:(id<FNode>)newSnapshotNode { + self.rootNode = [self.rootNode updateChild:path withNewChild:newSnapshotNode]; +} + +@end diff --git a/Firebase/Database/Core/FSparseSnapshotTree.h b/Firebase/Database/Core/FSparseSnapshotTree.h new file mode 100644 index 0000000..b860c9d --- /dev/null +++ b/Firebase/Database/Core/FSparseSnapshotTree.h @@ -0,0 +1,34 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FNode.h" +#import "FPath.h" +#import "FTypedefs_Private.h" + +@class FSparseSnapshotTree; + +typedef void (^fbt_void_nsstring_sstree) (NSString*, FSparseSnapshotTree*); + +@interface FSparseSnapshotTree : NSObject + +- (id<FNode>) findPath:(FPath *)path; +- (void) rememberData:(id<FNode>)data onPath:(FPath *)path; +- (BOOL) forgetPath:(FPath *)path; +- (void) forEachTreeAtPath:(FPath *)prefixPath do:(fbt_void_path_node)func; +- (void) forEachChild:(fbt_void_nsstring_sstree)func; + +@end diff --git a/Firebase/Database/Core/FSparseSnapshotTree.m b/Firebase/Database/Core/FSparseSnapshotTree.m new file mode 100644 index 0000000..1f16888 --- /dev/null +++ b/Firebase/Database/Core/FSparseSnapshotTree.m @@ -0,0 +1,144 @@ +/* + * 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 "FSparseSnapshotTree.h" +#import "FChildrenNode.h" + +@interface FSparseSnapshotTree () { + id<FNode> value; + NSMutableDictionary* children; +} + +@end + +@implementation FSparseSnapshotTree + +- (id) init { + self = [super init]; + if (self) { + value = nil; + children = nil; + } + return self; +} + +- (id<FNode>) findPath:(FPath *)path { + if (value != nil) { + return [value getChild:path]; + } else if (![path isEmpty] && children != nil) { + NSString* childKey = [path getFront]; + path = [path popFront]; + FSparseSnapshotTree* childTree = children[childKey]; + if (childTree != nil) { + return [childTree findPath:path]; + } else { + return nil; + } + } else { + return nil; + } +} + +- (void) rememberData:(id<FNode>)data onPath:(FPath *)path { + if ([path isEmpty]) { + value = data; + children = nil; + } else if (value != nil) { + value = [value updateChild:path withNewChild:data]; + } else { + if (children == nil) { + children = [[NSMutableDictionary alloc] init]; + } + + NSString* childKey = [path getFront]; + if (children[childKey] == nil) { + children[childKey] = [[FSparseSnapshotTree alloc] init]; + } + + FSparseSnapshotTree* child = children[childKey]; + path = [path popFront]; + [child rememberData:data onPath:path]; + } +} + +- (BOOL) forgetPath:(FPath *)path { + if ([path isEmpty]) { + value = nil; + children = nil; + return YES; + } else { + if (value != nil) { + if ([value isLeafNode]) { + // non-empty path at leaf. the path leads to nowhere + return NO; + } else { + id<FNode> tmp = value; + value = nil; + + [tmp enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) { + [self rememberData:node onPath:[[FPath alloc] initWith:key]]; + }]; + + // we've cleared out the value and set children. Call ourself again to hit the next case + return [self forgetPath:path]; + } + } else if (children != nil) { + NSString* childKey = [path getFront]; + path = [path popFront]; + + if (children[childKey] != nil) { + FSparseSnapshotTree* child = children[childKey]; + BOOL safeToRemove = [child forgetPath:path]; + if (safeToRemove) { + [children removeObjectForKey:childKey]; + } + } + + if ([children count] == 0) { + children = nil; + return YES; + } else { + return NO; + } + } else { + return YES; + } + } +} + +- (void) forEachTreeAtPath:(FPath *)prefixPath do:(fbt_void_path_node)func { + if (value != nil) { + func(prefixPath, value); + } else { + [self forEachChild:^(NSString* key, FSparseSnapshotTree* tree) { + FPath* path = [prefixPath childFromString:key]; + [tree forEachTreeAtPath:path do:func]; + }]; + } +} + + +- (void) forEachChild:(fbt_void_nsstring_sstree)func { + if (children != nil) { + for (NSString* key in children) { + FSparseSnapshotTree* tree = [children objectForKey:key]; + func(key, tree); + } + } +} + + +@end diff --git a/Firebase/Database/Core/FSyncPoint.h b/Firebase/Database/Core/FSyncPoint.h new file mode 100644 index 0000000..4e5a4e2 --- /dev/null +++ b/Firebase/Database/Core/FSyncPoint.h @@ -0,0 +1,66 @@ +/* + * 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 <Foundation/Foundation.h> + +@protocol FOperation; +@class FWriteTreeRef; +@protocol FNode; +@protocol FEventRegistration; +@class FQuerySpec; +@class FChildrenNode; +@class FTupleRemovedQueriesEvents; +@class FView; +@class FPath; +@class FCacheNode; +@class FPersistenceManager; + +@interface FSyncPoint : NSObject + +- (id)initWithPersistenceManager:(FPersistenceManager *)persistence; + +- (BOOL) isEmpty; + +/** +* Returns array of FEvent +*/ +- (NSArray *) applyOperation:(id<FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id<FNode>)optCompleteServerCache; + +/** +* Returns array of FEvent +*/ +- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration + forNonExistingViewForQuery:(FQuerySpec *)query + writesCache:(FWriteTreeRef *)writesCache + serverCache:(FCacheNode *)serverCache; + +- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration + forExistingViewForQuery:(FQuerySpec *)query; + +- (FTupleRemovedQueriesEvents *) removeEventRegistration:(id <FEventRegistration>)eventRegistration + forQuery:(FQuerySpec *)query + cancelError:(NSError *)cancelError; +/** +* Returns array of FViews +*/ +- (NSArray *) queryViews; +- (id<FNode>) completeServerCacheAtPath:(FPath *)path; +- (FView *) viewForQuery:(FQuerySpec *)query; +- (BOOL) viewExistsForQuery:(FQuerySpec *)query; +- (BOOL) hasCompleteView; +- (FView *) completeView; + +@end diff --git a/Firebase/Database/Core/FSyncPoint.m b/Firebase/Database/Core/FSyncPoint.m new file mode 100644 index 0000000..cd429f1 --- /dev/null +++ b/Firebase/Database/Core/FSyncPoint.m @@ -0,0 +1,257 @@ +/* + * 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 "FSyncPoint.h" +#import "FOperation.h" +#import "FWriteTreeRef.h" +#import "FNode.h" +#import "FEventRegistration.h" +#import "FIRDatabaseQuery.h" +#import "FChildrenNode.h" +#import "FTupleRemovedQueriesEvents.h" +#import "FView.h" +#import "FOperationSource.h" +#import "FQuerySpec.h" +#import "FQueryParams.h" +#import "FPath.h" +#import "FEmptyNode.h" +#import "FViewCache.h" +#import "FCacheNode.h" +#import "FPersistenceManager.h" +#import "FDataEvent.h" + +/** +* SyncPoint represents a single location in a SyncTree with 1 or more event registrations, meaning we need to +* maintain 1 or more Views at this location to cache server data and raise appropriate events for server changes +* and user writes (set, transaction, update). +* +* It's responsible for: +* - Maintaining the set of 1 or more views necessary at this location (a SyncPoint with 0 views should be removed). +* - Proxying user / server operations to the views as appropriate (i.e. applyServerOverwrite, +* applyUserOverwrite, etc.) +*/ +@interface FSyncPoint () +/** +* The Views being tracked at this location in the tree, stored as a map where the key is a +* queryParams and the value is the View for that query. +* +* NOTE: This list will be quite small (usually 1, but perhaps 2 or 3; any more is an odd use case). +* +* Maps NSString -> FView +*/ +@property (nonatomic, strong) NSMutableDictionary *views; + +@property (nonatomic, strong) FPersistenceManager *persistenceManager; +@end + +@implementation FSyncPoint + +- (id) initWithPersistenceManager:(FPersistenceManager *)persistence { + self = [super init]; + if (self) { + self.persistenceManager = persistence; + self.views = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (BOOL) isEmpty { + return [self.views count] == 0; +} + +- (NSArray *) applyOperation:(id<FOperation>)operation + toView:(FView *)view + writesCache:(FWriteTreeRef *)writesCache + serverCache:(id<FNode>)optCompleteServerCache { + FViewOperationResult *result = [view applyOperation:operation writesCache:writesCache serverCache:optCompleteServerCache]; + if (!view.query.loadsAllData) { + NSMutableSet *removed = [NSMutableSet set]; + NSMutableSet *added = [NSMutableSet set]; + [result.changes enumerateObjectsUsingBlock:^(FChange *change, NSUInteger idx, BOOL *stop) { + if (change.type == FIRDataEventTypeChildAdded) { + [added addObject:change.childKey]; + } else if (change.type == FIRDataEventTypeChildRemoved) { + [removed addObject:change.childKey]; + } + }]; + if ([removed count] > 0 || [added count] > 0) { + [self.persistenceManager updateTrackedQueryKeysWithAddedKeys:added removedKeys:removed forQuery:view.query]; + } + } + return result.events; +} + +- (NSArray *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache { + FQueryParams *queryParams = operation.source.queryParams; + if (queryParams != nil) { + FView *view = [self.views objectForKey:queryParams]; + NSAssert(view != nil, @"SyncTree gave us an op for an invalid query."); + return [self applyOperation:operation toView:view writesCache:writesCache serverCache:optCompleteServerCache]; + } else { + NSMutableArray *events = [[NSMutableArray alloc] init]; + [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) { + NSArray *eventsForView = [self applyOperation:operation toView:view writesCache:writesCache serverCache:optCompleteServerCache]; + [events addObjectsFromArray:eventsForView]; + }]; + return events; + } +} + +/** +* Add an event callback for the specified query +* Returns Array of FEvent events to raise. +*/ +- (NSArray *) addEventRegistration:(id <FEventRegistration>)eventRegistration + forNonExistingViewForQuery:(FQuerySpec *)query + writesCache:(FWriteTreeRef *)writesCache + serverCache:(FCacheNode *)serverCache { + NSAssert(self.views[query.params] == nil, @"Found view for query: %@", query.params); + // TODO: make writesCache take flag for complete server node + id<FNode> eventCache = [writesCache calculateCompleteEventCacheWithCompleteServerCache:serverCache.isFullyInitialized ? serverCache.node : nil]; + BOOL eventCacheComplete; + if (eventCache != nil) { + eventCacheComplete = YES; + } else { + eventCache = [writesCache calculateCompleteEventChildrenWithCompleteServerChildren:serverCache.node]; + eventCacheComplete = NO; + } + + FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:eventCache index:query.index]; + FCacheNode *eventCacheNode = [[FCacheNode alloc] initWithIndexedNode:indexed + isFullyInitialized:eventCacheComplete + isFiltered:NO]; + FViewCache *viewCache = [[FViewCache alloc] initWithEventCache:eventCacheNode serverCache:serverCache]; + FView *view = [[FView alloc] initWithQuery:query initialViewCache:viewCache]; + // If this is a non-default query we need to tell persistence our current view of the data + if (!query.loadsAllData) { + NSMutableSet *allKeys = [NSMutableSet set]; + [view.eventCache enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) { + [allKeys addObject:key]; + }]; + [self.persistenceManager setTrackedQueryKeys:allKeys forQuery:query]; + } + self.views[query.params] = view; + return [self addEventRegistration:eventRegistration forExistingViewForQuery:query]; +} + +- (NSArray *)addEventRegistration:(id<FEventRegistration>)eventRegistration + forExistingViewForQuery:(FQuerySpec *)query { + FView *view = self.views[query.params]; + NSAssert(view != nil, @"No view for query: %@", query); + [view addEventRegistration:eventRegistration]; + return [view initialEvents:eventRegistration]; +} + +/** +* Remove event callback(s). Return cancelEvents if a cancelError is specified. +* +* If query is the default query, we'll check all views for the specified eventRegistration. +* If eventRegistration is nil, we'll remove all callbacks for the specified view(s). +* +* @return FTupleRemovedQueriesEvents removed queries and any cancel events +*/ +- (FTupleRemovedQueriesEvents *) removeEventRegistration:(id <FEventRegistration>)eventRegistration + forQuery:(FQuerySpec *)query + cancelError:(NSError *)cancelError { + NSMutableArray *removedQueries = [[NSMutableArray alloc] init]; + __block NSMutableArray *cancelEvents = [[NSMutableArray alloc] init]; + BOOL hadCompleteView = [self hasCompleteView]; + if ([query isDefault]) { + // When you do [ref removeObserverWithHandle:], we search all views for the registration to remove. + [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *viewQueryParams, FView *view, BOOL *stop) { + [cancelEvents addObjectsFromArray:[view removeEventRegistration:eventRegistration cancelError:cancelError]]; + if ([view isEmpty]) { + [self.views removeObjectForKey:viewQueryParams]; + + // We'll deal with complete views later + if (![view.query loadsAllData]) { + [removedQueries addObject:view.query]; + } + } + }]; + } else { + // remove the callback from the specific view + FView *view = [self.views objectForKey:query.params]; + if (view != nil) { + [cancelEvents addObjectsFromArray:[view removeEventRegistration:eventRegistration cancelError:cancelError]]; + + if ([view isEmpty]) { + [self.views removeObjectForKey:query.params]; + + // We'll deal with complete views later + if (![view.query loadsAllData]) { + [removedQueries addObject:view.query]; + } + } + } + } + + if (hadCompleteView && ![self hasCompleteView]) { + // We removed our last complete view + [removedQueries addObject:[FQuerySpec defaultQueryAtPath:query.path]]; + } + + return [[FTupleRemovedQueriesEvents alloc] initWithRemovedQueries:removedQueries cancelEvents:cancelEvents]; +} + +- (NSArray *) queryViews { + __block NSMutableArray *filteredViews = [[NSMutableArray alloc] init]; + + [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) { + if (![view.query loadsAllData]) { + [filteredViews addObject:view]; + } + }]; + + return filteredViews; +} + +- (id <FNode>) completeServerCacheAtPath:(FPath *)path { + __block id<FNode> serverCache = nil; + [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) { + serverCache = [view completeServerCacheFor:path]; + *stop = (serverCache != nil); + }]; + return serverCache; +} + +- (FView *) viewForQuery:(FQuerySpec *)query { + return [self.views objectForKey:query.params]; +} + +- (BOOL) viewExistsForQuery:(FQuerySpec *)query { + return [self viewForQuery:query] != nil; +} + +- (BOOL) hasCompleteView { + return [self completeView] != nil; +} + +- (FView *) completeView { + __block FView *completeView = nil; + + [self.views enumerateKeysAndObjectsUsingBlock:^(FQueryParams *key, FView *view, BOOL *stop) { + if ([view.query loadsAllData]) { + completeView = view; + *stop = YES; + } + }]; + + return completeView; +} + + +@end diff --git a/Firebase/Database/Core/FSyncTree.h b/Firebase/Database/Core/FSyncTree.h new file mode 100644 index 0000000..887f721 --- /dev/null +++ b/Firebase/Database/Core/FSyncTree.h @@ -0,0 +1,61 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FListenProvider; +@protocol FNode; +@class FPath; +@protocol FEventRegistration; +@protocol FPersistedServerCache; +@class FQuerySpec; +@class FCompoundWrite; +@class FPersistenceManager; +@class FCompoundHash; +@protocol FClock; + +@protocol FSyncTreeHash <NSObject> + +- (NSString *)simpleHash; +- (FCompoundHash *)compoundHash; +- (BOOL)includeCompoundHash; + +@end + +@interface FSyncTree : NSObject + +- (id) initWithListenProvider:(FListenProvider *)provider; +- (id) initWithPersistenceManager:(FPersistenceManager *)persistenceManager + listenProvider:(FListenProvider *)provider; + +// These methods all return NSArray of FEvent +- (NSArray *) applyUserOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible; +- (NSArray *) applyUserMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId; +- (NSArray *) ackUserWriteWithWriteId:(NSInteger)writeId revert:(BOOL)revert persist:(BOOL)persist clock:(id<FClock>)clock; +- (NSArray *) applyServerOverwriteAtPath:(FPath *)path newData:(id<FNode>)newData; +- (NSArray *) applyServerMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren; +- (NSArray *) applyServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges; +- (NSArray *) applyTaggedQueryOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData tagId:(NSNumber *)tagId; +- (NSArray *) applyTaggedQueryMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren tagId:(NSNumber *)tagId; +- (NSArray *) applyTaggedServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges tagId:(NSNumber *)tagId; +- (NSArray *) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query; +- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query cancelError:(NSError *)cancelError; +- (void)keepQuery:(FQuerySpec *)query synced:(BOOL)keepSynced; +- (NSArray *) removeAllWrites; + +- (id<FNode>) calcCompleteEventCacheAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude; + +@end diff --git a/Firebase/Database/Core/FSyncTree.m b/Firebase/Database/Core/FSyncTree.m new file mode 100644 index 0000000..37100c1 --- /dev/null +++ b/Firebase/Database/Core/FSyncTree.m @@ -0,0 +1,817 @@ +/* + * 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 "FSyncTree.h" +#import "FListenProvider.h" +#import "FWriteTree.h" +#import "FNode.h" +#import "FPath.h" +#import "FEventRegistration.h" +#import "FImmutableTree.h" +#import "FOperation.h" +#import "FWriteTreeRef.h" +#import "FOverwrite.h" +#import "FOperationSource.h" +#import "FMerge.h" +#import "FAckUserWrite.h" +#import "FView.h" +#import "FSyncPoint.h" +#import "FEmptyNode.h" +#import "FQueryParams.h" +#import "FQuerySpec.h" +#import "FSnapshotHolder.h" +#import "FChildrenNode.h" +#import "FTupleRemovedQueriesEvents.h" +#import "FAtomicNumber.h" +#import "FEventRaiser.h" +#import "FListenComplete.h" +#import "FSnapshotUtilities.h" +#import "FCacheNode.h" +#import "FUtilities.h" +#import "FCompoundWrite.h" +#import "FWriteRecord.h" +#import "FPersistenceManager.h" +#import "FKeepSyncedEventRegistration.h" +#import "FServerValues.h" +#import "FCompoundHash.h" +#import "FRangeMerge.h" + +// Size after which we start including the compound hash +static const NSUInteger kFSizeThresholdForCompoundHash = 1024; + +@interface FListenContainer : NSObject<FSyncTreeHash> + +@property (nonatomic, strong) FView *view; +@property (nonatomic, copy) fbt_nsarray_nsstring onComplete; + +@end + +@implementation FListenContainer + +- (instancetype)initWithView:(FView *)view onComplete:(fbt_nsarray_nsstring)onComplete { + self = [super init]; + if (self != nil) { + self->_view = view; + self->_onComplete = onComplete; + } + return self; +} + +- (id<FNode>)serverCache { + return self.view.serverCache; +} + +- (FCompoundHash *)compoundHash { + return [FCompoundHash fromNode:[self serverCache]]; +} + +- (NSString *)simpleHash { + return [[self serverCache] dataHash]; +} + +- (BOOL)includeCompoundHash { + return [FSnapshotUtilities estimateSerializedNodeSize:[self serverCache]] > kFSizeThresholdForCompoundHash; +} + +@end + +@interface FSyncTree () + +/** +* Tree of SyncPoints. There's a SyncPoint at any location that has 1 or more views. +*/ +@property (nonatomic, strong) FImmutableTree *syncPointTree; + +/** +* A tree of all pending user writes (user-initiated set, transactions, updates, etc) +*/ +@property (nonatomic, strong) FWriteTree *pendingWriteTree; + +/** +* Maps tagId -> FTuplePathQueryParams +*/ +@property (nonatomic, strong) NSMutableDictionary *tagToQueryMap; +@property (nonatomic, strong) NSMutableDictionary *queryToTagMap; +@property (nonatomic, strong) FListenProvider *listenProvider; +@property (nonatomic, strong) FPersistenceManager *persistenceManager; +@property (nonatomic, strong) FAtomicNumber *queryTagCounter; +@property (nonatomic, strong) NSMutableSet *keepSyncedQueries; + +@end + +/** +* SyncTree is the central class for managing event callback registration, data caching, views +* (query processing), and event generation. There are typically two SyncTree instances for +* each Repo, one for the normal Firebase data, and one for the .info data. +* +* It has a number of responsibilities, including: +* - Tracking all user event callbacks (registered via addEventRegistration: and removeEventRegistration:). +* - Applying and caching data changes for user setValue:, runTransactionBlock:, and updateChildValues: calls +* (applyUserOverwriteAtPath:, applyUserMergeAtPath:). +* - Applying and caching data changes for server data changes (applyServerOverwriteAtPath:, +* applyServerMergeAtPath:). +* - Generating user-facing events for server and user changes (all of the apply* methods +* return the set of events that need to be raised as a result). +* - Maintaining the appropriate set of server listens to ensure we are always subscribed +* to the correct set of paths and queries to satisfy the current set of user event +* callbacks (listens are started/stopped using the provided listenProvider). +* +* NOTE: Although SyncTree tracks event callbacks and calculates events to raise, the actual +* events are returned to the caller rather than raised synchronously. +*/ +@implementation FSyncTree + +- (id) initWithListenProvider:(FListenProvider *)provider { + return [self initWithPersistenceManager:nil listenProvider:provider]; +} + +- (id) initWithPersistenceManager:(FPersistenceManager *)persistenceManager listenProvider:(FListenProvider *)provider { + self = [super init]; + if (self) { + self.syncPointTree = [FImmutableTree empty]; + self.pendingWriteTree = [[FWriteTree alloc] init]; + self.tagToQueryMap = [[NSMutableDictionary alloc] init]; + self.queryToTagMap = [[NSMutableDictionary alloc] init]; + self.listenProvider = provider; + self.persistenceManager = persistenceManager; + self.queryTagCounter = [[FAtomicNumber alloc] init]; + self.keepSyncedQueries = [NSMutableSet set]; + } + return self; +} + +#pragma mark - +#pragma mark Apply Operations + +/** +* Apply data changes for a user-generated setValue: runTransactionBlock: updateChildValues:, etc. +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) applyUserOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible { + // Record pending write + [self.pendingWriteTree addOverwriteAtPath:path newData:newData writeId:writeId isVisible:visible]; + if (!visible) { + return @[]; + } else { + FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource userInstance] path:path snap:newData]; + return [self applyOperationToSyncPoints:operation]; + } +} + +/** +* Apply the data from a user-generated updateChildValues: call +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) applyUserMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId { + // Record pending merge + [self.pendingWriteTree addMergeAtPath:path changedChildren:changedChildren writeId:writeId]; + + FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource userInstance] path:path children:changedChildren]; + return [self applyOperationToSyncPoints:operation]; +} + +/** + * Acknowledge a pending user write that was previously registered with applyUserOverwriteAtPath: or applyUserMergeAtPath: + * TODO[offline]: Taking a serverClock here is awkward, but server values are awkward. :-( + * @return NSArray of FEvent to raise. + */ +- (NSArray *) ackUserWriteWithWriteId:(NSInteger)writeId revert:(BOOL)revert persist:(BOOL)persist clock:(id<FClock>)clock { + FWriteRecord *write = [self.pendingWriteTree writeForId:writeId]; + BOOL needToReevaluate = [self.pendingWriteTree removeWriteId:writeId]; + if (write.visible) { + if (persist) { + [self.persistenceManager removeUserWrite:writeId]; + } + if (!revert) { + NSDictionary *serverValues = [FServerValues generateServerValues:clock]; + if ([write isOverwrite]) { + id<FNode> resolvedNode = [FServerValues resolveDeferredValueSnapshot:write.overwrite withServerValues:serverValues]; + [self.persistenceManager applyUserWrite:resolvedNode toServerCacheAtPath:write.path]; + } else { + FCompoundWrite *resolvedMerge = [FServerValues resolveDeferredValueCompoundWrite:write.merge withServerValues:serverValues]; + [self.persistenceManager applyUserMerge:resolvedMerge toServerCacheAtPath:write.path]; + } + } + } + if (!needToReevaluate) { + return @[]; + } else { + __block FImmutableTree *affectedTree = [FImmutableTree empty]; + if (write.isOverwrite) { + affectedTree = [affectedTree setValue:@YES atPath:[FPath empty]]; + } else { + [write.merge enumerateWrites:^(FPath *path, id <FNode> node, BOOL *stop) { + affectedTree = [affectedTree setValue:@YES atPath:path]; + }]; + } + FAckUserWrite *operation = [[FAckUserWrite alloc] initWithPath:write.path affectedTree:affectedTree revert:revert]; + return [self applyOperationToSyncPoints:operation]; + } +} + +/** +* Apply new server data for the specified path +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) applyServerOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData { + [self.persistenceManager updateServerCacheWithNode:newData forQuery:[FQuerySpec defaultQueryAtPath:path]]; + FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource serverInstance] path:path snap:newData]; + return [self applyOperationToSyncPoints:operation]; +} + +/** +* Applied new server data to be merged in at the specified path +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) applyServerMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren { + [self.persistenceManager updateServerCacheWithMerge:changedChildren atPath:path]; + FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource serverInstance] path:path children:changedChildren]; + return [self applyOperationToSyncPoints:operation]; +} + +- (NSArray *) applyServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges { + FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path]; + if (syncPoint == nil) { + // Removed view, so it's safe to just ignore this update + return @[]; + } else { + // This could be for any "complete" (unfiltered) view, and if there is more than one complete view, they should + // each have the same cache so it doesn't matter which one we use. + FView *view = [syncPoint completeView]; + if (view != nil) { + id<FNode> serverNode = [view serverCache]; + for (FRangeMerge *merge in ranges) { + serverNode = [merge applyToNode:serverNode]; + } + return [self applyServerOverwriteAtPath:path newData:serverNode]; + } else { + // There doesn't exist a view for this update, so it was removed and it's safe to just ignore this range + // merge + return @[]; + } + } +} + +/** +* Apply a listen complete to a path +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) applyListenCompleteAtPath:(FPath *)path { + [self.persistenceManager setQueryComplete:[FQuerySpec defaultQueryAtPath:path]]; + id<FOperation> operation = [[FListenComplete alloc] initWithSource:[FOperationSource serverInstance] path:path]; + return [self applyOperationToSyncPoints:operation]; +} + +/** +* Apply a listen complete to a path +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) applyTaggedListenCompleteAtPath:(FPath *)path tagId:(NSNumber *)tagId { + FQuerySpec *query = [self queryForTag:tagId]; + if (query != nil) { + [self.persistenceManager setQueryComplete:query]; + FPath *relativePath = [FPath relativePathFrom:query.path to:path]; + id<FOperation> op = [[FListenComplete alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params] + path:relativePath]; + return [self applyTaggedOperation:op atPath:query.path]; + } else { + // We've already removed the query. No big deal, ignore the update. + return @[]; + } +} + +/** +* Internal helper method to apply tagged operation +*/ +- (NSArray *) applyTaggedOperation:(id<FOperation>)operation atPath:(FPath *)path { + FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path]; + NSAssert(syncPoint != nil, @"Missing sync point for query tag that we're tracking."); + FWriteTreeRef *writesCache = [self.pendingWriteTree childWritesForPath:path]; + return [syncPoint applyOperation:operation writesCache:writesCache serverCache:nil]; +} + +/** +* Apply new server data for the specified tagged query +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) applyTaggedQueryOverwriteAtPath:(FPath *)path newData:(id <FNode>)newData tagId:(NSNumber *)tagId { + FQuerySpec *query = [self queryForTag:tagId]; + if (query != nil) { + FPath *relativePath = [FPath relativePathFrom:query.path to:path]; + FQuerySpec *queryToOverwrite = relativePath.isEmpty ? query : [FQuerySpec defaultQueryAtPath:path]; + [self.persistenceManager updateServerCacheWithNode:newData forQuery:queryToOverwrite]; + FOverwrite *operation = [[FOverwrite alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params] + path:relativePath snap:newData]; + return [self applyTaggedOperation:operation atPath:query.path]; + } else { + // Query must have been removed already + return @[]; + } +} + +/** +* Apply server data to be merged in for the specified tagged query +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) applyTaggedQueryMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren tagId:(NSNumber *)tagId { + FQuerySpec *query = [self queryForTag:tagId]; + if (query != nil) { + FPath *relativePath = [FPath relativePathFrom:query.path to:path]; + [self.persistenceManager updateServerCacheWithMerge:changedChildren atPath:path]; + FMerge *operation = [[FMerge alloc] initWithSource:[FOperationSource forServerTaggedQuery:query.params] + path:relativePath + children:changedChildren]; + return [self applyTaggedOperation:operation atPath:query.path]; + } else { + // We've already removed the query. No big deal, ignore the update. + return @[]; + } +} + +- (NSArray *) applyTaggedServerRangeMergeAtPath:(FPath *)path updates:(NSArray *)ranges tagId:(NSNumber *)tagId { + FQuerySpec *query = [self queryForTag:tagId]; + if (query != nil) { + NSAssert([path isEqual:query.path], @"Tagged update path and query path must match"); + FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path]; + NSAssert(syncPoint != nil, @"Missing sync point for query tag that we're tracking."); + FView *view = [syncPoint viewForQuery:query]; + NSAssert(view != nil, @"Missing view for query tag that we're tracking"); + id<FNode> serverNode = [view serverCache]; + for (FRangeMerge *merge in ranges) { + serverNode = [merge applyToNode:serverNode]; + } + return [self applyTaggedQueryOverwriteAtPath:path newData:serverNode tagId:tagId]; + } else { + // We've already removed the query. No big deal, ignore the update. + return @[]; + } +} + +/** +* Add an event callback for the specified query +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) addEventRegistration:(id<FEventRegistration>)eventRegistration forQuery:(FQuerySpec *)query { + FPath *path = query.path; + + __block BOOL foundAncestorDefaultView = NO; + [self.syncPointTree forEachOnPath:query.path whileBlock:^BOOL(FPath *pathToSyncPoint, FSyncPoint *syncPoint) { + foundAncestorDefaultView = foundAncestorDefaultView || [syncPoint hasCompleteView]; + return !foundAncestorDefaultView; + }]; + + [self.persistenceManager setQueryActive:query]; + + FSyncPoint *syncPoint = [self.syncPointTree valueAtPath:path]; + if (syncPoint == nil) { + syncPoint = [[FSyncPoint alloc] initWithPersistenceManager:self.persistenceManager]; + self.syncPointTree = [self.syncPointTree setValue:syncPoint atPath:path]; + } + + BOOL viewAlreadyExists = [syncPoint viewExistsForQuery:query]; + NSArray *events; + if (viewAlreadyExists) { + events = [syncPoint addEventRegistration:eventRegistration forExistingViewForQuery:query]; + } else { + if (![query loadsAllData]) { + // We need to track a tag for this query + NSAssert(self.queryToTagMap[query] == nil, @"View does not exist, but we have a tag"); + NSNumber *tagId = [self.queryTagCounter getAndIncrement]; + self.queryToTagMap[query] = tagId; + self.tagToQueryMap[tagId] = query; + } + + FWriteTreeRef *writesCache = [self.pendingWriteTree childWritesForPath:path]; + FCacheNode *serverCache = [self serverCacheForQuery:query]; + events = [syncPoint addEventRegistration:eventRegistration + forNonExistingViewForQuery:query + writesCache:writesCache + serverCache:serverCache]; + + // There was no view and no default listen + if (!foundAncestorDefaultView) { + FView *view = [syncPoint viewForQuery:query]; + NSMutableArray *mutableEvents = [events mutableCopy]; + [mutableEvents addObjectsFromArray:[self setupListenerOnQuery:query view:view]]; + events = mutableEvents; + } + } + + return events; +} + +- (FCacheNode *)serverCacheForQuery:(FQuerySpec *)query { + __block id<FNode> serverCacheNode = nil; + + [self.syncPointTree forEachOnPath:query.path whileBlock:^BOOL(FPath *pathToSyncPoint, FSyncPoint *syncPoint) { + FPath *relativePath = [FPath relativePathFrom:pathToSyncPoint to:query.path]; + serverCacheNode = [syncPoint completeServerCacheAtPath:relativePath]; + return serverCacheNode == nil; + }]; + + FCacheNode *serverCache; + if (serverCacheNode != nil) { + FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:serverCacheNode index:query.index]; + serverCache = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:YES isFiltered:NO]; + } else { + FCacheNode *persistenceServerCache = [self.persistenceManager serverCacheForQuery:query]; + if (persistenceServerCache.isFullyInitialized) { + serverCache = persistenceServerCache; + } else { + serverCacheNode = [FEmptyNode emptyNode]; + + FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:query.path]; + [subtree forEachChild:^(NSString *childKey, FSyncPoint *childSyncPoint) { + id<FNode> completeCache = [childSyncPoint completeServerCacheAtPath:[FPath empty]]; + if (completeCache) { + serverCacheNode = [serverCacheNode updateImmediateChild:childKey withNewChild:completeCache]; + } + }]; + // Fill the node with any available children we have + [persistenceServerCache.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) { + if (![serverCacheNode hasChild:key]) { + serverCacheNode = [serverCacheNode updateImmediateChild:key withNewChild:node]; + } + }]; + FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:serverCacheNode index:query.index]; + serverCache = [[FCacheNode alloc] initWithIndexedNode:indexed isFullyInitialized:NO isFiltered:NO]; + } + } + + return serverCache; +} + +/** +* Remove event callback(s). +* +* If query is the default query, we'll check all queries for the specified eventRegistration. +* If eventRegistration is null, we'll remove all callbacks for the specified query/queries. +* +* @param eventRegistration if nil, all callbacks are removed +* @param cancelError If provided, appropriate cancel events will be returned +* @return NSArray of FEvent to raise. +*/ +- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration + forQuery:(FQuerySpec *)query + cancelError:(NSError *)cancelError { + // Find the syncPoint first. Then deal with whether or not it has matching listeners + FPath *path = query.path; + FSyncPoint *maybeSyncPoint = [self.syncPointTree valueAtPath:path]; + NSArray *cancelEvents = @[]; + + // A removal on a default query affects all queries at that location. A removal on an indexed query, even one without + // other query constraints, does *not* affect all queries at that location. So this check must be for 'default', and + // not loadsAllData: + if (maybeSyncPoint && ([query isDefault] || [maybeSyncPoint viewExistsForQuery:query])) { + FTupleRemovedQueriesEvents *removedAndEvents = [maybeSyncPoint removeEventRegistration:eventRegistration forQuery:query cancelError:cancelError]; + if ([maybeSyncPoint isEmpty]) { + self.syncPointTree = [self.syncPointTree removeValueAtPath:path]; + } + NSArray *removed = removedAndEvents.removedQueries; + cancelEvents = removedAndEvents.cancelEvents; + + // We may have just removed one of many listeners and can short-circuit this whole process + // We may also not have removed a default listener, in which case all of the descendant listeners should already + // be properly set up. + // + // Since indexed queries can shadow if they don't have other query constraints, check for loadsAllData: instead + // of isDefault: + NSUInteger defaultQueryIndex = [removed indexOfObjectPassingTest:^BOOL(FQuerySpec *q, NSUInteger idx, BOOL *stop) { + return [q loadsAllData]; + }]; + BOOL removingDefault = defaultQueryIndex != NSNotFound; + [removed enumerateObjectsUsingBlock:^(FQuerySpec *query, NSUInteger idx, BOOL *stop) { + [self.persistenceManager setQueryInactive:query]; + }]; + NSNumber *covered = [self.syncPointTree findOnPath:path andApplyBlock:^id(FPath *relativePath, FSyncPoint *parentSyncPoint) { + return [NSNumber numberWithBool:[parentSyncPoint hasCompleteView]]; + }]; + + if (removingDefault && ![covered boolValue]) { + FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:path]; + // There are potentially child listeners. Determine what if any listens we need to send before executing + // the removal + if (![subtree isEmpty]) { + // We need to fold over our subtree and collect the listeners to send + NSArray *newViews = [self collectDistinctViewsForSubTree:subtree]; + + // Ok, we've collected all the listens we need. Set them up. + [newViews enumerateObjectsUsingBlock:^(FView *view, NSUInteger idx, BOOL *stop) { + FQuerySpec *newQuery = view.query; + FListenContainer *listenContainer = [self createListenerForView:view]; + self.listenProvider.startListening([self queryForListening:newQuery], [self tagForQuery:newQuery], + listenContainer, listenContainer.onComplete); + }]; + } else { + // There's nothing below us, so nothing we need to start listening on + } + } + + // If we removed anything and we're not covered by a higher up listen, we need to stop listening on this query. + // The above block has us covered in terms of making sure we're set up on listens lower in the tree. + // Also, note that if we have a cancelError, it's already been removed at the provider level. + if (![covered boolValue] && [removed count] > 0 && cancelError == nil) { + // If we removed a default, then we weren't listening on any of the other queries here. Just cancel the one + // default. Otherwise, we need to iterate through and cancel each individual query + if (removingDefault) { + // We don't tag default listeners + self.listenProvider.stopListening([self queryForListening:query], nil); + } else { + [removed enumerateObjectsUsingBlock:^(FQuerySpec *queryToRemove, NSUInteger idx, BOOL *stop) { + NSNumber *tagToRemove = [self.queryToTagMap objectForKey:queryToRemove]; + self.listenProvider.stopListening([self queryForListening:queryToRemove], tagToRemove); + }]; + } + } + // Now, clear all the tags we're tracking for the removed listens. + [self removeTags:removed]; + } else { + // No-op, this listener must've been already removed + } + return cancelEvents; +} + +- (void)keepQuery:(FQuerySpec *)query synced:(BOOL)keepSynced { + // Only do something if we actually need to add/remove an event registration + if (keepSynced && ![self.keepSyncedQueries containsObject:query]) { + [self addEventRegistration:[FKeepSyncedEventRegistration instance] forQuery:query]; + [self.keepSyncedQueries addObject:query]; + } else if (!keepSynced && [self.keepSyncedQueries containsObject:query]) { + [self removeEventRegistration:[FKeepSyncedEventRegistration instance] forQuery:query cancelError:nil]; + [self.keepSyncedQueries removeObject:query]; + } +} + +- (NSArray *) removeAllWrites { + [self.persistenceManager removeAllUserWrites]; + NSArray *removedWrites = [self.pendingWriteTree removeAllWrites]; + if (removedWrites.count > 0) { + FImmutableTree *affectedTree = [[FImmutableTree empty] setValue:@YES atPath:[FPath empty]]; + return [self applyOperationToSyncPoints:[[FAckUserWrite alloc] initWithPath:[FPath empty] + affectedTree:affectedTree revert:YES]]; + } else { + return @[]; + } +} + +/** +* Returns a complete cache, if we have one, of the data at a particular path. The location must have a listener above +* it, but as this is only used by transaction code, that should always be the case anyways. +* +* Note: this method will *include* hidden writes from transaction with applyLocally set to false. +* @param path The path to the data we want +* @param writeIdsToExclude A specific set to be excluded +*/ +- (id <FNode>) calcCompleteEventCacheAtPath:(FPath *)path excludeWriteIds:(NSArray *)writeIdsToExclude { + BOOL includeHiddenSets = YES; + FWriteTree *writeTree = self.pendingWriteTree; + id<FNode> serverCache = [self.syncPointTree findOnPath:path andApplyBlock:^id<FNode>(FPath *pathSoFar, FSyncPoint *syncPoint) { + FPath *relativePath = [FPath relativePathFrom:pathSoFar to:path]; + id<FNode> serverCache = [syncPoint completeServerCacheAtPath:relativePath]; + if (serverCache) { + return serverCache; + } else { + return nil; + } + }]; + return [writeTree calculateCompleteEventCacheAtPath:path completeServerCache:serverCache excludeWriteIds:writeIdsToExclude includeHiddenWrites:includeHiddenSets]; +} + +#pragma mark - +#pragma mark Private Methods +/** +* This collapses multiple unfiltered views into a single view, since we only need a single +* listener for them. +* @return NSArray of FView +*/ +- (NSArray *) collectDistinctViewsForSubTree:(FImmutableTree *)subtree { + return [subtree foldWithBlock:^NSArray *(FPath *relativePath, FSyncPoint *maybeChildSyncPoint, NSDictionary *childMap) { + if (maybeChildSyncPoint && [maybeChildSyncPoint hasCompleteView]) { + FView *completeView = [maybeChildSyncPoint completeView]; + return @[completeView]; + } else { + // No complete view here, flatten any deeper listens into an array + NSMutableArray *views = [[NSMutableArray alloc] init]; + if (maybeChildSyncPoint) { + views = [[maybeChildSyncPoint queryViews] mutableCopy]; + } + [childMap enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, NSArray *childViews, BOOL *stop) { + [views addObjectsFromArray:childViews]; + }]; + return views; + } + }]; +} + +/** +* @param queries NSArray of FQuerySpec +*/ +- (void) removeTags:(NSArray *)queries { + [queries enumerateObjectsUsingBlock:^(FQuerySpec *removedQuery, NSUInteger idx, BOOL *stop) { + if (![removedQuery loadsAllData]) { + // We should have a tag for this + NSNumber *removedQueryTag = self.queryToTagMap[removedQuery]; + [self.queryToTagMap removeObjectForKey:removedQuery]; + [self.tagToQueryMap removeObjectForKey:removedQueryTag]; + } + }]; +} + +- (FQuerySpec *) queryForListening:(FQuerySpec *)query { + if (query.loadsAllData && !query.isDefault) { + // We treat queries that load all data as default queries + return [FQuerySpec defaultQueryAtPath:query.path]; + } else { + return query; + } +} + +/** +* For a given new listen, manage the de-duplication of outstanding subscriptions. +* @return NSArray of FEvent events to support synchronous data sources +*/ +- (NSArray *) setupListenerOnQuery:(FQuerySpec *)query view:(FView *)view { + FPath *path = query.path; + NSNumber *tagId = [self tagForQuery:query]; + FListenContainer *listenContainer = [self createListenerForView:view]; + + NSArray *events = self.listenProvider.startListening([self queryForListening:query], tagId, listenContainer, + listenContainer.onComplete); + + FImmutableTree *subtree = [self.syncPointTree subtreeAtPath:path]; + // The root of this subtree has our query. We're here because we definitely need to send a listen for that, but we + // may need to shadow other listens as well. + if (tagId != nil) { + NSAssert(![subtree.value hasCompleteView], @"If we're adding a query, it shouldn't be shadowed"); + } else { + // Shadow everything at or below this location, this is a default listener. + NSArray *queriesToStop = [subtree foldWithBlock:^id(FPath *relativePath, FSyncPoint *maybeChildSyncPoint, NSDictionary *childMap) { + if (![relativePath isEmpty] && maybeChildSyncPoint != nil && [maybeChildSyncPoint hasCompleteView]) { + return @[[maybeChildSyncPoint completeView].query]; + } else { + // No default listener here, flatten any deeper queries into an array + NSMutableArray *queries = [[NSMutableArray alloc] init]; + if (maybeChildSyncPoint != nil) { + for (FView *view in [maybeChildSyncPoint queryViews]) { + [queries addObject:view.query]; + } + } + [childMap enumerateKeysAndObjectsUsingBlock:^(NSString *key, NSArray *childQueries, BOOL *stop) { + [queries addObjectsFromArray:childQueries]; + }]; + return queries; + } + }]; + for (FQuerySpec *queryToStop in queriesToStop) { + self.listenProvider.stopListening([self queryForListening:queryToStop], [self tagForQuery:queryToStop]); + } + } + return events; +} + +- (FListenContainer *) createListenerForView:(FView *)view { + FQuerySpec *query = view.query; + NSNumber *tagId = [self tagForQuery:query]; + + FListenContainer *listenContainer = [[FListenContainer alloc] initWithView:view + onComplete:^(NSString *status) { + if ([status isEqualToString:@"ok"]) { + if (tagId != nil) { + return [self applyTaggedListenCompleteAtPath:query.path tagId:tagId]; + } else { + return [self applyListenCompleteAtPath:query.path]; + } + } else { + // If a listen failed, kill all of the listeners here, not just the one that triggered the error. + // Note that this may need to be scoped to just this listener if we change permissions on filtered children + NSError *error = [FUtilities errorForStatus:status andReason:nil]; + FFWarn(@"I-RDB038012", @"Listener at %@ failed: %@", query.path, status); + return [self removeEventRegistration:nil forQuery:query cancelError:error]; + } + }]; + + return listenContainer; +} + +/** +* @return The query associated with the given tag, if we have one +*/ +- (FQuerySpec *) queryForTag:(NSNumber *)tagId { + return self.tagToQueryMap[tagId]; +} + +/** +* @return The tag associated with the given query +*/ +- (NSNumber *) tagForQuery:(FQuerySpec *)query { + return self.queryToTagMap[query]; +} + +#pragma mark - +#pragma mark applyOperation Helpers + +/** +* A helper method that visits all descendant and ancestor SyncPoints, applying the operation. +* +* NOTES: +* - Descendant SyncPoints will be visited first (since we raise events depth-first). + +* - We call applyOperation: on each SyncPoint passing three things: +* 1. A version of the Operation that has been made relative to the SyncPoint location. +* 2. A WriteTreeRef of any writes we have cached at the SyncPoint location. +* 3. A snapshot Node with cached server data, if we have it. + +* - We concatenate all of the events returned by each SyncPoint and return the result. +* +* @return Array of FEvent +*/ +- (NSArray *) applyOperationToSyncPoints:(id<FOperation>)operation { + return [self applyOperationHelper:operation syncPointTree:self.syncPointTree serverCache:nil + writesCache:[self.pendingWriteTree childWritesForPath:[FPath empty]]]; +} + +/** +* Recursive helper for applyOperationToSyncPoints_ +*/ +- (NSArray *) applyOperationHelper:(id<FOperation>)operation syncPointTree:(FImmutableTree *)syncPointTree + serverCache:(id<FNode>)serverCache writesCache:(FWriteTreeRef *)writesCache { + if ([operation.path isEmpty]) { + return [self applyOperationDescendantsHelper:operation syncPointTree:syncPointTree serverCache:serverCache writesCache:writesCache]; + } else { + FSyncPoint *syncPoint = syncPointTree.value; + + // If we don't have cached server data, see if we can get it from this SyncPoint + if (serverCache == nil && syncPoint != nil) { + serverCache = [syncPoint completeServerCacheAtPath:[FPath empty]]; + } + + NSMutableArray *events = [[NSMutableArray alloc] init]; + NSString *childKey = [operation.path getFront]; + id<FOperation> childOperation = [operation operationForChild:childKey]; + FImmutableTree *childTree = [syncPointTree.children get:childKey]; + if (childTree != nil && childOperation != nil) { + id<FNode> childServerCache = serverCache ? [serverCache getImmediateChild:childKey] : nil; + FWriteTreeRef *childWritesCache = [writesCache childWriteTreeRef:childKey]; + [events addObjectsFromArray:[self applyOperationHelper:childOperation syncPointTree:childTree serverCache:childServerCache writesCache:childWritesCache]]; + } + + if (syncPoint) { + [events addObjectsFromArray:[syncPoint applyOperation:operation writesCache:writesCache serverCache:serverCache]]; + } + + return events; + } +} + +/** +* Recursive helper for applyOperationToSyncPoints: +*/ +- (NSArray *) applyOperationDescendantsHelper:(id<FOperation>)operation syncPointTree:(FImmutableTree *)syncPointTree + serverCache:(id<FNode>)serverCache writesCache:(FWriteTreeRef *)writesCache { + FSyncPoint *syncPoint = syncPointTree.value; + + // If we don't have cached server data, see if we can get it from this SyncPoint + id<FNode> resolvedServerCache; + if (serverCache == nil & syncPoint != nil) { + resolvedServerCache = [syncPoint completeServerCacheAtPath:[FPath empty]]; + } else { + resolvedServerCache = serverCache; + } + + NSMutableArray *events = [[NSMutableArray alloc] init]; + [syncPointTree.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) { + id<FNode> childServerCache = nil; + if (resolvedServerCache != nil) { + childServerCache = [resolvedServerCache getImmediateChild:childKey]; + } + FWriteTreeRef *childWritesCache = [writesCache childWriteTreeRef:childKey]; + id<FOperation> childOperation = [operation operationForChild:childKey]; + if (childOperation != nil) { + [events addObjectsFromArray:[self applyOperationDescendantsHelper:childOperation + syncPointTree:childTree + serverCache:childServerCache + writesCache:childWritesCache]]; + } + }]; + + if (syncPoint) { + [events addObjectsFromArray:[syncPoint applyOperation:operation writesCache:writesCache serverCache:resolvedServerCache]]; + } + + return events; +} + +@end diff --git a/Firebase/Database/Core/FWriteRecord.h b/Firebase/Database/Core/FWriteRecord.h new file mode 100644 index 0000000..a9b53fe --- /dev/null +++ b/Firebase/Database/Core/FWriteRecord.h @@ -0,0 +1,40 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FPath; +@class FCompoundWrite; +@protocol FNode; + +@interface FWriteRecord : NSObject + +- initWithPath:(FPath *)path overwrite:(id<FNode>)overwrite writeId:(NSInteger)writeId visible:(BOOL)isVisible; +- initWithPath:(FPath *)path merge:(FCompoundWrite *)merge writeId:(NSInteger)writeId; + +@property (nonatomic, readonly) NSInteger writeId; +@property (nonatomic, strong, readonly) FPath *path; +@property (nonatomic, strong, readonly) id<FNode> overwrite; +/** +* Maps NSString -> id<FNode> +*/ +@property (nonatomic, strong, readonly) FCompoundWrite *merge; +@property (nonatomic, readonly) BOOL visible; + +- (BOOL)isMerge; +- (BOOL)isOverwrite; + +@end diff --git a/Firebase/Database/Core/FWriteRecord.m b/Firebase/Database/Core/FWriteRecord.m new file mode 100644 index 0000000..47c952c --- /dev/null +++ b/Firebase/Database/Core/FWriteRecord.m @@ -0,0 +1,117 @@ +/* + * 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 "FWriteRecord.h" +#import "FPath.h" +#import "FNode.h" +#import "FCompoundWrite.h" + +@interface FWriteRecord () +@property (nonatomic, readwrite) NSInteger writeId; +@property (nonatomic, strong, readwrite) FPath *path; +@property (nonatomic, strong, readwrite) id<FNode> overwrite; +@property (nonatomic, strong, readwrite) FCompoundWrite *merge; +@property (nonatomic, readwrite) BOOL visible; +@end + +@implementation FWriteRecord + +- (id)initWithPath:(FPath *)path overwrite:(id<FNode>)overwrite writeId:(NSInteger)writeId visible:(BOOL)isVisible { + self = [super init]; + if (self) { + self.path = path; + if (overwrite == nil) { + [NSException raise:NSInvalidArgumentException format:@"Can't pass nil as overwrite parameter to an overwrite write record"]; + } + self.overwrite = overwrite; + self.merge = nil; + self.writeId = writeId; + self.visible = isVisible; + } + return self; +} + +- (id)initWithPath:(FPath *)path merge:(FCompoundWrite *)merge writeId:(NSInteger)writeId { + self = [super init]; + if (self) { + self.path = path; + if (merge == nil) { + [NSException raise:NSInvalidArgumentException format:@"Can't pass nil as merge parameter to an merge write record"]; + } + self.overwrite = nil; + self.merge = merge; + self.writeId = writeId; + self.visible = YES; + } + return self; +} + +- (id<FNode>)overwrite { + if (self->_overwrite == nil) { + [NSException raise:NSInvalidArgumentException format:@"Can't get overwrite for merge write record!"]; + } + return self->_overwrite; +} + +- (FCompoundWrite *)compoundWrite { + if (self->_merge == nil) { + [NSException raise:NSInvalidArgumentException format:@"Can't get merge for overwrite write record!"]; + } + return self->_merge; +} + +- (BOOL)isMerge { + return self->_merge != nil; +} + +- (BOOL)isOverwrite { + return self->_overwrite != nil; +} + +- (NSString *)description { + if (self.isOverwrite) { + return [NSString stringWithFormat:@"FWriteRecord { writeId = %lu, path = %@, overwrite = %@, visible = %d }", + (unsigned long)self.writeId, self.path, self.overwrite, self.visible]; + } else { + return [NSString stringWithFormat:@"FWriteRecord { writeId = %lu, path = %@, merge = %@ }", + (unsigned long)self.writeId, self.path, self.merge]; + } +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[self class]]) { + return NO; + } + FWriteRecord *other = (FWriteRecord *)object; + if (self->_writeId != other->_writeId) return NO; + if (self->_path != other->_path && ![self->_path isEqual:other->_path]) return NO; + if (self->_overwrite != other->_overwrite && ![self->_overwrite isEqual:other->_overwrite]) return NO; + if (self->_merge != other->_merge && ![self->_merge isEqual:other->_merge]) return NO; + if (self->_visible != other->_visible) return NO; + + return YES; +} + +- (NSUInteger)hash { + NSUInteger hash = self->_writeId * 17; + hash = hash * 31 + self->_path.hash; + hash = hash * 31 + self->_overwrite.hash; + hash = hash * 31 + self->_merge.hash; + hash = hash * 31 + ((self->_visible) ? 1 : 0); + return hash; +} + +@end diff --git a/Firebase/Database/Core/FWriteTree.h b/Firebase/Database/Core/FWriteTree.h new file mode 100644 index 0000000..243bc9f --- /dev/null +++ b/Firebase/Database/Core/FWriteTree.h @@ -0,0 +1,63 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FPath; +@protocol FNode; +@class FCompoundWrite; +@class FWriteTreeRef; +@class FChildrenNode; +@class FNamedNode; +@class FWriteRecord; +@protocol FIndex; +@class FCacheNode; + +@interface FWriteTree : NSObject + +- (FWriteTreeRef *) childWritesForPath:(FPath *)path; +- (void) addOverwriteAtPath:(FPath *)path newData:(id<FNode>)newData writeId:(NSInteger)writeId isVisible:(BOOL)visible; +- (void) addMergeAtPath:(FPath *)path changedChildren:(FCompoundWrite *)changedChildren writeId:(NSInteger)writeId; +- (BOOL) removeWriteId:(NSInteger)writeId; +- (NSArray *) removeAllWrites; +- (FWriteRecord *)writeForId:(NSInteger)writeId; + +- (id<FNode>) calculateCompleteEventCacheAtPath:(FPath *)treePath + completeServerCache:(id<FNode>)completeServerCache + excludeWriteIds:(NSArray *)writeIdsToExclude + includeHiddenWrites:(BOOL)includeHiddenWrites; + +- (id<FNode>) calculateCompleteEventChildrenAtPath:(FPath *)treePath + completeServerChildren:(id<FNode>)completeServerChildren; + +- (id<FNode>) calculateEventCacheAfterServerOverwriteAtPath:(FPath *)treePath + childPath:(FPath *)childPath + existingEventSnap:(id<FNode>)existingEventSnap + existingServerSnap:(id<FNode>)existingServerSnap; + +- (id<FNode>) calculateCompleteChildAtPath:(FPath *)treePath + childKey:(NSString *)childKey + cache:(FCacheNode *)existingServerCache; + +- (id<FNode>) shadowingWriteAtPath:(FPath *)path; + +- (FNamedNode *) calculateNextNodeAfterPost:(FNamedNode *)post + atPath:(FPath *)path + completeServerData:(id<FNode>)completeServerData + reverse:(BOOL)reverse + index:(id<FIndex>)index; + +@end diff --git a/Firebase/Database/Core/FWriteTree.m b/Firebase/Database/Core/FWriteTree.m new file mode 100644 index 0000000..c5b08ea --- /dev/null +++ b/Firebase/Database/Core/FWriteTree.m @@ -0,0 +1,458 @@ +/* + * 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 diff --git a/Firebase/Database/Core/FWriteTreeRef.h b/Firebase/Database/Core/FWriteTreeRef.h new file mode 100644 index 0000000..791dd26 --- /dev/null +++ b/Firebase/Database/Core/FWriteTreeRef.h @@ -0,0 +1,51 @@ +/* + * 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 <Foundation/Foundation.h> + +@protocol FNode; +@class FChildrenNode; +@class FPath; +@class FNamedNode; +@class FWriteRecord; +@class FWriteTree; +@protocol FIndex; +@class FCacheNode; + +@interface FWriteTreeRef : NSObject + +- (id) initWithPath:(FPath *)aPath writeTree:(FWriteTree *)tree; + +- (id <FNode>) calculateCompleteEventCacheWithCompleteServerCache:(id <FNode>)completeServerCache; + +- (FChildrenNode *) calculateCompleteEventChildrenWithCompleteServerChildren:(FChildrenNode *)completeServerChildren; + +- (id<FNode>) calculateEventCacheAfterServerOverwriteWithChildPath:(FPath *)childPath + existingEventSnap:(id<FNode>)existingEventSnap + existingServerSnap:(id<FNode>)existingServerSnap; + +- (id<FNode>) shadowingWriteAtPath:(FPath *)path; + +- (FNamedNode *) calculateNextNodeAfterPost:(FNamedNode *)post + completeServerData:(id<FNode>)completeServerData + reverse:(BOOL)reverse + index:(id<FIndex>)index; + +- (id<FNode>) calculateCompleteChild:(NSString *)childKey cache:(FCacheNode *)existingServerCache; + +- (FWriteTreeRef *) childWriteTreeRef:(NSString *)childKey; + +@end diff --git a/Firebase/Database/Core/FWriteTreeRef.m b/Firebase/Database/Core/FWriteTreeRef.m new file mode 100644 index 0000000..392369b --- /dev/null +++ b/Firebase/Database/Core/FWriteTreeRef.m @@ -0,0 +1,133 @@ +/* + * 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 "FWriteTreeRef.h" +#import "FPath.h" +#import "FNode.h" +#import "FWriteTree.h" +#import "FChildrenNode.h" +#import "FNamedNode.h" +#import "FWriteRecord.h" +#import "FIndex.h" +#import "FCacheNode.h" + +@interface FWriteTreeRef () +/** +* The path to this particular FWriteTreeRef. Used for calling methods on writeTree while exposing a simpler interface +* to callers. +*/ +@property (nonatomic, strong) FPath *path; +/** +* A reference to the actual tree of the write data. All methods are pass-through to the tree, but with the appropriate +* path prefixed. +* +* This lets us make cheap references to points in the tree for sync points without having to copy and maintain all of +* the data. +*/ +@property (nonatomic, strong) FWriteTree *writeTree; +@end + +/** +* A FWriteTreeRef wraps a FWriteTree and a FPath, for convenient access to a particular subtree. All the methods just +* proxy to the underlying FWriteTree. +*/ +@implementation FWriteTreeRef +- (id) initWithPath:(FPath *)aPath writeTree:(FWriteTree *)tree { + self = [super init]; + if (self) { + self.path = aPath; + self.writeTree = tree; + } + return self; +} + +/** +* @return If possible, returns a complete event cache, using the underlying server data if possible. In addition, can +* be used to get a cache that includes hidden writes, and excludes arbitrary writes. Note that customizing the returned +* node can lead to a more expensive calculation. +*/ +- (id <FNode>) calculateCompleteEventCacheWithCompleteServerCache:(id<FNode>)completeServerCache { + return [self.writeTree calculateCompleteEventCacheAtPath:self.path completeServerCache:completeServerCache excludeWriteIds:nil includeHiddenWrites:NO]; +} + +/** +* @return If possible, returns a children node containing all of the complete children we have data for. The returned +* data is a mix of the given server data and write data. +*/ +- (FChildrenNode *) calculateCompleteEventChildrenWithCompleteServerChildren:(id<FNode>)completeServerChildren { + return [self.writeTree calculateCompleteEventChildrenAtPath:self.path completeServerChildren:completeServerChildren]; +} + +/** +* Given that either the underlying server data has updated or the outstanding writes have been updating, determine what, +* if anything, needs to be applied to the event cache. +* +* Possibilities: +* +* 1. No writes are shadowing. Events should be raised, the snap to be applied comes from the server data. +* +* 2. Some writes are completly shadowing. No events to be raised. +* +* 3. Is partially shadowed. Events should be raised. +* +* Either existingEventSnap or existingServerSnap must exist, this is validated via an assert. +*/ +- (id<FNode>) calculateEventCacheAfterServerOverwriteWithChildPath:(FPath *)childPath existingEventSnap:(id <FNode>)existingEventSnap existingServerSnap:(id <FNode>)existingServerSnap { + return [self.writeTree calculateEventCacheAfterServerOverwriteAtPath:self.path childPath:childPath existingEventSnap:existingEventSnap existingServerSnap:existingServerSnap]; +} + +/** +* 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 nil if there is no write at this path. +*/ +- (id<FNode>) shadowingWriteAtPath:(FPath *)path { + return [self.writeTree shadowingWriteAtPath:[self.path child:path]]; +} + +/** +* This method is used when processing child remove events on a query. If we can, we pull in children that are outside +* the window, but may now be in the window. +*/ +- (FNamedNode *)calculateNextNodeAfterPost:(FNamedNode *)post + completeServerData:(id<FNode>)completeServerData + reverse:(BOOL)reverse + index:(id<FIndex>)index +{ + return [self.writeTree calculateNextNodeAfterPost:post + atPath:self.path + completeServerData:completeServerData + reverse:reverse + index:index]; +} + +/** +* 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>) calculateCompleteChild:(NSString *)childKey cache:(FCacheNode *)existingServerCache { + return [self.writeTree calculateCompleteChildAtPath:self.path childKey:childKey cache:existingServerCache]; +} + +/** +* @return a WriteTreeref for a child. +*/ +- (FWriteTreeRef *) childWriteTreeRef:(NSString *)childKey { + return [[FWriteTreeRef alloc] initWithPath:[self.path childFromString:childKey] writeTree:self.writeTree]; +} + + +@end diff --git a/Firebase/Database/Core/Operation/FAckUserWrite.h b/Firebase/Database/Core/Operation/FAckUserWrite.h new file mode 100644 index 0000000..a337996 --- /dev/null +++ b/Firebase/Database/Core/Operation/FAckUserWrite.h @@ -0,0 +1,35 @@ +/* + * 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 "FOperation.h" + +@class FPath; +@class FOperationSource; +@class FImmutableTree; + + +@interface FAckUserWrite : NSObject <FOperation> + +- initWithPath:(FPath *)operationPath affectedTree:(FImmutableTree *)affectedTree revert:(BOOL)shouldRevert; + +@property (nonatomic, strong, readonly) FOperationSource *source; +@property (nonatomic, readonly) FOperationType type; +@property (nonatomic, strong, readonly) FPath *path; +// A FImmutableTree, containing @YES for each affected path. Affected paths can't overlap. +@property (nonatomic, strong, readonly) FImmutableTree *affectedTree; +@property (nonatomic, readonly) BOOL revert; + +@end diff --git a/Firebase/Database/Core/Operation/FAckUserWrite.m b/Firebase/Database/Core/Operation/FAckUserWrite.m new file mode 100644 index 0000000..f81e7f5 --- /dev/null +++ b/Firebase/Database/Core/Operation/FAckUserWrite.m @@ -0,0 +1,55 @@ +/* + * 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 "FAckUserWrite.h" +#import "FPath.h" +#import "FOperationSource.h" +#import "FImmutableTree.h" + + +@implementation FAckUserWrite + +- (id) initWithPath:(FPath *)operationPath affectedTree:(FImmutableTree *)tree revert:(BOOL)shouldRevert { + self = [super init]; + if (self) { + self->_source = [FOperationSource userInstance]; + self->_type = FOperationTypeAckUserWrite; + self->_path = operationPath; + self->_affectedTree = tree; + self->_revert = shouldRevert; + } + return self; +} + +- (FAckUserWrite *) operationForChild:(NSString *)childKey { + if (![self.path isEmpty]) { + NSAssert([self.path.getFront isEqualToString:childKey], @"operationForChild called for unrelated child."); + return [[FAckUserWrite alloc] initWithPath:[self.path popFront] affectedTree:self.affectedTree revert:self.revert]; + } else if (self.affectedTree.value != nil) { + NSAssert(self.affectedTree.children.isEmpty, @"affectedTree should not have overlapping affected paths."); + // All child locations are affected as well; just return same operation. + return self; + } else { + FImmutableTree *childTree = [self.affectedTree subtreeAtPath:[[FPath alloc] initWith:childKey]]; + return [[FAckUserWrite alloc] initWithPath:[FPath empty] affectedTree:childTree revert:self.revert]; + } +} + +- (NSString *) description { + return [NSString stringWithFormat:@"FAckUserWrite { path=%@, revert=%d, affectedTree=%@ }", self.path, self.revert, self.affectedTree]; +} + +@end diff --git a/Firebase/Database/Core/Operation/FMerge.h b/Firebase/Database/Core/Operation/FMerge.h new file mode 100644 index 0000000..4cab613 --- /dev/null +++ b/Firebase/Database/Core/Operation/FMerge.h @@ -0,0 +1,30 @@ +/* + * 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 "FOperation.h" + +@class FCompoundWrite; + +@interface FMerge : NSObject <FOperation> + +- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath children:(FCompoundWrite *)children; + +@property (nonatomic, strong, readonly) FOperationSource *source; +@property (nonatomic, readonly) FOperationType type; +@property (nonatomic, strong, readonly) FPath *path; +@property (nonatomic, strong, readonly) FCompoundWrite *children; + +@end diff --git a/Firebase/Database/Core/Operation/FMerge.m b/Firebase/Database/Core/Operation/FMerge.m new file mode 100644 index 0000000..8e6d924 --- /dev/null +++ b/Firebase/Database/Core/Operation/FMerge.m @@ -0,0 +1,71 @@ +/* + * 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 "FMerge.h" +#import "FOperationSource.h" +#import "FPath.h" +#import "FNode.h" +#import "FOverwrite.h" +#import "FCompoundWrite.h" + +@interface FMerge () +@property (nonatomic, strong, readwrite) FOperationSource *source; +@property (nonatomic, readwrite) FOperationType type; +@property (nonatomic, strong, readwrite) FPath *path; +@property (nonatomic, strong) FCompoundWrite *children; +@end + +@implementation FMerge + +@synthesize source; +@synthesize type; +@synthesize path; +@synthesize children; + +- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath children:(FCompoundWrite *)someChildren { + self = [super init]; + if (self) { + self.source = aSource; + self.type = FOperationTypeMerge; + self.path = aPath; + self.children = someChildren; + } + return self; +} + +- (id<FOperation>) operationForChild:(NSString *)childKey { + if ([self.path isEmpty]) { + FCompoundWrite *childTree = [self.children childCompoundWriteAtPath:[[FPath alloc] initWith:childKey]]; + if (childTree.isEmpty) { + return nil; + } else if (childTree.rootWrite != nil) { + // We have a snapshot for the child in question. This becomes an overwrite of the child. + return [[FOverwrite alloc] initWithSource:self.source path:[FPath empty] snap:childTree.rootWrite]; + } else { + // This is a merge at a deeper level + return [[FMerge alloc] initWithSource:self.source path:[FPath empty] children:childTree]; + } + } else { + NSAssert([self.path.getFront isEqualToString:childKey], @"Can't get a merge for a child not on the path of the operation"); + return [[FMerge alloc] initWithSource:self.source path:[self.path popFront] children:self.children]; + } +} + +- (NSString *) description { + return [NSString stringWithFormat:@"FMerge { path=%@, soruce=%@ children=%@}", self.path, self.source, self.children]; +} + +@end diff --git a/Firebase/Database/Core/Operation/FOperation.h b/Firebase/Database/Core/Operation/FOperation.h new file mode 100644 index 0000000..2bbbbd2 --- /dev/null +++ b/Firebase/Database/Core/Operation/FOperation.h @@ -0,0 +1,34 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FOperationSource; +@class FPath; + +typedef NS_ENUM(NSInteger, FOperationType) { + FOperationTypeOverwrite = 0, + FOperationTypeMerge = 1, + FOperationTypeAckUserWrite = 2, + FOperationTypeListenComplete = 3 +}; + +@protocol FOperation <NSObject> +@property (nonatomic, strong, readonly) FOperationSource *source; +@property (nonatomic, readonly) FOperationType type; +@property (nonatomic, strong, readonly) FPath *path; +- (id<FOperation>) operationForChild:(NSString *)childKey; +@end diff --git a/Firebase/Database/Core/Operation/FOperationSource.h b/Firebase/Database/Core/Operation/FOperationSource.h new file mode 100644 index 0000000..a069c2f --- /dev/null +++ b/Firebase/Database/Core/Operation/FOperationSource.h @@ -0,0 +1,34 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FQueryParams; + +@interface FOperationSource : NSObject + +@property (nonatomic, readonly) BOOL fromUser; +@property (nonatomic, readonly) BOOL fromServer; +@property (nonatomic, readonly) BOOL isTagged; +@property (nonatomic, strong, readonly) FQueryParams *queryParams; + +- initWithFromUser:(BOOL)isFromUser fromServer:(BOOL)isFromServer queryParams:(FQueryParams *)params tagged:(BOOL)isTagged; + ++ (FOperationSource *) userInstance; ++ (FOperationSource *) serverInstance; ++ (FOperationSource *) forServerTaggedQuery:(FQueryParams *)params; + +@end diff --git a/Firebase/Database/Core/Operation/FOperationSource.m b/Firebase/Database/Core/Operation/FOperationSource.m new file mode 100644 index 0000000..9a34a2e --- /dev/null +++ b/Firebase/Database/Core/Operation/FOperationSource.m @@ -0,0 +1,73 @@ +/* + * 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 "FOperationSource.h" +#import "FPath.h" +#import "FQueryParams.h" + +@interface FOperationSource () +@property (nonatomic, readwrite) BOOL fromUser; +@property (nonatomic, readwrite) BOOL fromServer; +@property (nonatomic, readwrite) BOOL isTagged; +@property (nonatomic, strong, readwrite) FQueryParams *queryParams; +@end + +@implementation FOperationSource + +@synthesize fromUser; +@synthesize fromServer; +@synthesize queryParams; + +- (id) initWithFromUser:(BOOL)isFromUser fromServer:(BOOL)isFromServer queryParams:(FQueryParams *)params tagged:(BOOL)tagged { + self = [super init]; + if (self) { + self.fromUser = isFromUser; + self.fromServer = isFromServer; + self.queryParams = params; + self.isTagged = tagged; + } + return self; +} + ++ (FOperationSource *) userInstance { + static FOperationSource *user = nil; + static dispatch_once_t userToken; + dispatch_once(&userToken, ^{ + user = [[FOperationSource alloc] initWithFromUser:YES fromServer:NO queryParams:nil tagged:NO]; + }); + return user; +} + ++ (FOperationSource *) serverInstance { + static FOperationSource *server = nil; + static dispatch_once_t serverToken; + dispatch_once(&serverToken, ^{ + server = [[FOperationSource alloc] initWithFromUser:NO fromServer:YES queryParams:nil tagged:NO]; + }); + return server; +} + ++ (FOperationSource *) forServerTaggedQuery:(FQueryParams *)params { + return [[FOperationSource alloc] initWithFromUser:NO fromServer:YES queryParams:params tagged:YES]; +} + +- (NSString *) description { + return [NSString stringWithFormat:@"FOperationSource { fromUser=%d, fromServer=%d, queryId=%@, tagged=%d }", + self.fromUser, self.fromServer, self.queryParams, self.isTagged]; +} + + +@end diff --git a/Firebase/Database/Core/Operation/FOverwrite.h b/Firebase/Database/Core/Operation/FOverwrite.h new file mode 100644 index 0000000..e950bed --- /dev/null +++ b/Firebase/Database/Core/Operation/FOverwrite.h @@ -0,0 +1,30 @@ +/* + * 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 "FOperation.h" + +@protocol FNode; + +@interface FOverwrite : NSObject <FOperation> + +- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath snap:(id<FNode>)aSnap; + +@property (nonatomic, strong, readonly) FOperationSource *source; +@property (nonatomic, readonly) FOperationType type; +@property (nonatomic, strong, readonly) FPath *path; +@property (nonatomic, strong, readonly) id<FNode> snap; + +@end diff --git a/Firebase/Database/Core/Operation/FOverwrite.m b/Firebase/Database/Core/Operation/FOverwrite.m new file mode 100644 index 0000000..b72d31a --- /dev/null +++ b/Firebase/Database/Core/Operation/FOverwrite.m @@ -0,0 +1,62 @@ +/* + * 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 "FOverwrite.h" +#import "FNode.h" +#import "FOperationSource.h" + +@interface FOverwrite () +@property (nonatomic, strong, readwrite) FOperationSource *source; +@property (nonatomic, readwrite) FOperationType type; +@property (nonatomic, strong, readwrite) FPath *path; +@property (nonatomic, strong) id<FNode> snap; +@end + +@implementation FOverwrite + +@synthesize source; +@synthesize type; +@synthesize path; +@synthesize snap; + +- (id) initWithSource:(FOperationSource *)aSource path:(FPath *)aPath snap:(id <FNode>)aSnap { + self = [super init]; + if (self) { + self.source = aSource; + self.type = FOperationTypeOverwrite; + self.path = aPath; + self.snap = aSnap; + } + return self; +} + +- (FOverwrite *) operationForChild:(NSString *)childKey { + if ([self.path isEmpty]) { + return [[FOverwrite alloc] initWithSource:self.source + path:[FPath empty] + snap:[self.snap getImmediateChild:childKey]]; + } else { + return [[FOverwrite alloc] initWithSource:self.source + path:[self.path popFront] + snap:self.snap]; + } +} + +- (NSString *) description { + return [NSString stringWithFormat:@"FOverwrite { path=%@, source=%@, snapshot=%@ }", self.path, self.source, self.snap]; +} + +@end diff --git a/Firebase/Database/Core/Utilities/FIRRetryHelper.h b/Firebase/Database/Core/Utilities/FIRRetryHelper.h new file mode 100644 index 0000000..ffe2726 --- /dev/null +++ b/Firebase/Database/Core/Utilities/FIRRetryHelper.h @@ -0,0 +1,33 @@ +/* + * 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 <Foundation/Foundation.h> + +@interface FIRRetryHelper : NSObject + +- (instancetype) initWithDispatchQueue:(dispatch_queue_t)dispatchQueue + minRetryDelayAfterFailure:(NSTimeInterval)minRetryDelayAfterFailure + maxRetryDelay:(NSTimeInterval)maxRetryDelay + retryExponent:(double)retryExponent + jitterFactor:(double)jitterFactor; + +- (void) retry:(void (^)())block; + +- (void) cancel; + +- (void) signalSuccess; + +@end diff --git a/Firebase/Database/Core/Utilities/FIRRetryHelper.m b/Firebase/Database/Core/Utilities/FIRRetryHelper.m new file mode 100644 index 0000000..199e17d --- /dev/null +++ b/Firebase/Database/Core/Utilities/FIRRetryHelper.m @@ -0,0 +1,139 @@ +/* + * 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 "FIRRetryHelper.h" +#import "FUtilities.h" + +@interface FIRRetryHelperTask : NSObject + +@property (nonatomic, strong) void (^block)(); + +@end + +@implementation FIRRetryHelperTask + +- (instancetype) initWithBlock:(void (^)())block { + self = [super init]; + if (self != nil) { + self->_block = [block copy]; + } + return self; +} + +- (BOOL) isCanceled { + return self.block == nil; +} + +- (void) cancel { + self.block = nil; +} + +- (void) execute { + if (self.block) { + self.block(); + } +} + +@end + + + +@interface FIRRetryHelper () + +@property (nonatomic, strong) dispatch_queue_t dispatchQueue; +@property (nonatomic) NSTimeInterval minRetryDelayAfterFailure; +@property (nonatomic) NSTimeInterval maxRetryDelay; +@property (nonatomic) double retryExponent; +@property (nonatomic) double jitterFactor; + +@property (nonatomic) BOOL lastWasSuccess; +@property (nonatomic) NSTimeInterval currentRetryDelay; + +@property (nonatomic, strong) FIRRetryHelperTask *scheduledRetry; + +@end + +@implementation FIRRetryHelper + +- (instancetype) initWithDispatchQueue:(dispatch_queue_t)dispatchQueue + minRetryDelayAfterFailure:(NSTimeInterval)minRetryDelayAfterFailure + maxRetryDelay:(NSTimeInterval)maxRetryDelay + retryExponent:(double)retryExponent + jitterFactor:(double)jitterFactor { + self = [super init]; + if (self != nil) { + self->_dispatchQueue = dispatchQueue; + self->_minRetryDelayAfterFailure = minRetryDelayAfterFailure; + self->_maxRetryDelay = maxRetryDelay; + self->_retryExponent = retryExponent; + self->_jitterFactor = jitterFactor; + self->_lastWasSuccess = YES; + } + return self; +} + +- (void) retry:(void (^)())block { + if (self.scheduledRetry != nil) { + FFLog(@"I-RDB054001", @"Canceling existing retry attempt"); + [self.scheduledRetry cancel]; + self.scheduledRetry = nil; + } + + NSTimeInterval delay; + if (self.lastWasSuccess) { + delay = 0; + } else { + if (self.currentRetryDelay == 0) { + self.currentRetryDelay = self.minRetryDelayAfterFailure; + } else { + NSTimeInterval newDelay = (self.currentRetryDelay * self.retryExponent); + self.currentRetryDelay = MIN(newDelay, self.maxRetryDelay); + } + + delay = ((1 - self.jitterFactor) * self.currentRetryDelay) + + (self.jitterFactor * self.currentRetryDelay * [FUtilities randomDouble]); + FFLog(@"I-RDB054002", @"Scheduling retry in %fs", delay); + + } + self.lastWasSuccess = NO; + FIRRetryHelperTask *task = [[FIRRetryHelperTask alloc] initWithBlock:block]; + self.scheduledRetry = task; + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (long long)(delay * NSEC_PER_SEC)); + dispatch_after(popTime, self.dispatchQueue, ^{ + if (![task isCanceled]) { + self.scheduledRetry = nil; + [task execute]; + } + }); +} + +- (void) signalSuccess { + self.lastWasSuccess = YES; + self.currentRetryDelay = 0; +} + +- (void) cancel { + if (self.scheduledRetry != nil) { + FFLog(@"I-RDB054003", @"Canceling existing retry attempt"); + [self.scheduledRetry cancel]; + self.scheduledRetry = nil; + } else { + FFLog(@"I-RDB054004", @"No existing retry attempt to cancel"); + } + self.currentRetryDelay = 0; +} + +@end diff --git a/Firebase/Database/Core/Utilities/FImmutableTree.h b/Firebase/Database/Core/Utilities/FImmutableTree.h new file mode 100644 index 0000000..005a9f2 --- /dev/null +++ b/Firebase/Database/Core/Utilities/FImmutableTree.h @@ -0,0 +1,51 @@ +/* + * 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 "FImmutableSortedDictionary.h" +#import "FPath.h" +#import "FTuplePathValue.h" + +@interface FImmutableTree : NSObject + +- (id) initWithValue:(id)aValue; +- (id) initWithValue:(id)aValue children:(FImmutableSortedDictionary *)childrenMap; + ++ (FImmutableTree *) empty; +- (BOOL) isEmpty; + +- (FTuplePathValue *) findRootMostMatchingPath:(FPath *)relativePath predicate:(BOOL (^)(id))predicate; +- (FTuplePathValue *) findRootMostValueAndPath:(FPath *)relativePath; +- (FImmutableTree *) subtreeAtPath:(FPath *)relativePath; +- (FImmutableTree *) setValue:(id)newValue atPath:(FPath *)relativePath; +- (FImmutableTree *) removeValueAtPath:(FPath *)relativePath; +- (id) valueAtPath:(FPath *)relativePath; +- (id) rootMostValueOnPath:(FPath *)path; +- (id) rootMostValueOnPath:(FPath *)path matching:(BOOL (^)(id))predicate; +- (id) leafMostValueOnPath:(FPath *)path; +- (id) leafMostValueOnPath:(FPath *)relativePath matching:(BOOL (^)(id))predicate; +- (BOOL) containsValueMatching:(BOOL (^)(id))predicate; +- (FImmutableTree *) setTree:(FImmutableTree *)newTree atPath:(FPath *)relativePath; +- (id) foldWithBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block; +- (id) findOnPath:(FPath *)path andApplyBlock:(id (^)(FPath *path, id value))block; +- (FPath *) forEachOnPath:(FPath *)path whileBlock:(BOOL (^)(FPath *path, id value))block; +- (FImmutableTree *) forEachOnPath:(FPath *)path performBlock:(void (^)(FPath *path, id value))block; +- (void) forEach:(void (^)(FPath *path, id value))block; +- (void) forEachChild:(void (^)(NSString *childKey, id childValue))block; + +@property (nonatomic, strong, readonly) id value; +@property (nonatomic, strong, readonly) FImmutableSortedDictionary *children; + +@end diff --git a/Firebase/Database/Core/Utilities/FImmutableTree.m b/Firebase/Database/Core/Utilities/FImmutableTree.m new file mode 100644 index 0000000..57bf74d --- /dev/null +++ b/Firebase/Database/Core/Utilities/FImmutableTree.m @@ -0,0 +1,421 @@ +/* + * 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 "FImmutableTree.h" +#import "FImmutableSortedDictionary.h" +#import "FPath.h" +#import "FUtilities.h" + +@interface FImmutableTree () +@property (nonatomic, strong, readwrite) id value; +/** +* Maps NSString -> FImmutableTree<T>, where <T> is type of value. +*/ +@property (nonatomic, strong, readwrite) FImmutableSortedDictionary *children; +@end + +@implementation FImmutableTree +@synthesize value; +@synthesize children; + +- (id) initWithValue:(id)aValue { + self = [super init]; + if (self) { + self.value = aValue; + self.children = [FImmutableTree emptyChildren]; + } + return self; +} + +- (id) initWithValue:(id)aValue children:(FImmutableSortedDictionary *)childrenMap { + self = [super init]; + if (self) { + self.value = aValue; + self.children = childrenMap; + } + return self; +} + ++ (FImmutableSortedDictionary *) emptyChildren { + static dispatch_once_t emptyChildrenToken; + static FImmutableSortedDictionary *emptyChildren; + dispatch_once(&emptyChildrenToken, ^{ + emptyChildren = [FImmutableSortedDictionary dictionaryWithComparator:[FUtilities stringComparator]]; + }); + return emptyChildren; +} + ++ (FImmutableTree *) empty { + static dispatch_once_t emptyImmutableTreeToken; + static FImmutableTree *emptyTree = nil; + dispatch_once(&emptyImmutableTreeToken, ^{ + emptyTree = [[FImmutableTree alloc] initWithValue:nil]; + }); + return emptyTree; +} + +- (BOOL) isEmpty { + return self.value == nil && [self.children isEmpty]; +} + +/** +* Given a path and a predicate, return the first node and the path to that node where the predicate returns true +* // TODO Do a perf test. If we're creating a bunch of FTuplePathValue objects on the way back out, it may be better to pass down a pathSoFar FPath +*/ +- (FTuplePathValue *) findRootMostMatchingPath:(FPath *)relativePath predicate:(BOOL (^)(id value))predicate { + if (self.value != nil && predicate(self.value)) { + return [[FTuplePathValue alloc] initWithPath:[FPath empty] value:self.value]; + } else { + if ([relativePath isEmpty]) { + return nil; + } else { + NSString *front = [relativePath getFront]; + FImmutableTree *child = [self.children get:front]; + if (child != nil) { + FTuplePathValue *childExistingPathAndValue = [child findRootMostMatchingPath:[relativePath popFront] predicate:predicate]; + if (childExistingPathAndValue != nil) { + FPath *fullPath = [[[FPath alloc] initWith:front] child:childExistingPathAndValue.path]; + return [[FTuplePathValue alloc] initWithPath:fullPath value:childExistingPathAndValue.value]; + } else { + return nil; + } + } else { + // No child matching path + return nil; + } + } + } +} + +/** +* Find, if it exists, the shortest subpath of the given path that points a defined value in the tree +*/ +- (FTuplePathValue *) findRootMostValueAndPath:(FPath *)relativePath { + return [self findRootMostMatchingPath:relativePath predicate:^BOOL(__unsafe_unretained id value){ + return YES; + }]; +} + +- (id) rootMostValueOnPath:(FPath *)path { + return [self rootMostValueOnPath:path matching:^BOOL(id value) { + return YES; + }]; +} + +- (id) rootMostValueOnPath:(FPath *)path matching:(BOOL (^)(id))predicate { + if (self.value != nil && predicate(self.value)) { + return self.value; + } else if (path.isEmpty) { + return nil; + } else { + return [[self.children get:path.getFront] rootMostValueOnPath:[path popFront] matching:predicate]; + } +} + +- (id) leafMostValueOnPath:(FPath *)path { + return [self leafMostValueOnPath:path matching:^BOOL(id value) { + return YES; + }]; +} + +- (id) leafMostValueOnPath:(FPath *)relativePath matching:(BOOL (^)(id))predicate { + __block id currentValue = self.value; + __block FImmutableTree *currentTree = self; + [relativePath enumerateComponentsUsingBlock:^(NSString *key, BOOL *stop) { + currentTree = [currentTree.children get:key]; + if (currentTree == nil) { + *stop = YES; + } else { + id treeValue = currentTree.value; + if (treeValue != nil && predicate(treeValue)) { + currentValue = treeValue; + } + } + }]; + return currentValue; +} + +- (BOOL) containsValueMatching:(BOOL (^)(id))predicate { + if (self.value != nil && predicate(self.value)) { + return YES; + } else { + __block BOOL found = NO; + [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *key, FImmutableTree *subtree, BOOL *stop) { + found = [subtree containsValueMatching:predicate]; + if (found) *stop = YES; + }]; + return found; + } +} + +- (FImmutableTree *) subtreeAtPath:(FPath *)relativePath { + if ([relativePath isEmpty]) { + return self; + } else { + NSString *front = [relativePath getFront]; + FImmutableTree *childTree = [self.children get:front]; + if (childTree != nil) { + return [childTree subtreeAtPath:[relativePath popFront]]; + } else { + return [FImmutableTree empty]; + } + } +} + +/** +* Sets a value at the specified path +*/ +- (FImmutableTree *) setValue:(id)newValue atPath:(FPath *)relativePath { + if ([relativePath isEmpty]) { + return [[FImmutableTree alloc] initWithValue:newValue children:self.children]; + } else { + NSString *front = [relativePath getFront]; + FImmutableTree *child = [self.children get:front]; + if (child == nil) { + child = [FImmutableTree empty]; + } + FImmutableTree *newChild = [child setValue:newValue atPath:[relativePath popFront]]; + FImmutableSortedDictionary *newChildren = [self.children insertKey:front withValue:newChild]; + return [[FImmutableTree alloc] initWithValue:self.value children:newChildren]; + } +} + +/** +* Remove the value at the specified path +*/ +- (FImmutableTree *) removeValueAtPath:(FPath *)relativePath { + if ([relativePath isEmpty]) { + if ([self.children isEmpty]) { + return [FImmutableTree empty]; + } else { + return [[FImmutableTree alloc] initWithValue:nil children:self.children]; + } + } else { + NSString *front = [relativePath getFront]; + FImmutableTree *child = [self.children get:front]; + if (child) { + FImmutableTree *newChild = [child removeValueAtPath:[relativePath popFront]]; + FImmutableSortedDictionary *newChildren; + if ([newChild isEmpty]) { + newChildren = [self.children removeKey:front]; + } else { + newChildren = [self.children insertKey:front withValue:newChild]; + } + if (self.value == nil && [newChildren isEmpty]) { + return [FImmutableTree empty]; + } else { + return [[FImmutableTree alloc] initWithValue:self.value children:newChildren]; + } + } else { + return self; + } + } +} + +/** +* Gets a value from the tree +*/ +- (id) valueAtPath:(FPath *)relativePath { + if ([relativePath isEmpty]) { + return self.value; + } else { + NSString *front = [relativePath getFront]; + FImmutableTree *child = [self.children get:front]; + if (child) { + return [child valueAtPath:[relativePath popFront]]; + } else { + return nil; + } + } +} + +/** +* Replaces the subtree at the specified path with the given new tree +*/ +- (FImmutableTree *) setTree:(FImmutableTree *)newTree atPath:(FPath *)relativePath { + if ([relativePath isEmpty]) { + return newTree; + } else { + NSString *front = [relativePath getFront]; + FImmutableTree *child = [self.children get:front]; + if (child == nil) { + child = [FImmutableTree empty]; + } + FImmutableTree *newChild = [child setTree:newTree atPath:[relativePath popFront]]; + FImmutableSortedDictionary *newChildren; + if ([newChild isEmpty]) { + newChildren = [self.children removeKey:front]; + } else { + newChildren = [self.children insertKey:front withValue:newChild]; + } + return [[FImmutableTree alloc] initWithValue:self.value children:newChildren]; + } +} + +/** +* Performs a depth first fold on this tree. Transforms a tree into a single value, given a function that operates on +* the path to a node, an optional current value, and a map of the child names to folded subtrees +*/ +- (id) foldWithBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block { + return [self foldWithPathSoFar:[FPath empty] withBlock:block]; +} + +/** +* Recursive helper for public facing foldWithBlock: method +*/ +- (id) foldWithPathSoFar:(FPath *)pathSoFar withBlock:(id (^)(FPath *path, id value, NSDictionary *foldedChildren))block { + __block NSMutableDictionary *accum = [[NSMutableDictionary alloc] init]; + [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) { + accum[childKey] = [childTree foldWithPathSoFar:[pathSoFar childFromString:childKey] withBlock:block]; + }]; + return block(pathSoFar, self.value, accum); +} + +/** +* Find the first matching value on the given path. Return the result of applying block to it. +*/ +- (id) findOnPath:(FPath *)path andApplyBlock:(id (^)(FPath *path, id value))block { + return [self findOnPath:path pathSoFar:[FPath empty] andApplyBlock:block]; +} + +- (id) findOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar andApplyBlock:(id (^)(FPath *path, id value))block { + id result = self.value ? block(pathSoFar, self.value) : nil; + if (result != nil) { + return result; + } else { + if ([pathToFollow isEmpty]) { + return nil; + } else { + NSString *front = [pathToFollow getFront]; + FImmutableTree *nextChild = [self.children get:front]; + if (nextChild != nil) { + return [nextChild findOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] andApplyBlock:block]; + } else { + return nil; + } + } + } +} +/** +* Call the block on each value along the path for as long as that function returns true +* @return The path to the deepest location inspected +*/ +- (FPath *) forEachOnPath:(FPath *)path whileBlock:(BOOL (^)(FPath *, id))block { + return [self forEachOnPath:path pathSoFar:[FPath empty] whileBlock:block]; +} + +- (FPath *) forEachOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar whileBlock:(BOOL (^)(FPath *, id))block { + if ([pathToFollow isEmpty]) { + if (self.value) { + block(pathSoFar, self.value); + } + return pathSoFar; + } else { + BOOL shouldContinue = YES; + if (self.value) { + shouldContinue = block(pathSoFar, self.value); + } + if (shouldContinue) { + NSString *front = [pathToFollow getFront]; + FImmutableTree *nextChild = [self.children get:front]; + if (nextChild) { + return [nextChild forEachOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] whileBlock:block]; + } else { + return pathSoFar; + } + } else { + return pathSoFar; + } + } +} + +- (FImmutableTree *) forEachOnPath:(FPath *)path performBlock:(void (^)(FPath *path, id value))block { + return [self forEachOnPath:path pathSoFar:[FPath empty] performBlock:block]; +} + +- (FImmutableTree *) forEachOnPath:(FPath *)pathToFollow pathSoFar:(FPath *)pathSoFar performBlock:(void (^)(FPath *path, id value))block { + if ([pathToFollow isEmpty]) { + return self; + } else { + if (self.value) { + block(pathSoFar, self.value); + } + NSString *front = [pathToFollow getFront]; + FImmutableTree *nextChild = [self.children get:front]; + if (nextChild) { + return [nextChild forEachOnPath:[pathToFollow popFront] pathSoFar:[pathSoFar childFromString:front] performBlock:block]; + } else { + return [FImmutableTree empty]; + } + } +} +/** +* Calls the given block for each node in the tree that has a value. Called in depth-first order +*/ +- (void) forEach:(void (^)(FPath *path, id value))block { + [self forEachPathSoFar:[FPath empty] withBlock:block]; +} + +- (void) forEachPathSoFar:(FPath *)pathSoFar withBlock:(void (^)(FPath *path, id value))block { + [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) { + [childTree forEachPathSoFar:[pathSoFar childFromString:childKey] withBlock:block]; + }]; + if (self.value) { + block(pathSoFar, self.value); + } +} + +- (void) forEachChild:(void (^)(NSString *childKey, id childValue))block { + [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) { + if (childTree.value) { + block(childKey, childTree.value); + } + }]; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[FImmutableTree class]]) { + return NO; + } + FImmutableTree *other = (FImmutableTree *)object; + return (self.value == other.value || [self.value isEqual:other.value]) && [self.children isEqual:other.children]; +} + +- (NSUInteger)hash { + return self.children.hash * 31 + [self.value hash]; +} + +- (NSString *) description { + NSMutableString *string = [[NSMutableString alloc] init]; + [string appendString:@"FImmutableTree { value="]; + [string appendString:(self.value ? [self.value description] : @"<nil>")]; + [string appendString:@", children={"]; + [self.children enumerateKeysAndObjectsUsingBlock:^(NSString *childKey, FImmutableTree *childTree, BOOL *stop) { + [string appendString:@" "]; + [string appendString:childKey]; + [string appendString:@"="]; + [string appendString:[childTree.value description]]; + }]; + [string appendString:@" } }"]; + return [NSString stringWithString:string]; +} + +- (NSString *) debugDescription { + return [self description]; +} + + +@end diff --git a/Firebase/Database/Core/Utilities/FPath.h b/Firebase/Database/Core/Utilities/FPath.h new file mode 100644 index 0000000..71a7167 --- /dev/null +++ b/Firebase/Database/Core/Utilities/FPath.h @@ -0,0 +1,45 @@ +/* + * 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 <Foundation/Foundation.h> + +@interface FPath : NSObject<NSCopying> + ++ (FPath *) relativePathFrom:(FPath *)outer to:(FPath *)inner; ++ (FPath *) empty; ++ (FPath *) pathWithString:(NSString *)string; + +- (id)initWith:(NSString *)path; +- (id)initWithPieces:(NSArray *)somePieces andPieceNum:(NSInteger)aPieceNum; + +- (id)copyWithZone:(NSZone *)zone; + +- (void)enumerateComponentsUsingBlock:(void (^)(NSString *key, BOOL *stop))block; +- (NSString *) getFront; +- (NSUInteger) length; +- (FPath *) popFront; +- (NSString *) getBack; +- (NSString *) toString; +- (NSString *) toStringWithTrailingSlash; +- (NSString *) wireFormat; +- (FPath *) parent; +- (FPath *) child:(FPath *)childPathObj; +- (FPath *) childFromString:(NSString *)childPath; +- (BOOL) isEmpty; +- (BOOL) contains:(FPath *)other; +- (NSComparisonResult) compare:(FPath *)other; + +@end diff --git a/Firebase/Database/Core/Utilities/FPath.m b/Firebase/Database/Core/Utilities/FPath.m new file mode 100644 index 0000000..485b903 --- /dev/null +++ b/Firebase/Database/Core/Utilities/FPath.m @@ -0,0 +1,298 @@ +/* + * 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 "FPath.h" + +#import "FUtilities.h" + +@interface FPath() + +@property (nonatomic, readwrite, assign) NSInteger pieceNum; +@property (nonatomic, strong) NSArray * pieces; + +@end + +@implementation FPath + +#pragma mark - +#pragma mark Initializers + ++ (FPath *) relativePathFrom:(FPath *)outer to:(FPath *)inner { + NSString* outerFront = [outer getFront]; + NSString* innerFront = [inner getFront]; + if (outerFront == nil) { + return inner; + } else if ([outerFront isEqualToString:innerFront]) { + return [self relativePathFrom:[outer popFront] to:[inner popFront]]; + } else { + @throw [[NSException alloc] initWithName:@"FirebaseDatabaseInternalError" reason:[NSString stringWithFormat:@"innerPath (%@) is not within outerPath (%@)", inner, outer] userInfo:nil]; + } +} + ++ (FPath *)pathWithString:(NSString *)string +{ + return [[FPath alloc] initWith:string]; +} + +- (id)initWith:(NSString *)path +{ + self = [super init]; + if (self) { + NSArray *pathPieces = [path componentsSeparatedByString:@"/"]; + NSMutableArray *newPieces = [[NSMutableArray alloc] init]; + for (NSInteger i = 0; i < pathPieces.count; i++) { + NSString *piece = [pathPieces objectAtIndex:i]; + if (piece.length > 0) { + [newPieces addObject:piece]; + } + } + + self.pieces = newPieces; + self.pieceNum = 0; + } + return self; +} + +- (id)initWithPieces:(NSArray *)somePieces andPieceNum:(NSInteger)aPieceNum { + self = [super init]; + if (self) { + self.pieceNum = aPieceNum; + self.pieces = somePieces; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + // Immutable, so it's safe to return self + return self; +} + +- (NSString *)description { + return [self toString]; +} + +#pragma mark - +#pragma mark Public methods + +- (NSString *) getFront { + if(self.pieceNum >= self.pieces.count) { + return nil; + } + return [self.pieces objectAtIndex:self.pieceNum]; +} + +/** +* @return The number of segments in this path +*/ +- (NSUInteger) length { + return self.pieces.count - self.pieceNum; +} + +- (FPath *) popFront { + NSInteger newPieceNum = self.pieceNum; + if (newPieceNum < self.pieces.count) { + newPieceNum++; + } + return [[FPath alloc] initWithPieces:self.pieces andPieceNum:newPieceNum]; +} + +- (NSString *) getBack { + if(self.pieceNum < self.pieces.count) { + return [self.pieces lastObject]; + } + else { + return nil; + } +} + +- (NSString *) toString { + return [self toStringWithTrailingSlash:NO]; +} + +- (NSString *) toStringWithTrailingSlash { + return [self toStringWithTrailingSlash:YES]; +} + +- (NSString *) toStringWithTrailingSlash:(BOOL)trailingSlash { + NSMutableString* pathString = [[NSMutableString alloc] init]; + for(NSInteger i = self.pieceNum; i < self.pieces.count; i++) { + [pathString appendString:@"/"]; + [pathString appendString:[self.pieces objectAtIndex:i]]; + } + if ([pathString length] == 0) { + return @"/"; + } else { + if (trailingSlash) { + [pathString appendString:@"/"]; + } + return pathString; + } +} + +- (NSString *)wireFormat { + if ([self isEmpty]) { + return @"/"; + } else { + NSMutableString* pathString = [[NSMutableString alloc] init]; + for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) { + if (i > self.pieceNum) { + [pathString appendString:@"/"]; + } + [pathString appendString:[self.pieces objectAtIndex:i]]; + } + return pathString; + } +} + +- (FPath *) parent { + if(self.pieceNum >= self.pieces.count) { + return nil; + } else { + NSMutableArray* newPieces = [[NSMutableArray alloc] init]; + for (NSInteger i = self.pieceNum; i < self.pieces.count - 1; i++) { + [newPieces addObject:[self.pieces objectAtIndex:i]]; + } + return [[FPath alloc] initWithPieces:newPieces andPieceNum:0]; + } +} + +- (FPath *) child:(FPath *)childPathObj { + NSMutableArray* newPieces = [[NSMutableArray alloc] init]; + for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) { + [newPieces addObject:[self.pieces objectAtIndex:i]]; + } + + for (NSInteger i = childPathObj.pieceNum; i < childPathObj.pieces.count; i++) { + [newPieces addObject:[childPathObj.pieces objectAtIndex:i]]; + } + + return [[FPath alloc] initWithPieces:newPieces andPieceNum:0]; +} + +- (FPath *)childFromString:(NSString *)childPath { + NSMutableArray* newPieces = [[NSMutableArray alloc] init]; + for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) { + [newPieces addObject:[self.pieces objectAtIndex:i]]; + } + + NSArray *pathPieces = [childPath componentsSeparatedByString:@"/"]; + for (unsigned int i = 0; i < pathPieces.count; i++) { + NSString *piece = [pathPieces objectAtIndex:i]; + if (piece.length > 0) { + [newPieces addObject:piece]; + } + } + + return [[FPath alloc] initWithPieces:newPieces andPieceNum:0]; +} + +/** +* @return True if there are no segments in this path +*/ +- (BOOL) isEmpty { + return self.pieceNum >= self.pieces.count; +} + +/** +* @return Singleton to represent an empty path +*/ ++ (FPath *) empty { + static dispatch_once_t oneEmptyPath; + static FPath *emptyPath; + dispatch_once(&oneEmptyPath, ^{ + emptyPath = [[FPath alloc] initWith:@""]; + }); + return emptyPath; +} + +- (BOOL) contains:(FPath *)other { + if (self.length > other.length) { + return NO; + } + + NSInteger i = self.pieceNum; + NSInteger j = other.pieceNum; + while (i < self.pieces.count) { + NSString* thisSeg = [self.pieces objectAtIndex:i]; + NSString* otherSeg = [other.pieces objectAtIndex:j]; + if (![thisSeg isEqualToString:otherSeg]) { + return NO; + } + ++i; + ++j; + } + return YES; +} + +- (void) enumerateComponentsUsingBlock:(void (^)(NSString *, BOOL *))block { + BOOL stop = NO; + for (NSInteger i = self.pieceNum; !stop && i < self.pieces.count; i++) { + block(self.pieces[i], &stop); + } +} + +- (NSComparisonResult) compare:(FPath *)other { + NSInteger myCount = self.pieces.count; + NSInteger otherCount = other.pieces.count; + for (NSInteger i = self.pieceNum, j = other.pieceNum; i < myCount && j < otherCount; i++, j++) { + NSComparisonResult comparison = [FUtilities compareKey:self.pieces[i] toKey:other.pieces[j]]; + if (comparison != NSOrderedSame) { + return comparison; + } + } + if (self.length < other.length) { + return NSOrderedAscending; + } else if (other.length < self.length) { + return NSOrderedDescending; + } else { + NSAssert(self.length == other.length, @"Paths must be the same lengths"); + return NSOrderedSame; + } +} + +/** +* @return YES if paths are the same +*/ +- (BOOL)isEqual:(id)other +{ + if (other == self) { + return YES; + } + if (!other || ![other isKindOfClass:[self class]]) { + return NO; + } + FPath *otherPath = (FPath *)other; + if (self.length != otherPath.length) { + return NO; + } + for (NSUInteger i = self.pieceNum, j = otherPath.pieceNum; i < self.pieces.count; i++, j++) { + if (![self.pieces[i] isEqualToString:otherPath.pieces[j]]) { + return NO; + } + } + return YES; +} + +- (NSUInteger) hash { + NSUInteger hashCode = 0; + for (NSInteger i = self.pieceNum; i < self.pieces.count; i++) { + hashCode = hashCode * 37 + [self.pieces[i] hash]; + } + return hashCode; +} + +@end diff --git a/Firebase/Database/Core/Utilities/FTree.h b/Firebase/Database/Core/Utilities/FTree.h new file mode 100644 index 0000000..8528526 --- /dev/null +++ b/Firebase/Database/Core/Utilities/FTree.h @@ -0,0 +1,48 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FTreeNode.h" +#import "FPath.h" + +@interface FTree : NSObject + +- (id)init; +- (id)initWithName:(NSString*)aName withParent:(FTree *)aParent withNode:(FTreeNode *)aNode; + +- (FTree *) subTree:(FPath*)path; +- (id)getValue; +- (void)setValue:(id)value; +- (void) clear; +- (BOOL) hasChildren; +- (BOOL) isEmpty; +- (void) forEachChildMutationSafe:(void (^)(FTree *))action; +- (void) forEachChild:(void (^)(FTree *))action; +- (void) forEachDescendant:(void (^)(FTree *))action; +- (void) forEachDescendant:(void (^)(FTree *))action includeSelf:(BOOL)incSelf childrenFirst:(BOOL)childFirst; +- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action; +- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action includeSelf:(BOOL)incSelf; +- (void) forEachImmediateDescendantWithValue:(void (^)(FTree *))action; +- (BOOL) valueExistsAtOrAbove:(FPath *)path; +- (FPath *)path; +- (void) updateParents; +- (void) updateChild:(NSString*)childName withNode:(FTree *)child; + +@property (nonatomic, strong) NSString* name; +@property (nonatomic, strong) FTree* parent; +@property (nonatomic, strong) FTreeNode* node; + +@end diff --git a/Firebase/Database/Core/Utilities/FTree.m b/Firebase/Database/Core/Utilities/FTree.m new file mode 100644 index 0000000..8576ffb --- /dev/null +++ b/Firebase/Database/Core/Utilities/FTree.m @@ -0,0 +1,183 @@ +/* + * 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 "FTree.h" +#import "FTreeNode.h" +#import "FPath.h" +#import "FUtilities.h" + +@implementation FTree + +@synthesize name; +@synthesize parent; +@synthesize node; + +- (id)init +{ + self = [super init]; + if (self) { + self.name = @""; + self.parent = nil; + self.node = [[FTreeNode alloc] init]; + } + return self; +} + + +- (id)initWithName:(NSString*)aName withParent:(FTree *)aParent withNode:(FTreeNode *)aNode +{ + self = [super init]; + if (self) { + self.name = aName != nil ? aName : @""; + self.parent = aParent != nil ? aParent : nil; + self.node = aNode != nil ? aNode : [[FTreeNode alloc] init]; + } + return self; +} + +- (FTree *) subTree:(FPath*)path { + FTree* child = self; + NSString* next = [path getFront]; + while(next != nil) { + FTreeNode* childNode = child.node.children[next]; + if (childNode == nil) { + childNode = [[FTreeNode alloc] init]; + } + child = [[FTree alloc] initWithName:next withParent:child withNode:childNode]; + path = [path popFront]; + next = [path getFront]; + } + return child; +} + +- (id)getValue { + return self.node.value; +} + +- (void)setValue:(id)value { + self.node.value = value; + [self updateParents]; +} + +- (void) clear { + self.node.value = nil; + [self.node.children removeAllObjects]; + self.node.childCount = 0; + [self updateParents]; +} + +- (BOOL) hasChildren { + return self.node.childCount > 0; +} + +- (BOOL) isEmpty { + return [self getValue] == nil && ![self hasChildren]; +} + +- (void) forEachChild:(void (^)(FTree *))action { + for(NSString* key in self.node.children) { + action([[FTree alloc] initWithName:key withParent:self withNode:[self.node.children objectForKey:key]]); + } +} + +- (void) forEachChildMutationSafe:(void (^)(FTree *))action { + for(NSString* key in [self.node.children copy]) { + action([[FTree alloc] initWithName:key withParent:self withNode:[self.node.children objectForKey:key]]); + } +} + +- (void) forEachDescendant:(void (^)(FTree *))action { + [self forEachDescendant:action includeSelf:NO childrenFirst:NO]; +} + +- (void) forEachDescendant:(void (^)(FTree *))action includeSelf:(BOOL)incSelf childrenFirst:(BOOL)childFirst { + if(incSelf && !childFirst) { + action(self); + } + + [self forEachChild:^(FTree* child) { + [child forEachDescendant:action includeSelf:YES childrenFirst:childFirst]; + }]; + + if(incSelf && childFirst) { + action(self); + } +} + +- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action { + return [self forEachAncestor:action includeSelf:NO]; +} + +- (BOOL) forEachAncestor:(BOOL (^)(FTree *))action includeSelf:(BOOL)incSelf { + FTree* aNode = (incSelf) ? self : self.parent; + while(aNode != nil) { + if(action(aNode)) { + return YES; + } + aNode = aNode.parent; + } + return NO; +} + +- (void) forEachImmediateDescendantWithValue:(void (^)(FTree *))action { + [self forEachChild:^(FTree * child) { + if([child getValue] != nil) { + action(child); + } + else { + [child forEachImmediateDescendantWithValue:action]; + } + }]; +} + +- (BOOL) valueExistsAtOrAbove:(FPath *)path { + FTreeNode* aNode = self.node; + while(aNode != nil) { + if(aNode.value != nil) { + return YES; + } + aNode = [aNode.children objectForKey:path.getFront]; + path = [path popFront]; + } + // XXX Check with Michael if this is correct; deviates from JS. + return NO; +} + +- (FPath *)path { + return [[FPath alloc] initWith:(self.parent == nil) ? self.name : + [NSString stringWithFormat:@"%@/%@", [self.parent path], self.name ]]; +} + +- (void) updateParents { + [self.parent updateChild:self.name withNode:self]; +} + +- (void) updateChild:(NSString*)childName withNode:(FTree *)child { + BOOL childEmpty = [child isEmpty]; + BOOL childExists = self.node.children[childName] != nil; + if(childEmpty && childExists) { + [self.node.children removeObjectForKey:childName]; + self.node.childCount = self.node.childCount - 1; + [self updateParents]; + } + else if(!childEmpty && !childExists) { + [self.node.children setObject:child.node forKey:childName]; + self.node.childCount = self.node.childCount + 1; + [self updateParents]; + } +} + +@end diff --git a/Firebase/Database/Core/Utilities/FTreeNode.h b/Firebase/Database/Core/Utilities/FTreeNode.h new file mode 100644 index 0000000..7e3497e --- /dev/null +++ b/Firebase/Database/Core/Utilities/FTreeNode.h @@ -0,0 +1,25 @@ +/* + * 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 <Foundation/Foundation.h> + +@interface FTreeNode : NSObject + +@property (nonatomic, strong) NSMutableDictionary* children; +@property (nonatomic, readwrite, assign) int childCount; +@property (nonatomic, strong) id value; + +@end diff --git a/Firebase/Database/Core/Utilities/FTreeNode.m b/Firebase/Database/Core/Utilities/FTreeNode.m new file mode 100644 index 0000000..9cba9c5 --- /dev/null +++ b/Firebase/Database/Core/Utilities/FTreeNode.m @@ -0,0 +1,36 @@ +/* + * 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 "FTreeNode.h" + +@implementation FTreeNode + +@synthesize children; +@synthesize childCount; +@synthesize value; + +- (id)init +{ + self = [super init]; + if (self) { + self.children = [[NSMutableDictionary alloc] init]; + self.childCount = 0; + self.value = nil; + } + return self; +} + +@end diff --git a/Firebase/Database/Core/View/FCacheNode.h b/Firebase/Database/Core/View/FCacheNode.h new file mode 100644 index 0000000..b23869c --- /dev/null +++ b/Firebase/Database/Core/View/FCacheNode.h @@ -0,0 +1,44 @@ +/* + * 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 <Foundation/Foundation.h> + +@protocol FNode; +@class FIndexedNode; +@class FPath; + +/** +* A cache node only stores complete children. Additionally it holds a flag whether the node can be considered fully +* initialized in the sense that we know at one point in time, this represented a valid state of the world, e.g. +* initialized with data from the server, or a complete overwrite by the client. It is not necessarily complete because +* it may have been from a tagged query. The filtered flag also tracks whether a node potentially had children removed +* due to a filter. +*/ +@interface FCacheNode : NSObject + +- (id) initWithIndexedNode:(FIndexedNode *)indexedNode + isFullyInitialized:(BOOL)fullyInitialized + isFiltered:(BOOL)filtered; + +- (BOOL) isCompleteForPath:(FPath *)path; +- (BOOL) isCompleteForChild:(NSString *)childKey; + +@property (nonatomic, readonly) BOOL isFullyInitialized; +@property (nonatomic, readonly) BOOL isFiltered; +@property (nonatomic, strong, readonly) FIndexedNode *indexedNode; +@property (nonatomic, strong, readonly) id<FNode> node; + +@end diff --git a/Firebase/Database/Core/View/FCacheNode.m b/Firebase/Database/Core/View/FCacheNode.m new file mode 100644 index 0000000..4767a25 --- /dev/null +++ b/Firebase/Database/Core/View/FCacheNode.m @@ -0,0 +1,60 @@ +/* + * 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 "FCacheNode.h" +#import "FNode.h" +#import "FPath.h" +#import "FEmptyNode.h" +#import "FIndexedNode.h" + +@interface FCacheNode () +@property (nonatomic, readwrite) BOOL isFullyInitialized; +@property (nonatomic, readwrite) BOOL isFiltered; +@property (nonatomic, strong, readwrite) FIndexedNode *indexedNode; +@end + +@implementation FCacheNode +- (id) initWithIndexedNode:(FIndexedNode *)indexedNode + isFullyInitialized:(BOOL)fullyInitialized + isFiltered:(BOOL)filtered +{ + self = [super init]; + if (self) { + self.indexedNode = indexedNode; + self.isFullyInitialized = fullyInitialized; + self.isFiltered = filtered; + } + return self; +} + +- (BOOL)isCompleteForPath:(FPath *)path { + if (path.isEmpty) { + return self.isFullyInitialized && !self.isFiltered; + } else { + NSString *childKey = [path getFront]; + return [self isCompleteForChild:childKey]; + } +} + +- (BOOL)isCompleteForChild:(NSString *)childKey { + return (self.isFullyInitialized && !self.isFiltered) || [self.node hasChild:childKey]; +} + +- (id<FNode>)node { + return self.indexedNode.node; +} + +@end diff --git a/Firebase/Database/Core/View/FCancelEvent.h b/Firebase/Database/Core/View/FCancelEvent.h new file mode 100644 index 0000000..38277f7 --- /dev/null +++ b/Firebase/Database/Core/View/FCancelEvent.h @@ -0,0 +1,30 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FEvent.h" + +@protocol FEventRegistration; + + +@interface FCancelEvent : NSObject<FEvent> + +- initWithEventRegistration:(id<FEventRegistration>)eventRegistration error:(NSError *)error path:(FPath *)path; + +@property (nonatomic, strong, readonly) NSError *error; +@property (nonatomic, strong, readonly) FPath *path; + +@end diff --git a/Firebase/Database/Core/View/FCancelEvent.m b/Firebase/Database/Core/View/FCancelEvent.m new file mode 100644 index 0000000..fb73f17 --- /dev/null +++ b/Firebase/Database/Core/View/FCancelEvent.m @@ -0,0 +1,55 @@ +/* + * 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 "FCancelEvent.h" +#import "FPath.h" +#import "FEventRegistration.h" + +@interface FCancelEvent () +@property (nonatomic, strong) id<FEventRegistration> eventRegistration; +@property (nonatomic, strong, readwrite) NSError *error; +@property (nonatomic, strong, readwrite) FPath *path; +@end + +@implementation FCancelEvent + +@synthesize eventRegistration; +@synthesize error; +@synthesize path; + +- (id)initWithEventRegistration:(id <FEventRegistration>)registration error:(NSError *)anError path:(FPath *)aPath { + self = [super init]; + if (self) { + self.eventRegistration = registration; + self.error = anError; + self.path = aPath; + } + return self; +} + +- (void) fireEventOnQueue:(dispatch_queue_t)queue { + [self.eventRegistration fireEvent:self queue:queue]; +} + +- (BOOL) isCancelEvent { + return YES; +} + +- (NSString *) description { + return [NSString stringWithFormat:@"%@: cancel", self.path]; +} + +@end diff --git a/Firebase/Database/Core/View/FChange.h b/Firebase/Database/Core/View/FChange.h new file mode 100644 index 0000000..d728fe0 --- /dev/null +++ b/Firebase/Database/Core/View/FChange.h @@ -0,0 +1,38 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FIRDatabaseReference.h" +#import "FNode.h" +#import "FIndexedNode.h" + +@interface FChange : NSObject + +@property (nonatomic, readonly) FIRDataEventType type; +@property (nonatomic, strong, readonly) FIndexedNode *indexedNode; +@property (nonatomic, strong, readonly) NSString *childKey; +@property (nonatomic, strong, readonly) NSString *prevKey; +@property (nonatomic, strong, readonly) FIndexedNode *oldIndexedNode; + +- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode; +- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode childKey:(NSString *)childKey; +- (id)initWithType:(FIRDataEventType)type + indexedNode:(FIndexedNode *)indexedNode + childKey:(NSString *)childKey + oldIndexedNode:(FIndexedNode *)oldIndexedNode; + +- (FChange *) changeWithPrevKey:(NSString *)prevKey; +@end diff --git a/Firebase/Database/Core/View/FChange.m b/Firebase/Database/Core/View/FChange.m new file mode 100644 index 0000000..893fce4 --- /dev/null +++ b/Firebase/Database/Core/View/FChange.m @@ -0,0 +1,65 @@ +/* + * 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 "FChange.h" + +@interface FChange () + +@property (nonatomic, strong, readwrite) NSString *prevKey; + +@end + +@implementation FChange + +- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode +{ + return [self initWithType:type indexedNode:indexedNode childKey:nil oldIndexedNode:nil]; +} + +- (id)initWithType:(FIRDataEventType)type indexedNode:(FIndexedNode *)indexedNode childKey:(NSString *)childKey +{ + return [self initWithType:type indexedNode:indexedNode childKey:childKey oldIndexedNode:nil]; +} + +- (id)initWithType:(FIRDataEventType)type + indexedNode:(FIndexedNode *)indexedNode + childKey:(NSString *)childKey + oldIndexedNode:(FIndexedNode *)oldIndexedNode +{ + self = [super init]; + if (self != nil) { + self->_type = type; + self->_indexedNode = indexedNode; + self->_childKey = childKey; + self->_oldIndexedNode = oldIndexedNode; + } + return self; +} + +- (FChange *) changeWithPrevKey:(NSString *)prevKey { + FChange *newChange = [[FChange alloc] initWithType:self.type + indexedNode:self.indexedNode + childKey:self.childKey + oldIndexedNode:self.oldIndexedNode]; + newChange.prevKey = prevKey; + return newChange; +} + +- (NSString *) description { + return [NSString stringWithFormat:@"event: %d, data: %@", (int)self.type, [self.indexedNode.node val]]; +} + +@end diff --git a/Firebase/Database/Core/View/FChildEventRegistration.h b/Firebase/Database/Core/View/FChildEventRegistration.h new file mode 100644 index 0000000..8da0b8f --- /dev/null +++ b/Firebase/Database/Core/View/FChildEventRegistration.h @@ -0,0 +1,37 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FEventRegistration.h" +#import "FTypedefs.h" + +@class FRepo; + +@interface FChildEventRegistration : NSObject <FEventRegistration> + +- (id) initWithRepo:(FRepo *)repo + handle:(FIRDatabaseHandle)fHandle + callbacks:(NSDictionary *)callbackBlocks + cancelCallback:(fbt_void_nserror)cancelCallbackBlock; + +/** +* Maps FIRDataEventType (as NSNumber) to fbt_void_datasnapshot_nsstring +*/ +@property (nonatomic, copy, readonly) NSDictionary *callbacks; +@property (nonatomic, copy, readonly) fbt_void_nserror cancelCallback; +@property (nonatomic, readonly) FIRDatabaseHandle handle; + +@end diff --git a/Firebase/Database/Core/View/FChildEventRegistration.m b/Firebase/Database/Core/View/FChildEventRegistration.m new file mode 100644 index 0000000..6308a90 --- /dev/null +++ b/Firebase/Database/Core/View/FChildEventRegistration.m @@ -0,0 +1,92 @@ +/* + * 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 "FChildEventRegistration.h" +#import "FIRDatabaseQuery_Private.h" +#import "FQueryParams.h" +#import "FQuerySpec.h" +#import "FIRDataSnapshot_Private.h" +#import "FDataEvent.h" +#import "FCancelEvent.h" + +@interface FChildEventRegistration () +@property (nonatomic, strong) FRepo *repo; +@property (nonatomic, copy, readwrite) NSDictionary *callbacks; +@property (nonatomic, copy, readwrite) fbt_void_nserror cancelCallback; +@property (nonatomic, readwrite) FIRDatabaseHandle handle; +@end + +@implementation FChildEventRegistration + +- (id)initWithRepo:(id)repo handle:(FIRDatabaseHandle)fHandle callbacks:(NSDictionary *)callbackBlocks cancelCallback:(fbt_void_nserror)cancelCallbackBlock { + self = [super init]; + if (self) { + self.repo = repo; + self.handle = fHandle; + self.callbacks = callbackBlocks; + self.cancelCallback = cancelCallbackBlock; + } + return self; +} + +- (BOOL) responseTo:(FIRDataEventType)eventType { + return self.callbacks != nil && [self.callbacks objectForKey:[NSNumber numberWithInteger:eventType]] != nil; +} + +- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query { + FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:[query.path childFromString:change.childKey]]; + FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode]; + + FDataEvent *eventData = [[FDataEvent alloc] initWithEventType:change.type eventRegistration:self + dataSnapshot:snapshot prevName:change.prevKey]; + return eventData; +} + +- (void) fireEvent:(id <FEvent>)event queue:(dispatch_queue_t)queue { + if ([event isCancelEvent]) { + FCancelEvent *cancelEvent = event; + FFLog(@"I-RDB061001", @"Raising cancel value event on %@", event.path); + NSAssert(self.cancelCallback != nil, @"Raising a cancel event on a listener with no cancel callback"); + dispatch_async(queue, ^{ + self.cancelCallback(cancelEvent.error); + }); + } else if (self.callbacks != nil) { + FDataEvent *dataEvent = event; + FFLog(@"I-RDB061002", @"Raising event callback (%ld) on %@", (long)dataEvent.eventType, dataEvent.path); + fbt_void_datasnapshot_nsstring callback = [self.callbacks objectForKey:[NSNumber numberWithInteger:dataEvent.eventType]]; + + if (callback != nil) { + dispatch_async(queue, ^{ + callback(dataEvent.snapshot, dataEvent.prevName); + }); + } + } +} + +- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path { + if (self.cancelCallback != nil) { + return [[FCancelEvent alloc] initWithEventRegistration:self error:error path:path]; + } else { + return nil; + } +} + +- (BOOL) matches:(id<FEventRegistration>)other { + return self.handle == NSNotFound || other.handle == NSNotFound || self.handle == other.handle; +} + + +@end diff --git a/Firebase/Database/Core/View/FDataEvent.h b/Firebase/Database/Core/View/FDataEvent.h new file mode 100644 index 0000000..da90b03 --- /dev/null +++ b/Firebase/Database/Core/View/FDataEvent.h @@ -0,0 +1,39 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FIRDataSnapshot.h" +#import "FIRDatabaseReference.h" +#import "FTupleUserCallback.h" +#import "FEvent.h" + +@protocol FEventRegistration; +@protocol FIndex; + +@interface FDataEvent : NSObject<FEvent> + +- initWithEventType:(FIRDataEventType)type eventRegistration:(id<FEventRegistration>)eventRegistration + dataSnapshot:(FIRDataSnapshot *)dataSnapshot; +- initWithEventType:(FIRDataEventType)type eventRegistration:(id<FEventRegistration>)eventRegistration + dataSnapshot:(FIRDataSnapshot *)snapshot prevName:(NSString *)prevName; + + +@property (nonatomic, strong, readonly) id<FEventRegistration> eventRegistration; +@property (nonatomic, strong, readonly) FIRDataSnapshot * snapshot; +@property (nonatomic, strong, readonly) NSString* prevName; +@property (nonatomic, readonly) FIRDataEventType eventType; + +@end diff --git a/Firebase/Database/Core/View/FDataEvent.m b/Firebase/Database/Core/View/FDataEvent.m new file mode 100644 index 0000000..6c97faf --- /dev/null +++ b/Firebase/Database/Core/View/FDataEvent.m @@ -0,0 +1,74 @@ +/* + * 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 "FDataEvent.h" +#import "FEventRegistration.h" +#import "FIndex.h" +#import "FIRDatabaseQuery_Private.h" + +@interface FDataEvent () +@property (nonatomic, strong, readwrite) id<FEventRegistration> eventRegistration; +@property (nonatomic, strong, readwrite) FIRDataSnapshot *snapshot; +@property (nonatomic, strong, readwrite) NSString *prevName; +@property (nonatomic, readwrite) FIRDataEventType eventType; +@end + +@implementation FDataEvent + +@synthesize eventRegistration; +@synthesize snapshot; +@synthesize prevName; +@synthesize eventType; + +- (id)initWithEventType:(FIRDataEventType)type eventRegistration:(id <FEventRegistration>)registration dataSnapshot:(FIRDataSnapshot *)dataSnapshot { + return [self initWithEventType:type eventRegistration:registration dataSnapshot:dataSnapshot prevName:nil]; +} + +- (id)initWithEventType:(FIRDataEventType)type eventRegistration:(id <FEventRegistration>)registration dataSnapshot:(FIRDataSnapshot *)dataSnapshot prevName:(NSString *)previousName { + self = [super init]; + if (self) { + self.eventRegistration = registration; + self.snapshot = dataSnapshot; + self.prevName = previousName; + self.eventType = type; + } + return self; +} + +- (FPath *) path { + // Used for logging, so delay calculation + FIRDatabaseReference *ref = self.snapshot.ref; + if (self.eventType == FIRDataEventTypeValue) { + return ref.path; + } else { + return ref.parent.path; + } +} + +- (void) fireEventOnQueue:(dispatch_queue_t)queue { + [self.eventRegistration fireEvent:self queue:queue]; +} + +- (BOOL) isCancelEvent { + return NO; +} + + +- (NSString *) description { + return [NSString stringWithFormat:@"event %d, data: %@", (int) eventType, [snapshot value]]; +} + +@end diff --git a/Firebase/Database/Core/View/FEvent.h b/Firebase/Database/Core/View/FEvent.h new file mode 100644 index 0000000..6b9e31a --- /dev/null +++ b/Firebase/Database/Core/View/FEvent.h @@ -0,0 +1,27 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FIRDataEventType.h" + +@class FPath; + +@protocol FEvent <NSObject> +- (FPath *) path; +- (void) fireEventOnQueue:(dispatch_queue_t)queue; +- (BOOL) isCancelEvent; +- (NSString *) description; +@end diff --git a/Firebase/Database/Core/View/FEventRaiser.h b/Firebase/Database/Core/View/FEventRaiser.h new file mode 100644 index 0000000..01a0130 --- /dev/null +++ b/Firebase/Database/Core/View/FEventRaiser.h @@ -0,0 +1,35 @@ +/* + * 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 "FTypedefs.h" + +@class FPath; +@class FRepo; +@class FIRDatabaseConfig; + +/** +* Left as instance methods rather than class methods so that we could potentially callback on different queues for different repos. +* This is semi-parallel to JS's FEventQueue +*/ +@interface FEventRaiser : NSObject + +- (id)initWithQueue:(dispatch_queue_t)queue; + +- (void) raiseEvents:(NSArray *)eventDataList; +- (void) raiseCallback:(fbt_void_void)callback; +- (void) raiseCallbacks:(NSArray *)callbackList; + +@end diff --git a/Firebase/Database/Core/View/FEventRaiser.m b/Firebase/Database/Core/View/FEventRaiser.m new file mode 100644 index 0000000..94a0907 --- /dev/null +++ b/Firebase/Database/Core/View/FEventRaiser.m @@ -0,0 +1,72 @@ +/* + * 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 "FEventRaiser.h" +#import "FDataEvent.h" +#import "FTypedefs.h" +#import "FUtilities.h" +#import "FTupleUserCallback.h" +#import "FRepo.h" +#import "FRepoManager.h" + +@interface FEventRaiser () + +@property (nonatomic, strong) dispatch_queue_t queue; + +@end + +/** +* This class exists for symmetry with other clients, but since events are async, we don't need to do the complicated +* stuff the JS client does to preserve event order. +*/ +@implementation FEventRaiser + +- (id)init { + [NSException raise:NSInternalInconsistencyException format:@"Can't use default constructor"]; + return nil; +} + +- (id)initWithQueue:(dispatch_queue_t)queue { + self = [super init]; + if (self != nil) { + self->_queue = queue; + } + return self; +} + +- (void) raiseEvents:(NSArray *)eventDataList { + for (id<FEvent> event in eventDataList) { + [event fireEventOnQueue:self.queue]; + } +} + +- (void) raiseCallback:(fbt_void_void)callback { + dispatch_async(self.queue, callback); +} + +- (void) raiseCallbacks:(NSArray *)callbackList { + for (fbt_void_void callback in callbackList) { + dispatch_async(self.queue, callback); + } +} + ++ (void) raiseCallbacks:(NSArray *)callbackList queue:(dispatch_queue_t)queue { + for (fbt_void_void callback in callbackList) { + dispatch_async(queue, callback); + } +} + +@end diff --git a/Firebase/Database/Core/View/FEventRegistration.h b/Firebase/Database/Core/View/FEventRegistration.h new file mode 100644 index 0000000..5b845ac --- /dev/null +++ b/Firebase/Database/Core/View/FEventRegistration.h @@ -0,0 +1,36 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FChange.h" +#import "FIRDataEventType.h" + +@protocol FEvent; +@class FDataEvent; +@class FCancelEvent; +@class FQuerySpec; + +@protocol FEventRegistration <NSObject> +- (BOOL) responseTo:(FIRDataEventType)eventType; +- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query; +- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue; +- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path; +/** +* Used to figure out what event registration match the event registration that needs to be removed. +*/ +- (BOOL) matches:(id<FEventRegistration>)other; +@property (nonatomic, readonly) FIRDatabaseHandle handle; +@end diff --git a/Firebase/Database/Core/View/FKeepSyncedEventRegistration.h b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.h new file mode 100644 index 0000000..669e012 --- /dev/null +++ b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.h @@ -0,0 +1,28 @@ +/* + * 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 <Foundation/Foundation.h> + +#import "FEventRegistration.h" + +/** + * A singleton event registration to mark a query as keep synced + */ +@interface FKeepSyncedEventRegistration : NSObject<FEventRegistration> + ++ (FKeepSyncedEventRegistration *)instance; + +@end diff --git a/Firebase/Database/Core/View/FKeepSyncedEventRegistration.m b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.m new file mode 100644 index 0000000..806d54f --- /dev/null +++ b/Firebase/Database/Core/View/FKeepSyncedEventRegistration.m @@ -0,0 +1,64 @@ +/* + * 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 "FKeepSyncedEventRegistration.h" + +@interface FKeepSyncedEventRegistration () + +@end + +@implementation FKeepSyncedEventRegistration + ++ (FKeepSyncedEventRegistration *)instance { + static dispatch_once_t onceToken; + static FKeepSyncedEventRegistration *keepSynced; + dispatch_once(&onceToken, ^{ + keepSynced = [[FKeepSyncedEventRegistration alloc] init]; + }); + return keepSynced; +} + +- (BOOL) responseTo:(FIRDataEventType)eventType { + return NO; +} + +- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query { + [NSException raise:NSInternalInconsistencyException format:@"Should never create event for FKeepSyncedEventRegistration"]; + return nil; +} + +- (void) fireEvent:(id<FEvent>)event queue:(dispatch_queue_t)queue { + [NSException raise:NSInternalInconsistencyException format:@"Should never raise event for FKeepSyncedEventRegistration"]; +} + +- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path { + // Don't create cancel events.... + return nil; +} + +- (FIRDatabaseHandle) handle { + // TODO[offline]: returning arbitray, can't return NSNotFound since that is used to match other event registrations + // We should really redo this to match on different kind of events (single observer, all observers, cancelled) + // rather than on a NSNotFound handle... + return NSNotFound - 1; +} + +- (BOOL) matches:(id<FEventRegistration>)other { + // Only matches singleton instance + return self == other; +} + +@end diff --git a/Firebase/Database/Core/View/FValueEventRegistration.h b/Firebase/Database/Core/View/FValueEventRegistration.h new file mode 100644 index 0000000..1220c60 --- /dev/null +++ b/Firebase/Database/Core/View/FValueEventRegistration.h @@ -0,0 +1,34 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FEventRegistration.h" +#import "FTypedefs.h" + +@class FRepo; + +@interface FValueEventRegistration : NSObject<FEventRegistration> + +- (id) initWithRepo:(FRepo *)repo + handle:(FIRDatabaseHandle)fHandle + callback:(fbt_void_datasnapshot)callbackBlock + cancelCallback:(fbt_void_nserror)cancelCallbackBlock; + +@property (nonatomic, copy, readonly) fbt_void_datasnapshot callback; +@property (nonatomic, copy, readonly) fbt_void_nserror cancelCallback; +@property (nonatomic, readonly) FIRDatabaseHandle handle; + +@end diff --git a/Firebase/Database/Core/View/FValueEventRegistration.m b/Firebase/Database/Core/View/FValueEventRegistration.m new file mode 100644 index 0000000..d351a4b --- /dev/null +++ b/Firebase/Database/Core/View/FValueEventRegistration.m @@ -0,0 +1,89 @@ +/* + * 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 "FValueEventRegistration.h" +#import "FIRDatabaseQuery_Private.h" +#import "FQueryParams.h" +#import "FQuerySpec.h" +#import "FIRDataSnapshot_Private.h" +#import "FCancelEvent.h" +#import "FDataEvent.h" + +@interface FValueEventRegistration () +@property (nonatomic, strong) FRepo* repo; +@property (nonatomic, copy, readwrite) fbt_void_datasnapshot callback; +@property (nonatomic, copy, readwrite) fbt_void_nserror cancelCallback; +@property (nonatomic, readwrite) FIRDatabaseHandle handle; +@end + +@implementation FValueEventRegistration + +- (id) initWithRepo:(FRepo *)repo + handle:(FIRDatabaseHandle)fHandle + callback:(fbt_void_datasnapshot)callbackBlock + cancelCallback:(fbt_void_nserror)cancelCallbackBlock { + self = [super init]; + if (self) { + self.repo = repo; + self.handle = fHandle; + self.callback = callbackBlock; + self.cancelCallback = cancelCallbackBlock; + } + return self; +} + +- (BOOL) responseTo:(FIRDataEventType)eventType { + return eventType == FIRDataEventTypeValue; +} + +- (FDataEvent *) createEventFrom:(FChange *)change query:(FQuerySpec *)query { + FIRDatabaseReference *ref = [[FIRDatabaseReference alloc] initWithRepo:self.repo path:query.path]; + FIRDataSnapshot *snapshot = [[FIRDataSnapshot alloc] initWithRef:ref indexedNode:change.indexedNode]; + FDataEvent *eventData = [[FDataEvent alloc] initWithEventType:FIRDataEventTypeValue eventRegistration:self + dataSnapshot:snapshot]; + return eventData; +} + +- (void) fireEvent:(id <FEvent>)event queue:(dispatch_queue_t)queue { + if ([event isCancelEvent]) { + FCancelEvent *cancelEvent = event; + FFLog(@"I-RDB065001", @"Raising cancel value event on %@", event.path); + NSAssert(self.cancelCallback != nil, @"Raising a cancel event on a listener with no cancel callback"); + dispatch_async(queue, ^{ + self.cancelCallback(cancelEvent.error); + }); + } else if (self.callback != nil) { + FDataEvent *dataEvent = event; + FFLog(@"I-RDB065002", @"Raising value event on %@", dataEvent.snapshot.key); + dispatch_async(queue, ^{ + self.callback(dataEvent.snapshot); + }); + } +} + +- (FCancelEvent *) createCancelEventFromError:(NSError *)error path:(FPath *)path { + if (self.cancelCallback != nil) { + return [[FCancelEvent alloc] initWithEventRegistration:self error:error path:path]; + } else { + return nil; + } +} + +- (BOOL) matches:(id<FEventRegistration>)other { + return self.handle == NSNotFound || other.handle == NSNotFound || self.handle == other.handle; +} + +@end diff --git a/Firebase/Database/Core/View/FView.h b/Firebase/Database/Core/View/FView.h new file mode 100644 index 0000000..2d0761a --- /dev/null +++ b/Firebase/Database/Core/View/FView.h @@ -0,0 +1,53 @@ +/* + * 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 <Foundation/Foundation.h> + +@protocol FNode; +@protocol FOperation; +@protocol FEventRegistration; +@class FWriteTreeRef; +@class FQuerySpec; +@class FChange; +@class FPath; +@class FViewCache; + +@interface FViewOperationResult : NSObject + +@property (nonatomic, strong, readonly) NSArray* changes; +@property (nonatomic, strong, readonly) NSArray* events; + +@end + + +@interface FView : NSObject + +@property (nonatomic, strong, readonly) FQuerySpec *query; + +- (id) initWithQuery:(FQuerySpec *)query initialViewCache:(FViewCache *)initialViewCache; + +- (id<FNode>) eventCache; +- (id<FNode>) serverCache; +- (id<FNode>) completeServerCacheFor:(FPath*)path; +- (BOOL) isEmpty; + +- (void) addEventRegistration:(id<FEventRegistration>)eventRegistration; +- (NSArray *) removeEventRegistration:(id<FEventRegistration>)eventRegistration cancelError:(NSError *)cancelError; + +- (FViewOperationResult *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache; +- (NSArray *) initialEvents:(id<FEventRegistration>)registration; + +@end diff --git a/Firebase/Database/Core/View/FView.m b/Firebase/Database/Core/View/FView.m new file mode 100644 index 0000000..1aea4d7 --- /dev/null +++ b/Firebase/Database/Core/View/FView.m @@ -0,0 +1,223 @@ +/* + * 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 "FView.h" +#import "FNode.h" +#import "FWriteTreeRef.h" +#import "FOperation.h" +#import "FIRDatabaseQuery.h" +#import "FIRDatabaseQuery_Private.h" +#import "FEventRegistration.h" +#import "FQueryParams.h" +#import "FQuerySpec.h" +#import "FViewCache.h" +#import "FPath.h" +#import "FEventGenerator.h" +#import "FOperationSource.h" +#import "FCancelEvent.h" +#import "FIndexedFilter.h" +#import "FCacheNode.h" +#import "FEmptyNode.h" +#import "FViewProcessor.h" +#import "FViewProcessorResult.h" +#import "FIndexedNode.h" + +@interface FViewOperationResult () + +@property (nonatomic, strong, readwrite) NSArray *changes; +@property (nonatomic, strong, readwrite) NSArray *events; + +@end + +@implementation FViewOperationResult + +- (id)initWithChanges:(NSArray *)changes events:(NSArray *)events { + self = [super init]; + if (self != nil) { + self->_changes = changes; + self->_events = events; + } + return self; +} + +@end + +/** +* A view represents a specific location and query that has 1 or more event registrations. +* +* It does several things: +* - Maintains the list of event registration for this location/query. +* - Maintains a cache of the data visible for this location/query. +* - Applies new operations (via applyOperation), updates the cache, and based on the event +* registrations returns the set of events to be raised. +*/ +@interface FView () + +@property (nonatomic, strong, readwrite) FQuerySpec *query; +@property (nonatomic, strong) FViewProcessor *processor; +@property (nonatomic, strong) FViewCache *viewCache; +@property (nonatomic, strong) NSMutableArray *eventRegistrations; +@property (nonatomic, strong) FEventGenerator *eventGenerator; + +@end + +@implementation FView +- (id) initWithQuery:(FQuerySpec *)query initialViewCache:(FViewCache *)initialViewCache { + self = [super init]; + if (self) { + self.query = query; + + FIndexedFilter *indexFilter = [[FIndexedFilter alloc] initWithIndex:query.index]; + id<FNodeFilter> filter = query.params.nodeFilter; + self.processor = [[FViewProcessor alloc] initWithFilter:filter]; + FCacheNode *initialServerCache = initialViewCache.cachedServerSnap; + FCacheNode *initialEventCache = initialViewCache.cachedEventSnap; + + // Don't filter server node with other filter than index, wait for tagged listen + FIndexedNode *emptyIndexedNode = [FIndexedNode indexedNodeWithNode:[FEmptyNode emptyNode] index:query.index]; + FIndexedNode *serverSnap = [indexFilter updateFullNode:emptyIndexedNode + withNewNode:initialServerCache.indexedNode + accumulator:nil]; + FIndexedNode *eventSnap = [filter updateFullNode:emptyIndexedNode + withNewNode:initialEventCache.indexedNode + accumulator:nil]; + FCacheNode *newServerCache = [[FCacheNode alloc] initWithIndexedNode:serverSnap + isFullyInitialized:initialServerCache.isFullyInitialized + isFiltered:indexFilter.filtersNodes]; + FCacheNode *newEventCache = [[FCacheNode alloc] initWithIndexedNode:eventSnap + isFullyInitialized:initialEventCache.isFullyInitialized + isFiltered:filter.filtersNodes]; + + self.viewCache = [[FViewCache alloc] initWithEventCache:newEventCache serverCache:newServerCache]; + + self.eventRegistrations = [[NSMutableArray alloc] init]; + + self.eventGenerator = [[FEventGenerator alloc] initWithQuery:query]; + } + + return self; +} + +- (id <FNode>) serverCache { + return self.viewCache.cachedServerSnap.node; +} + +- (id <FNode>) eventCache { + return self.viewCache.cachedEventSnap.node; +} + +- (id <FNode>) completeServerCacheFor:(FPath*)path { + id<FNode> cache = self.viewCache.completeServerSnap; + if (cache) { + // If this isn't a "loadsAllData" view, then cache isn't actually a complete cache and + // we need to see if it contains the child we're interested in. + if ([self.query loadsAllData] || + (!path.isEmpty && ![cache getImmediateChild:path.getFront].isEmpty)) { + return [cache getChild:path]; + } + } + return nil; +} + +- (BOOL) isEmpty { + return self.eventRegistrations.count == 0; +} + +- (void) addEventRegistration:(id <FEventRegistration>)eventRegistration { + [self.eventRegistrations addObject:eventRegistration]; +} + +/** +* @param eventRegistration If null, remove all callbacks. +* @param cancelError If a cancelError is provided, appropriate cancel events will be returned. +* @return Cancel events, if cancelError was provided. +*/ +- (NSArray *) removeEventRegistration:(id <FEventRegistration>)eventRegistration cancelError:(NSError *)cancelError { + NSMutableArray *cancelEvents = [[NSMutableArray alloc] init]; + if (cancelError != nil) { + NSAssert(eventRegistration == nil, @"A cancel should cancel all event registrations."); + FPath *path = self.query.path; + for (id <FEventRegistration> registration in self.eventRegistrations) { + FCancelEvent *maybeEvent = [registration createCancelEventFromError:cancelError path:path]; + if (maybeEvent) { + [cancelEvents addObject:maybeEvent]; + } + } + } + + if (eventRegistration) { + NSUInteger i = 0; + while (i < self.eventRegistrations.count) { + id<FEventRegistration> existing = self.eventRegistrations[i]; + if ([existing matches:eventRegistration]) { + [self.eventRegistrations removeObjectAtIndex:i]; + } else { + i++; + } + } + } else { + [self.eventRegistrations removeAllObjects]; + } + return cancelEvents; +} + +/** + * Applies the given Operation, updates our cache, and returns the appropriate events and changes + */ +- (FViewOperationResult *) applyOperation:(id <FOperation>)operation writesCache:(FWriteTreeRef *)writesCache serverCache:(id <FNode>)optCompleteServerCache { + if (operation.type == FOperationTypeMerge && operation.source.queryParams != nil) { + NSAssert(self.viewCache.completeServerSnap != nil, @"We should always have a full cache before handling merges"); + NSAssert(self.viewCache.completeEventSnap != nil, @"Missing event cache, even though we have a server cache"); + } + FViewCache *oldViewCache = self.viewCache; + FViewProcessorResult *result = [self.processor applyOperationOn:oldViewCache operation:operation writesCache:writesCache completeCache:optCompleteServerCache]; + + NSAssert(result.viewCache.cachedServerSnap.isFullyInitialized || !oldViewCache.cachedServerSnap.isFullyInitialized, @"Once a server snap is complete, it should never go back."); + + self.viewCache = result.viewCache; + NSArray *events = [self generateEventsForChanges:result.changes eventCache:result.viewCache.cachedEventSnap.indexedNode registration:nil]; + return [[FViewOperationResult alloc] initWithChanges:result.changes events:events]; +} + +- (NSArray *) initialEvents:(id<FEventRegistration>)registration { + FCacheNode *eventSnap = self.viewCache.cachedEventSnap; + NSMutableArray *initialChanges = [[NSMutableArray alloc] init]; + [eventSnap.indexedNode.node enumerateChildrenUsingBlock:^(NSString *key, id<FNode> node, BOOL *stop) { + FIndexedNode *indexed = [FIndexedNode indexedNodeWithNode:node]; + FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded indexedNode:indexed childKey:key]; + [initialChanges addObject:change]; + }]; + if (eventSnap.isFullyInitialized) { + FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeValue indexedNode:eventSnap.indexedNode]; + [initialChanges addObject:change]; + } + return [self generateEventsForChanges:initialChanges eventCache:eventSnap.indexedNode registration:registration]; +} + +- (NSArray *) generateEventsForChanges:(NSArray *)changes eventCache:(FIndexedNode *)eventCache registration:(id<FEventRegistration>)registration { + NSArray *registrations; + if (registration == nil) { + registrations = [[NSArray alloc] initWithArray:self.eventRegistrations]; + } else { + registrations = [[NSArray alloc] initWithObjects:registration, nil]; + } + return [self.eventGenerator generateEventsForChanges:changes eventCache:eventCache eventRegistrations:registrations]; +} + +- (NSString *) description { + return [NSString stringWithFormat:@"FView (%@)", self.query]; +} +@end diff --git a/Firebase/Database/Core/View/FViewCache.h b/Firebase/Database/Core/View/FViewCache.h new file mode 100644 index 0000000..4d01877 --- /dev/null +++ b/Firebase/Database/Core/View/FViewCache.h @@ -0,0 +1,35 @@ +#/* +* 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 <Foundation/Foundation.h> + +@protocol FNode; +@class FCacheNode; +@class FIndexedNode; + +@interface FViewCache : NSObject + +- (id) initWithEventCache:(FCacheNode *)eventCache serverCache:(FCacheNode *)serverCache; + +- (FViewCache *) updateEventSnap:(FIndexedNode *)eventSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered; +- (FViewCache *) updateServerSnap:(FIndexedNode *)serverSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered; + +@property (nonatomic, strong, readonly) FCacheNode *cachedEventSnap; +@property (nonatomic, strong, readonly) id<FNode> completeEventSnap; +@property (nonatomic, strong, readonly) FCacheNode *cachedServerSnap; +@property (nonatomic, strong, readonly) id<FNode> completeServerSnap; + +@end diff --git a/Firebase/Database/Core/View/FViewCache.m b/Firebase/Database/Core/View/FViewCache.m new file mode 100644 index 0000000..c6ec8b1 --- /dev/null +++ b/Firebase/Database/Core/View/FViewCache.m @@ -0,0 +1,61 @@ +/* + * 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 "FViewCache.h" +#import "FCacheNode.h" +#import "FNode.h" +#import "FEmptyNode.h" + +@interface FViewCache () +@property (nonatomic, strong, readwrite) FCacheNode *cachedEventSnap; +@property (nonatomic, strong, readwrite) FCacheNode *cachedServerSnap; +@end + +@implementation FViewCache + +- (id) initWithEventCache:(FCacheNode *)eventCache serverCache:(FCacheNode *)serverCache { + self = [super init]; + if (self) { + self.cachedEventSnap = eventCache; + self.cachedServerSnap = serverCache; + } + return self; +} + +- (FViewCache *) updateEventSnap:(FIndexedNode *)eventSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered { + FCacheNode *updatedEventCache = [[FCacheNode alloc] initWithIndexedNode:eventSnap + isFullyInitialized:complete + isFiltered:filtered]; + return [[FViewCache alloc] initWithEventCache:updatedEventCache serverCache:self.cachedServerSnap]; +} + +- (FViewCache *) updateServerSnap:(FIndexedNode *)serverSnap isComplete:(BOOL)complete isFiltered:(BOOL)filtered { + FCacheNode *updatedServerCache = [[FCacheNode alloc] initWithIndexedNode:serverSnap + isFullyInitialized:complete + isFiltered:filtered]; + return [[FViewCache alloc] initWithEventCache:self.cachedEventSnap serverCache:updatedServerCache]; +} + +- (id<FNode>) completeEventSnap { + return (self.cachedEventSnap.isFullyInitialized) ? self.cachedEventSnap.node : nil; +} + +- (id<FNode>) completeServerSnap { + return (self.cachedServerSnap.isFullyInitialized) ? self.cachedServerSnap.node : nil; +} + + +@end diff --git a/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h new file mode 100644 index 0000000..59b0a85 --- /dev/null +++ b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h @@ -0,0 +1,28 @@ +/* + * 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 <Foundation/Foundation.h> + +@class FChange; + + +@interface FChildChangeAccumulator : NSObject + +- (id) init; +- (void) trackChildChange:(FChange *)change; +- (NSArray *) changes; + +@end diff --git a/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m new file mode 100644 index 0000000..e43fd7c --- /dev/null +++ b/Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m @@ -0,0 +1,80 @@ +/* + * 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 "FChildChangeAccumulator.h" +#import "FChange.h" +#import "FIndex.h" + +@interface FChildChangeAccumulator () +@property (nonatomic, strong) NSMutableDictionary *changeMap; +@end + +@implementation FChildChangeAccumulator + +- (id) init { + self = [super init]; + if (self) { + self.changeMap = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void) trackChildChange:(FChange *)change { + FIRDataEventType type = change.type; + NSString *childKey = change.childKey; + NSAssert(type == FIRDataEventTypeChildAdded || type == FIRDataEventTypeChildChanged || type == FIRDataEventTypeChildRemoved, @"Only child changes supported for tracking."); + NSAssert(![change.childKey isEqualToString:@".priority"], @"Changes not tracked on priority"); + if (self.changeMap[childKey] != nil) { + FChange *oldChange = [self.changeMap objectForKey:childKey]; + FIRDataEventType oldType = oldChange.type; + if (type == FIRDataEventTypeChildAdded && oldType == FIRDataEventTypeChildRemoved) { + FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged + indexedNode:change.indexedNode + childKey:childKey + oldIndexedNode:oldChange.indexedNode]; + [self.changeMap setObject:newChange forKey:childKey]; + } else if (type == FIRDataEventTypeChildRemoved && oldType == FIRDataEventTypeChildAdded) { + [self.changeMap removeObjectForKey:childKey]; + } else if (type == FIRDataEventTypeChildRemoved && oldType == FIRDataEventTypeChildChanged) { + FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved + indexedNode:oldChange.oldIndexedNode + childKey:childKey]; + [self.changeMap setObject:newChange forKey:childKey]; + } else if (type == FIRDataEventTypeChildChanged && oldType == FIRDataEventTypeChildAdded) { + FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded + indexedNode:change.indexedNode + childKey:childKey]; + [self.changeMap setObject:newChange forKey:childKey]; + } else if (type == FIRDataEventTypeChildChanged && oldType == FIRDataEventTypeChildChanged) { + FChange *newChange = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged + indexedNode:change.indexedNode + childKey:childKey + oldIndexedNode:oldChange.oldIndexedNode]; + [self.changeMap setObject:newChange forKey:childKey]; + } else { + NSString *reason = [NSString stringWithFormat:@"Illegal combination of changes: %@ occurred after %@", change, oldChange]; + @throw [[NSException alloc] initWithName:@"FirebaseDatabaseInternalError" reason:reason userInfo:nil]; + } + } else { + [self.changeMap setObject:change forKey:childKey]; + } +} + +- (NSArray *) changes { + return [self.changeMap allValues]; +} + +@end diff --git a/Firebase/Database/Core/View/Filter/FCompleteChildSource.h b/Firebase/Database/Core/View/Filter/FCompleteChildSource.h new file mode 100644 index 0000000..4e99045 --- /dev/null +++ b/Firebase/Database/Core/View/Filter/FCompleteChildSource.h @@ -0,0 +1,28 @@ +/* + * 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 <Foundation/Foundation.h> + +@protocol FNode; +@class FNamedNode; +@protocol FIndex; + +@protocol FCompleteChildSource<NSObject> + +- (id<FNode>) completeChild:(NSString *)childKey; +- (FNamedNode *) childByIndex:(id<FIndex>)index afterChild:(FNamedNode *)child isReverse:(BOOL)reverse; + +@end diff --git a/Firebase/Database/Core/View/Filter/FIndexedFilter.h b/Firebase/Database/Core/View/Filter/FIndexedFilter.h new file mode 100644 index 0000000..5081a77 --- /dev/null +++ b/Firebase/Database/Core/View/Filter/FIndexedFilter.h @@ -0,0 +1,27 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FNodeFilter.h" + +@protocol FIndex; + + +@interface FIndexedFilter : NSObject<FNodeFilter> + +- (id) initWithIndex:(id<FIndex>)theIndex; + +@end diff --git a/Firebase/Database/Core/View/Filter/FIndexedFilter.m b/Firebase/Database/Core/View/Filter/FIndexedFilter.m new file mode 100644 index 0000000..44c411c --- /dev/null +++ b/Firebase/Database/Core/View/Filter/FIndexedFilter.m @@ -0,0 +1,147 @@ +/* + * 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 "FNode.h" +#import "FIndexedFilter.h" +#import "FChildChangeAccumulator.h" +#import "FIndex.h" +#import "FChange.h" +#import "FChildrenNode.h" +#import "FKeyIndex.h" +#import "FEmptyNode.h" +#import "FIndexedNode.h" + +@interface FIndexedFilter () +@property (nonatomic, strong, readwrite) id<FIndex> index; +@end + +@implementation FIndexedFilter +- (id) initWithIndex:(id<FIndex>)theIndex { + self = [super init]; + if (self) { + self.index = theIndex; + } + return self; +} + +- (FIndexedNode *)updateChildIn:(FIndexedNode *)indexedNode + forChildKey:(NSString *)childKey + newChild:(id<FNode>)newChildSnap + affectedPath:(FPath *)affectedPath + fromSource:(id<FCompleteChildSource>)source + accumulator:(FChildChangeAccumulator *)optChangeAccumulator +{ + NSAssert([indexedNode hasIndex:self.index], @"The index in FIndexedNode must match the index of the filter"); + id<FNode> node = indexedNode.node; + id<FNode> oldChildSnap = [node getImmediateChild:childKey]; + + // Check if anything actually changed. + if ([[oldChildSnap getChild:affectedPath] isEqual:[newChildSnap getChild:affectedPath]]) { + // There's an edge case where a child can enter or leave the view because affectedPath was set to null. + // In this case, affectedPath will appear null in both the old and new snapshots. So we need + // to avoid treating these cases as "nothing changed." + if (oldChildSnap.isEmpty == newChildSnap.isEmpty) { + // Nothing changed. + #ifdef DEBUG + NSAssert([oldChildSnap isEqual:newChildSnap], @"Old and new snapshots should be equal."); + #endif + + return indexedNode; + } + } + if (optChangeAccumulator) { + if (newChildSnap.isEmpty) { + if ([node hasChild:childKey]) { + FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved + indexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap] + childKey:childKey]; + [optChangeAccumulator trackChildChange:change]; + } else { + NSAssert(node.isLeafNode, @"A child remove without an old child only makes sense on a leaf node."); + } + } else if (oldChildSnap.isEmpty) { + FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded + indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap] + childKey:childKey]; + [optChangeAccumulator trackChildChange:change]; + } else { + FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged + indexedNode:[FIndexedNode indexedNodeWithNode:newChildSnap] + childKey:childKey + oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]]; + [optChangeAccumulator trackChildChange:change]; + } + } + if (node.isLeafNode && newChildSnap.isEmpty) { + return indexedNode; + } else { + return [indexedNode updateChild:childKey withNewChild:newChildSnap]; + } +} + +- (FIndexedNode *)updateFullNode:(FIndexedNode *)oldSnap + withNewNode:(FIndexedNode *)newSnap + accumulator:(FChildChangeAccumulator *)optChangeAccumulator +{ + if (optChangeAccumulator) { + [oldSnap.node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) { + if (![newSnap.node hasChild:childKey]) { + FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildRemoved + indexedNode:[FIndexedNode indexedNodeWithNode:childNode] + childKey:childKey]; + [optChangeAccumulator trackChildChange:change]; + } + }]; + + [newSnap.node enumerateChildrenUsingBlock:^(NSString *childKey, id<FNode> childNode, BOOL *stop) { + if ([oldSnap.node hasChild:childKey]) { + id<FNode> oldChildSnap = [oldSnap.node getImmediateChild:childKey]; + if (![oldChildSnap isEqual:childNode]) { + FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildChanged + indexedNode:[FIndexedNode indexedNodeWithNode:childNode] + childKey:childKey + oldIndexedNode:[FIndexedNode indexedNodeWithNode:oldChildSnap]]; + [optChangeAccumulator trackChildChange:change]; + } + } else { + FChange *change = [[FChange alloc] initWithType:FIRDataEventTypeChildAdded + indexedNode:[FIndexedNode indexedNodeWithNode:childNode] + childKey:childKey]; + [optChangeAccumulator trackChildChange:change]; + } + }]; + } + return newSnap; +} + +- (FIndexedNode *)updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap +{ + if ([oldSnap.node isEmpty]) { + return oldSnap; + } else { + return [oldSnap updatePriority:priority]; + } +} + +- (BOOL) filtersNodes { + return NO; +} + +- (id<FNodeFilter>) indexedFilter { + return self; +} + +@end diff --git a/Firebase/Database/Core/View/Filter/FLimitedFilter.h b/Firebase/Database/Core/View/Filter/FLimitedFilter.h new file mode 100644 index 0000000..1690980 --- /dev/null +++ b/Firebase/Database/Core/View/Filter/FLimitedFilter.h @@ -0,0 +1,26 @@ +/* + * 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 <Foundation/Foundation.h> +#import "FNodeFilter.h" + +@class FQueryParams; + + +@interface FLimitedFilter : NSObject<FNodeFilter> + +- (id) initWithQueryParams:(FQueryParams *)params; +@end diff --git a/Firebase/Database/Core/View/Filter/FLimitedFilter.m b/Firebase/Database/Core/View/Filter/FLimitedFilter.m new file mode 100644 index 0000000..8bc6e87 --- /dev/null +++ b/Firebase/Database/Core/View/Filter/FLimitedFilter.m @@ -0,0 +1,243 @@ +/* + * 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<FIndex> 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<FNode>)newChildSnap + affectedPath:(FPath *)affectedPath + fromSource:(id<FCompleteChildSource>)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<FNode>)newChildSnap + fromSource:(id<FCompleteChildSource>)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<FNode> 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<FNode> 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<FNode>)priority forNode:(FIndexedNode *)oldSnap +{ + // Don't support priorities on queries. + return oldSnap; +} + +- (BOOL) filtersNodes { + return YES; +} + +- (id<FNodeFilter>) indexedFilter { + return self.rangedFilter.indexedFilter; +} + +@end diff --git a/Firebase/Database/Core/View/Filter/FNodeFilter.h b/Firebase/Database/Core/View/Filter/FNodeFilter.h new file mode 100644 index 0000000..f29a85a --- /dev/null +++ b/Firebase/Database/Core/View/Filter/FNodeFilter.h @@ -0,0 +1,71 @@ +/* + * 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 <Foundation/Foundation.h> + +@protocol FNode; +@class FIndexedNode; +@protocol FCompleteChildSource; +@class FChildChangeAccumulator; +@protocol FIndex; +@class FPath; + +/** +* FNodeFilter is used to update nodes and complete children of nodes while applying queries on the fly and keeping +* track of any child changes. This class does not track value changes as value changes depend on more than just the +* node itself. Different kind of queries require different kind of implementations of this interface. +*/ +@protocol FNodeFilter<NSObject> + +/** +* Update a single complete child in the snap. If the child equals the old child in the snap, this is a no-op. +* The method expects an indexed snap. +*/ +- (FIndexedNode *) updateChildIn:(FIndexedNode *)oldSnap + forChildKey:(NSString *)childKey + newChild:(id<FNode>)newChildSnap + affectedPath:(FPath *)affectedPath + fromSource:(id<FCompleteChildSource>)source + accumulator:(FChildChangeAccumulator *)optChangeAccumulator; + +/** +* Update a node in full and output any resulting change from this complete update. +*/ +- (FIndexedNode *) updateFullNode:(FIndexedNode *)oldSnap + withNewNode:(FIndexedNode *)newSnap + accumulator:(FChildChangeAccumulator *)optChangeAccumulator; + +/** +* Update the priority of the root node +*/ +- (FIndexedNode *) updatePriority:(id<FNode>)priority forNode:(FIndexedNode *)oldSnap; + +/** +* Returns true if children might be filtered due to query critiera +*/ +- (BOOL) filtersNodes; + +/** +* Returns the index filter that this filter uses to get a NodeFilter that doesn't filter any children. +*/ +@property (nonatomic, strong, readonly) id<FNodeFilter> indexedFilter; + +/** +* Returns the index that this filter uses +*/ +@property (nonatomic, strong, readonly) id<FIndex> index; + +@end |