aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Database/Core
diff options
context:
space:
mode:
Diffstat (limited to 'Firebase/Database/Core')
-rw-r--r--Firebase/Database/Core/FCompoundHash.h40
-rw-r--r--Firebase/Database/Core/FCompoundHash.m236
-rw-r--r--Firebase/Database/Core/FListenProvider.h33
-rw-r--r--Firebase/Database/Core/FListenProvider.m26
-rw-r--r--Firebase/Database/Core/FPersistentConnection.h78
-rw-r--r--Firebase/Database/Core/FPersistentConnection.m945
-rw-r--r--Firebase/Database/Core/FQueryParams.h59
-rw-r--r--Firebase/Database/Core/FQueryParams.m372
-rw-r--r--Firebase/Database/Core/FQuerySpec.h36
-rw-r--r--Firebase/Database/Core/FQuerySpec.m85
-rw-r--r--Firebase/Database/Core/FRangeMerge.h35
-rw-r--r--Firebase/Database/Core/FRangeMerge.m107
-rw-r--r--Firebase/Database/Core/FRepo.h76
-rw-r--r--Firebase/Database/Core/FRepo.m1116
-rw-r--r--Firebase/Database/Core/FRepoInfo.h34
-rw-r--r--Firebase/Database/Core/FRepoInfo.m115
-rw-r--r--Firebase/Database/Core/FRepoManager.h32
-rw-r--r--Firebase/Database/Core/FRepoManager.m131
-rw-r--r--Firebase/Database/Core/FRepo_Private.h42
-rw-r--r--Firebase/Database/Core/FServerValues.h30
-rw-r--r--Firebase/Database/Core/FServerValues.m93
-rw-r--r--Firebase/Database/Core/FSnapshotHolder.h27
-rw-r--r--Firebase/Database/Core/FSnapshotHolder.m46
-rw-r--r--Firebase/Database/Core/FSparseSnapshotTree.h34
-rw-r--r--Firebase/Database/Core/FSparseSnapshotTree.m144
-rw-r--r--Firebase/Database/Core/FSyncPoint.h66
-rw-r--r--Firebase/Database/Core/FSyncPoint.m257
-rw-r--r--Firebase/Database/Core/FSyncTree.h61
-rw-r--r--Firebase/Database/Core/FSyncTree.m817
-rw-r--r--Firebase/Database/Core/FWriteRecord.h40
-rw-r--r--Firebase/Database/Core/FWriteRecord.m117
-rw-r--r--Firebase/Database/Core/FWriteTree.h63
-rw-r--r--Firebase/Database/Core/FWriteTree.m458
-rw-r--r--Firebase/Database/Core/FWriteTreeRef.h51
-rw-r--r--Firebase/Database/Core/FWriteTreeRef.m133
-rw-r--r--Firebase/Database/Core/Operation/FAckUserWrite.h35
-rw-r--r--Firebase/Database/Core/Operation/FAckUserWrite.m55
-rw-r--r--Firebase/Database/Core/Operation/FMerge.h30
-rw-r--r--Firebase/Database/Core/Operation/FMerge.m71
-rw-r--r--Firebase/Database/Core/Operation/FOperation.h34
-rw-r--r--Firebase/Database/Core/Operation/FOperationSource.h34
-rw-r--r--Firebase/Database/Core/Operation/FOperationSource.m73
-rw-r--r--Firebase/Database/Core/Operation/FOverwrite.h30
-rw-r--r--Firebase/Database/Core/Operation/FOverwrite.m62
-rw-r--r--Firebase/Database/Core/Utilities/FIRRetryHelper.h33
-rw-r--r--Firebase/Database/Core/Utilities/FIRRetryHelper.m139
-rw-r--r--Firebase/Database/Core/Utilities/FImmutableTree.h51
-rw-r--r--Firebase/Database/Core/Utilities/FImmutableTree.m421
-rw-r--r--Firebase/Database/Core/Utilities/FPath.h45
-rw-r--r--Firebase/Database/Core/Utilities/FPath.m298
-rw-r--r--Firebase/Database/Core/Utilities/FTree.h48
-rw-r--r--Firebase/Database/Core/Utilities/FTree.m183
-rw-r--r--Firebase/Database/Core/Utilities/FTreeNode.h25
-rw-r--r--Firebase/Database/Core/Utilities/FTreeNode.m36
-rw-r--r--Firebase/Database/Core/View/FCacheNode.h44
-rw-r--r--Firebase/Database/Core/View/FCacheNode.m60
-rw-r--r--Firebase/Database/Core/View/FCancelEvent.h30
-rw-r--r--Firebase/Database/Core/View/FCancelEvent.m55
-rw-r--r--Firebase/Database/Core/View/FChange.h38
-rw-r--r--Firebase/Database/Core/View/FChange.m65
-rw-r--r--Firebase/Database/Core/View/FChildEventRegistration.h37
-rw-r--r--Firebase/Database/Core/View/FChildEventRegistration.m92
-rw-r--r--Firebase/Database/Core/View/FDataEvent.h39
-rw-r--r--Firebase/Database/Core/View/FDataEvent.m74
-rw-r--r--Firebase/Database/Core/View/FEvent.h27
-rw-r--r--Firebase/Database/Core/View/FEventRaiser.h35
-rw-r--r--Firebase/Database/Core/View/FEventRaiser.m72
-rw-r--r--Firebase/Database/Core/View/FEventRegistration.h36
-rw-r--r--Firebase/Database/Core/View/FKeepSyncedEventRegistration.h28
-rw-r--r--Firebase/Database/Core/View/FKeepSyncedEventRegistration.m64
-rw-r--r--Firebase/Database/Core/View/FValueEventRegistration.h34
-rw-r--r--Firebase/Database/Core/View/FValueEventRegistration.m89
-rw-r--r--Firebase/Database/Core/View/FView.h53
-rw-r--r--Firebase/Database/Core/View/FView.m223
-rw-r--r--Firebase/Database/Core/View/FViewCache.h35
-rw-r--r--Firebase/Database/Core/View/FViewCache.m61
-rw-r--r--Firebase/Database/Core/View/Filter/FChildChangeAccumulator.h28
-rw-r--r--Firebase/Database/Core/View/Filter/FChildChangeAccumulator.m80
-rw-r--r--Firebase/Database/Core/View/Filter/FCompleteChildSource.h28
-rw-r--r--Firebase/Database/Core/View/Filter/FIndexedFilter.h27
-rw-r--r--Firebase/Database/Core/View/Filter/FIndexedFilter.m147
-rw-r--r--Firebase/Database/Core/View/Filter/FLimitedFilter.h26
-rw-r--r--Firebase/Database/Core/View/Filter/FLimitedFilter.m243
-rw-r--r--Firebase/Database/Core/View/Filter/FNodeFilter.h71
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